Promote circle-vs-Rect detection and the x-fraction/rect-to-CSS helpers from games/breakout into Core, at RM-034's second real caller
Context
Phase 0 of RM-034 (glemy's third reference game, a platformer, chosen specifically to produce a real second data point on two Core generalizations decision 0052 explicitly deferred: whether physics.update's restitution composition generalizes, and whether rectangle collision deserves promotion into glemy/physics). Before writing any platformer-specific code, direct comparison against what a platformer's player-vs-platform collision needs confirmed games/breakout/rect.gleam's Rect/nearest_point/overlaps/penetration_axis are already game-agnostic circle-vs-AABB math with nothing Breakout-specific in them -- the only thing that differs between Breakout (reflect off a brick) and a platformer (stop dead on a platform) is what happens after detection, exactly the same detection-vs-resolution split physics/collision.gleam already draws against collision_sweep's caller-supplied interact. Critically, a platformer cannot import games/breakout/rect.gleam directly -- one game reaching into another game's own games/<name>/ tree would violate the exact boundary decision 0052 established -- so the only honest choices were duplicate the identical ~90 lines a second time, or promote them. Same reasoning applied to game_breakout.gleam's x_fraction/rect_to_css, both already documented as 'kept local, exactly one real caller.'
Options considered
Decision
Moved games/breakout/rect.gleam to physics/rect.gleam verbatim (Rect, nearest_point, overlaps, Axis/penetration_axis), doc comments updated to describe it as Core rather than game-specific, and its own test file relocated to test/glemy/physics/rect_test.gleam. games/breakout/brick.gleam, paddle.gleam, and breakout.gleam updated to import glemy/physics/rect instead. Added bounds.x_fraction(world_x, bounds) -> Float to physics/bounds.gleam (moved verbatim from game_breakout.gleam, now documented as y_fraction_from_top's x-axis sibling) and rect.to_css(rect, bounds) -> #(Float, Float, Float, Float) to physics/rect.gleam (moved verbatim from game_breakout.gleam's rect_to_css, renamed to fit its new home). game_breakout.gleam deleted both local functions and calls the promoted Core versions instead; its own x_fraction/rect_to_css tests relocated into test/glemy/physics/bounds_test.gleam and rect_test.gleam respectively. No public API changed shape anywhere in Breakout -- Brick.rect, paddle_rect, etc. keep their existing field types, now typed against physics/rect.Rect instead of games/breakout/rect.Rect.
Verification
gleam test and gleam test --target javascript both pass with identical counts to before this phase (263 Erlang / 288 JavaScript) -- a pure relocation with tests moved, not added or removed, is exactly the expected regression proof. deno task check-warnings passes after updating the baseline (76 warnings, every new/changed entry confirmed as the already-documented @target(javascript) target-gating artifact under new file paths, plus one genuine improvement -- an unused type import this refactor incidentally fixed). deno task browser-check-breakout passes for real (57 real requestAnimationFrame frames, a real held ArrowLeft pinned the paddle at the left wall, a real ball-brick collision hid a brick div and updated the score, 1 sound played, zero page errors) -- confirms Breakout's actual runtime behavior is byte-for-byte unchanged after the refactor, not just that it type-checks. deno task browser-check (Tiers) also re-run since physics/bounds.gleam was touched -- still passes for real (185 frames, a real click-and-hold grew the entity count, zero page errors).
Consequences
physics/rect.gleam is now Core, alongside physics/bounds.gleam/collision.gleam/collision_sweep.gleam/entity.gleam -- the first Core module added since the original Tiers-to-Breakout generalization round (decisions 0047-0050), and the first one promoted with a second caller's real evidence in hand from the start rather than discovered after the fact. games/platformer (Phase 1, next) builds its own player-vs-platform collision directly on physics/rect's detection functions, writing only its own resolution logic (stop, not reflect) -- the actual point of this promotion. A fourth game needing the same detection math has zero further Core work to do; a fourth game needing genuinely different detection math (rect-vs-rect, say) is real new evidence to design from, not a guess.
References
- src/glemy/physics/rect.gleam
- src/glemy/physics/bounds.gleam
- src/glemy/games/breakout/brick.gleam
- src/glemy/games/breakout/paddle.gleam
- src/glemy/games/breakout.gleam
- src/glemy/game_breakout.gleam
- test/glemy/physics/rect_test.gleam
- test/glemy/physics/bounds_test.gleam
- test/glemy/game_breakout_test.gleam
- docs/development-plan.jsonl