Build the platformer's pure game logic (glemy/games/platformer), confirming a third distinct wall/landing policy and a real Landed-event design bug found by direct testing
Context
RM-034 Phase 1: glemy's third reference game's own Model/tick, built on top of Phase 0's promoted physics/rect detection math (decision 0058). The headline question this whole game was chosen to answer: does any single wall/landing restitution policy generalize across all three games. Tiers damps to ~10% (physics/bounds.bounce, decision 0029), Breakout reflects near-elastically (bounce_off_walls, decision 0052) -- a platformer needs a third, genuinely distinct policy: landing on a platform or hitting a side wall must stop the player dead (zero restitution), not bounce at all. While implementing the Landed GameEvent (fired when the player transitions from falling to resting on a platform), direct test design surfaced a real logic bug before it shipped: the first-draft implementation decided 'was this a landing' by checking whether the player's velocity.y was negative at the instant of collision resolution -- but under continuous gravity integration, a player resting motionless on a platform still picks up a tiny negative vy every single tick (gravity acts for one tick before the very next tick's collision resolution catches and re-zeroes it), so that check would have fired Landed on every tick a player stood still, not just the real landing transition.
Options considered
Decision
Added glemy/games/platformer.gleam: GameStatus (Playing/Won/Lost, same sum-type reasoning as Breakout's), Model (player: Entity, bounds, platforms: List(Rect), goal: Rect, grounded: Bool, status), GameEvent (Jumped/Landed/Fell), Input (move_left/move_right/jump), tick, new, colored_player. tick composes entity.integrate directly (bypassing physics.update, same reasoning as Breakout's own tick) with three bespoke, zero-restitution resolution functions: stop_at_walls (side walls and a soft ceiling stop the player dead, the bottom edge is left untouched entirely -- same 'falling past bounds.min.y is the lose trigger, not a wall' convention as Breakout), resolve_platforms/resolve_platform_hit (resolves at most one overlapping platform per frame, same simplification as brick.resolve_first_hit -- landing snaps position and zeroes vertical velocity and sets grounded, hitting the underside stops upward velocity only, hitting a side pushes back to the nearer edge and zeroes horizontal velocity). Jump is gated on the *incoming* model.grounded (read before this tick's own motion can overwrite it), not a same-tick freshly-computed value -- a deliberate MVP simplification (no same-tick land-then-jump chaining) noted as a future tuning candidate if it feels wrong in play. A small, fixed six-platform staircase level plus a goal rectangle, MVP-scoped identically to Breakout's own single-life, no-lives-counter precedent: no score, no hazards beyond falling off the bottom, no coyote-time/variable-jump-height/one-way-platforms.
Verification
gleam test and gleam test --target javascript both pass -- 281 Erlang / 306 JavaScript, exactly the prior baseline (263/288 as of decision 0058) plus 18 new tests, all pure Gleam with zero FFI dependency, so both counts grew identically. The Landed-event bug was caught and fixed before any test ran (by reasoning through the exact frame trace during test-case design), then a dedicated regression test (tick_does_not_refire_landed_while_already_resting_test) was written specifically to pin the corrected behavior -- a resting player integrated through one more real (nonzero-dt) tick produces zero events, not [Landed]. deno task check-warnings passes with the baseline completely unchanged (76/76, zero new warnings) -- nothing in this phase touches @target(javascript)-gated code.
Consequences
glemy/games/platformer is now a complete, independently-tested game engine's worth of pure logic with no rendering or FFI wiring yet, mirroring Breakout's own Phase 1 checkpoint -- Phase 2 (rendering/input wiring) has a fully verified foundation to build on. The wall/landing-policy negative result is now backed by three independent data points instead of one: physics.update's composition remains correctly not a drop-in generic physics step for any game with non-default wall behavior, exactly as decisions 0052/0058 already concluded, now demonstrated a third distinct way (zero restitution, not just a different degree of bounce). The grounded-state-transition pattern for detecting a discrete physical event from continuous simulation state (rather than trusting a single frame's transient derivative) is a real, reusable technique a future game's own tick function should reach for first, not rediscover, if it needs a similar one-shot event derived from ongoing physics.