Build Breakout's pure game logic (glemy/games/breakout, rect/brick/paddle) with rectangles kept entirely outside physics, and confirm physics.update does not generalize to a second genre
Context
Phase 1 of glemy's second reference game (a Breakout/Arkanoid-style paddle-and-brick game, chosen specifically to stress-test the genuinely genre-agnostic claims established across decisions 0047-0050 with a real second, genre-distinct consumer). The user confirmed the scope explicitly: rectangles (the paddle, bricks) stay entirely inside glemy/games/breakout, never becoming physics.Model entities and never touching physics/entity.gleam/collision.gleam/collision_sweep.gleam/render.gleam -- consistent with this project's repeated 'don't generalize Core from one caller' discipline. Building the ball's own motion surfaced a real, previously-unverified limit in the existing Core API: physics/bounds.bounce's restitution ramp (decision 0029, ~10% rebound at genuine impact speed) is tuned specifically for Tiers' fruit to 'dead-plop', not a generically reusable elastic bounce -- reusing physics.update for the ball as originally assumed would have made it lose ~90% of its speed on the first wall hit.
Options considered
Decision
Built glemy/games/breakout/rect.gleam (a plain Rect type plus the standard clamp-to-nearest-point circle-vs-AABB overlap test and penetration-axis detection), glemy/games/breakout/brick.gleam (brick grid layout, per-brick score, reflect, and resolve_first_hit -- resolving at most one brick collision per frame, matching physics/collision_sweep.resolve_target_against_rest's own 'resolve one interaction and move on' philosophy), glemy/games/breakout/paddle.gleam (held-key movement reusing physics/bounds.clamp_x directly via the half-width-as-radius equivalence, and a trig-free hit-position-dependent bounce reflection that preserves total ball speed), and glemy/games/breakout.gleam (the hub: Model/GameStatus/GameEvent/Input/tick/new). tick composes physics/entity.integrate and physics/collision_sweep.resolve_all_collisions directly, with a bespoke bounce_off_walls function (near-full-elastic on left/right/top, nothing at the bottom -- what makes falling past bounds.min.y a lose trigger rather than a wall) instead of calling physics.update. GameStatus is a three-way sum type (Playing/Won/Lost), not two independent Bools, ruling out the nonsensical 'both true' state Gleam's type system can reject for free.
Verification
gleam test and gleam test --target javascript both pass -- 253 Erlang / 276 JavaScript, exactly the prior baseline (195/218 as of decision 0051) plus 58 new tests across rect_test/brick_test/paddle_test/breakout_test.gleam, all pure Gleam with no FFI dependency, so both counts grew identically. deno task check-warnings passes with the baseline completely unchanged (51/51, zero new warnings) -- nothing in this phase touches @target(javascript)-gated code. deno task browser-check was not re-run for this phase: nothing here touches glemy/game-shaped rendering or real-browser behavior yet (only pure game logic, no runner wiring), matching CLAUDE.md's own scoping for when that check is required -- it will run again once Phase 2 wires this up to real rendering and input.
Consequences
glemy/games/breakout is now a complete, independently-tested game engine's worth of pure logic with no rendering or FFI wiring yet -- the next phase (rendering the ball via the existing WebGPU pipeline, the paddle/bricks via a CSS-div overlay, and wiring real keyboard input) has a fully verified foundation to build on. The physics.update negative result is now a documented, evidenced fact about this project's Core API rather than an assumption: a future third game's own tick function should expect to compose physics/entity.integrate and physics/collision_sweep.resolve_all_collisions directly if it needs anything other than Tiers' specific restitution tuning, not assume physics.update is a drop-in generic physics step.
References
- src/glemy/games/breakout.gleam
- src/glemy/games/breakout/rect.gleam
- src/glemy/games/breakout/brick.gleam
- src/glemy/games/breakout/paddle.gleam
- test/glemy/games/breakout_test.gleam
- test/glemy/games/breakout/rect_test.gleam
- test/glemy/games/breakout/brick_test.gleam
- test/glemy/games/breakout/paddle_test.gleam
- src/glemy/physics.gleam
- src/glemy/physics/bounds.gleam
- docs/technical-architecture.md