Danger line / game over: plain continuous-time threshold, tick freezes once lost
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
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.