Extend the Core API with three small utility modules (cooldown, stopwatch, random) and physics.entity_count, and reject a generic PreviewQueue abstraction
Context
The user requested proactively growing the Core API to give the architecture 'the feel of a real game engine' and to simplify glemy/games/tiers.gleam by building on top of it, rather than waiting for a second game to justify further extraction -- a deliberate departure from decisions 0047/0048's stricter build-nothing-ahead-of-need posture, explicitly requested and research-backed rather than assumed. A close read of games/tiers.gleam's tick function found three genuinely generic, already-duplicated-in-spirit patterns: a countdown timer (cooldown_remaining), a hold-while-condition-then-reset accumulator (danger_timer), and generic index-selection-from-a-list math already hand-rolled Int-only inside games/tiers/rules.random_droppable. A fourth candidate, generalizing the current_tier/next_tier two-ahead preview-advance pattern into a reusable PreviewQueue(a), was also seriously considered.
Options considered
Decision
Added three new top-level Core modules, siblings to glemy/physics/glemy/render/glemy/io rather than nested under physics/ (every engine surveyed -- Bevy's bevy_time vs. its physics crates, Godot's Timer node vs. PhysicsServer2D, LOVE2D's love.timer/love.math vs. love.physics -- keeps timer/random utilities separate from physics, direct precedent for not re-diluting the physics name decision 0048 just fixed): glemy/cooldown.gleam (tick(remaining, dt) -> Float, floored at 0.0; is_ready(remaining) -> Bool), glemy/stopwatch.gleam (tick(elapsed, dt, holding) -> Float, accumulates while holding else resets to 0.0, no threshold concept), and glemy/random.gleam (pick(random, options) -> Result(a, Nil), a verified byte-for-byte generalization of random_droppable's existing index-selection algorithm, taking the random float as a plain argument rather than generating it internally -- validated against Godot's own confirmed, documented seeded-RNG-non-reproducibility problem across engine versions, godot/godot#27856, as a concrete reason never to let a Core utility own random state). Added physics.entity_count(model) -> Int. Rewired games/tiers/rules.random_droppable into a thin wrapper (random.pick |> result.unwrap(0)) and games/tiers.tick's three corresponding expressions (cooldown_remaining, at_capacity, danger_timer) to call the new primitives -- Model's field types unchanged (still plain Float/Int), so no test file outside the new modules' own required any edit.
Verification
gleam test and gleam test --target javascript both pass -- 186 Erlang / 209 JavaScript, exactly the prior baseline (164/187) plus the new modules' own test counts (8 random_test + 7 cooldown_test + 5 stopwatch_test + 2 physics_test additions), with zero changes to any pre-existing test file's pass count, confirming the rewiring of games/tiers.gleam was behavior-preserving by construction, not just by inspection. deno task check-warnings passes with the existing baseline completely unchanged (51/51, zero new warnings) -- none of the new modules are FFI or @target(javascript)-gated, so none produce the usual target-gating artifact. deno task browser-check passes for real (180 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 the actual running game still behaves identically through the rewired tick path.
Consequences
glemy/games/tiers.tick now reads as composition of named Core primitives (cooldown.tick, stopwatch.tick, physics.entity_count) rather than hand-rolled float arithmetic inline, the concrete 'feel of a real engine' improvement this decision was made to produce. A second, genre-distinct game gets the same three primitives for free, without needing to re-derive countdown/accumulate/random-pick math from scratch. The explicitly rejected PreviewQueue(a) remains a real, named candidate for revisiting once a second game's own tick demonstrates the same shape -- not silently dropped, written down here so a future reader doesn't have to rediscover why it wasn't built now.
References
- src/glemy/cooldown.gleam
- src/glemy/stopwatch.gleam
- src/glemy/random.gleam
- src/glemy/physics.gleam
- src/glemy/games/tiers.gleam
- src/glemy/games/tiers/rules.gleam
- test/glemy/cooldown_test.gleam
- test/glemy/stopwatch_test.gleam
- test/glemy/random_test.gleam
- docs/technical-architecture.md
- ARCHITECTURE.md
- https://docs.rs/bevy/latest/bevy/time/struct.Stopwatch.html
- https://docs.rs/bevy/latest/bevy/time/struct.Timer.html
- https://bevy-cheatbook.github.io/fundamentals/time.html
- https://github.com/godotengine/godot/issues/27856