← All decisions

Split glemy/game.gleam into glemy/game_tiers.gleam plus a shared game_ffi.mjs, in preparation for a second game runner

architecturesecond-gamerefactor

Context

Starting work on glemy's second reference game (a Breakout/Arkanoid-style paddle-and-brick game, chosen specifically to stress-test the Core API's genericity claims established across decisions 0047-0050 with a genuinely genre-distinct second consumer). glemy/game.gleam's own doc comment already anticipated this moment explicitly ('a second game would get its own runner shaped like this one, gluing io/render to its own tick function'), but the module and its FFI file were both named and shaped for exactly one game. game_ffi.mjs itself was a mix of genuinely generic runner infrastructure (requestFrame, its error-reporting wrapper, canvas-layout conversion for input) and Tiers-specific DOM/audio writes (setScoreText, setGameOver, setDangerLinePosition, setNextTierColor, playDropSound, playMergeSound, triggerMergeFlash) that a second game's runner has no use for and shouldn't import.

Options considered

Leave game.gleam/game_ffi.mjs as-is and have the second game's runner import selectively from them — rejected
Would leave game_ffi.mjs's exports permanently ambiguous about which are safe for any runner to use versus specific to Tiers' own DOM elements (#glemy-score, #glemy-next-tier, etc.) -- a second game's runner importing setNextTierColor by mistake would compile fine and fail only at runtime against elements that don't exist on its own page. Splitting the file makes the generic/specific boundary a real module boundary, not a convention someone has to remember.
Keep one game.gleam file and add a branch/mode parameter to select which game it drives — rejected
Directly contradicts this project's own established Core/Shell discipline (ARCHITECTURE.md's FFI-minimality rule, decision 0047's whole premise): a runner is Shell wiring for one specific game's tick function, not a generic dispatcher. A second, genre-distinct game has its own Model/Input/GameEvent shapes entirely incompatible with tiers.Model -- a single parameterized runner would need its own internal case-dispatch on which game is active, reintroducing exactly the kind of game-specific branching this project moved out of Core in decisions 0047/0048.

Decision

Renamed glemy/game.gleam to glemy/game_tiers.gleam (mechanical, git mv, mirrors the pe->physics rename precedent) and split glemy/game_ffi.mjs: requestFrame, its error-reporting wrapper, and canvasBoundingRectLeftAndWidth stay in game_ffi.mjs as genuinely generic runner infrastructure every game's runner needs; setScoreText/setGameOver/setDangerLinePosition/setNextTierColor/playDropSound/playMergeSound/triggerMergeFlash (all Tiers-specific DOM/audio writes) moved to a new glemy/game_tiers_ffi.mjs. Test files/helpers renamed to match (game_test.gleam -> game_tiers_test.gleam, game_test_ffi.mjs -> game_tiers_test_ffi.mjs). index.html's module import updated to the renamed compiled output. Established the standing convention (documented in ARCHITECTURE.md): each game gets its own game_<name>.gleam + game_<name>_ffi.mjs, mirroring games/<name>/'s existing one-tree-per-game convention one layer up at the Shell boundary, with game_ffi.mjs remaining the one shared file for genuinely generic runner concerns. While touching these files, also fixed several pre-existing stale doc-comment references found along the way: a duplicated '(now glemy/physics)' artifact left over from the pe->physics rename's own bulk sed (in io.gleam), a stale glemy/pe/bounds reference in tools/browser_check.ts, and a stale clamp_preview_x citation in ARCHITECTURE.md (that function was replaced by physics/bounds.clamp_x in decision 0050).

Verification

gleam test and gleam test --target javascript both pass with identical counts to before this change (195 Erlang / 218 JavaScript) -- confirming the rename and FFI split changed no behavior, only structure. deno task check-warnings passes after updating the baseline (51/51, same count, only paths renamed) -- every diff confirmed as the already-documented @target(javascript) target-gating artifact under its new filename, not a new issue. deno task browser-check passes for real against the renamed runner (182 real requestAnimationFrame frames, a real click-and-hold grew the entity count 4 -> 5, a real entity spawned near the expected world-x, zero page errors), confirming Tiers' actual running game is unaffected end-to-end.

Consequences

glemy/game_ffi.mjs's exports are now unambiguously safe for any future game's runner to import; a second game's own game_<name>_ffi.mjs starts from a clean slate with no risk of accidentally reaching for a Tiers-specific DOM write. This is the last piece of foundational restructuring before the Breakout game's own logic and rendering land in subsequent, separately-verified steps (this project's roadmap tracks it as a multi-phase initiative, not one change).

References