← All decisions

Danger line / game over: plain continuous-time threshold, tick freezes once lost

merge-gamegame-overlose-condition

Context

Phase 6 of the approved merge-puzzler plan: the game needs an actual lose condition, or a full game loop with no way to lose isn't really a game yet. The reference game's own rule: if the stack builds up above a line near the top and stays there continuously for a few seconds, the run ends.

Options considered

Gate the danger timer on 'the most recently dropped piece has finished settling' (matching the reference game's own behavior, which pauses the timer while a fresh drop is still actively falling through the danger zone) — rejected
Requires tracking per-entity settling state (e.g. a velocity-below-threshold check, or explicitly tagging the most-recent drop) that nothing in this codebase currently models -- real added complexity for a distinction that's only observable in fairly specific circumstances (a piece dropped exactly through the danger line while it's still falling fast). Deliberately deferred as a documented simplification rather than silently ignored.
A plain continuous-time threshold: any frame with at least one entity above the danger line adds dt to a running timer; any frame with none resets it to zero; crossing a fixed threshold ends the game — chosen
Minimal, obviously correct, fully testable with plain Model/Input values and no new concepts -- and functionally equivalent to the reference game's behavior for the actual failure case that matters (a stack that's genuinely too tall, not draining back down), which is what a lose condition needs to catch.

Decision

pe.Model gained danger_timer: Float and game_over: Bool. Two new private constants: danger_line_margin_from_top (5.0, deliberately smaller than spawn_margin_from_top's 15.0, so a freshly dropped entity spawns *below* the danger line and doesn't immediately trip it) and danger_timer_threshold (3.0 seconds, matching the reference game). pe.tick, after running the existing drop/physics step, checks whether any entity's post-update position.y is above danger_line_y(bounds) (bounds.max.y - danger_line_margin_from_top): if so, danger_timer accumulates by dt; otherwise it resets to 0.0. game_over becomes True the instant danger_timer strictly exceeds the threshold (not >=; landing exactly on it is not yet game over). Once model.game_over is True, tick is wrapped in a case that makes every subsequent call a complete no-op, returning the input model completely unchanged -- the simulation freezes at the exact moment of loss, consistent with this project's earlier-confirmed reload-to-restart decision (no in-place reset exists, so nothing would ever need to un-freeze it).

Verification

132 Erlang / 166 JavaScript tests passing (up from 127/161), all additive, zero regressions -- every pre-existing Model(...) construction across the whole test suite got the two new fields mechanically (danger_timer: 0.0, game_over: False), verified inert by the full pre-existing suite passing unchanged. Five new pe_test.gleam tests cover: the timer accumulating while an entity sits above the line; the timer resetting to zero the moment nothing is above it (even from a nonzero starting value, proving it's a real reset not just a default); game_over flipping True once the timer strictly exceeds the 3.0s threshold (using exact power-of-two-fraction float values -- 2.5 + 0.75 = 3.25 -- specifically to avoid any floating-point-tolerance ambiguity in the assertion); game_over staying False exactly at the threshold (2.0 + 1.0 = 3.0), pinning the strict-inequality boundary deliberately; and tick being a full, exact no-op (structural equality against the original Model, including entities, score, cooldown, and preview state) once game_over is already True, even when given a large dt and an otherwise-drop-triggering Input. Re-ran tools/browser_check.ts to confirm nothing about the real drop/render loop broke.

Consequences

Any future feature reading Model must now account for game_over possibly being True and the simulation being frozen -- Phase 7's HTML overlay is the immediate next consumer, showing a game-over message once this flips. The settling-aware timer gating (rejected above) remains a documented, available refinement if the plain continuous-time version ever proves too punishing or too lenient during Phase 8 tuning.

References