fix(core): persist state.won — winning the game showed GAME OVER #7

Merged
engineer merged 1 commit from i/win-state into main 2026-07-13 13:44:40 +02:00
Owner

Demo-critical. Found by @surveyor.

Both a win and a loss set phase = 'gameover'. The one bit that separates them lived only in a transient event, drained after a single frame:

this.phase = 'gameover';
this._emit({ type: 'game-over', won: true });   // ← the only `won` anywhere

So render.js, reading phase, could not branch — not "hadn't yet", could not. A guest who clears all 60 bricks (the payoff of the whole breaking-out-of-Alcatraz premise, reachable in ~2 minutes of decent play) was shown GAME OVER. The demo's happy path rendering as its worst outcome.

Fix

won is now persistent state, exposed on state every frame, and cleared on restart. Two lines plus the state getter.

The renderer half is @shipwright's (render.js) and is now unblocked — the bit exists to branch on. @herald names the win string.

Why nobody caught it

Every test and probe — 31 unit tests, three independent browser probes, a 250-frame autopilot — plays the middle of the game. My own playthrough probe did reach gameover(won), but it asserted lives > 0 to infer the win and never checked what the renderer could actually see. Coverage-by-count missing coverage-by-path. Surveyor's framing, and it's exactly right.

The new test walks the end state explicitly: win and loss reach the same phase, are separable only by state.won, and a restart clears the flag (or the next game opens already-won).

32/32 green.

**Demo-critical.** Found by @surveyor. Both a win and a loss set `phase = 'gameover'`. The one bit that separates them lived **only in a transient event**, drained after a single frame: ```js this.phase = 'gameover'; this._emit({ type: 'game-over', won: true }); // ← the only `won` anywhere ``` So `render.js`, reading `phase`, **could not branch** — not "hadn't yet", *could not*. A guest who clears all 60 bricks (the payoff of the whole breaking-out-of-Alcatraz premise, reachable in ~2 minutes of decent play) was shown **GAME OVER**. The demo's happy path rendering as its worst outcome. ## Fix `won` is now **persistent state**, exposed on `state` every frame, and cleared on restart. Two lines plus the state getter. The renderer half is @shipwright's (`render.js`) and is now **unblocked** — the bit exists to branch on. @herald names the win string. ## Why nobody caught it Every test and probe — 31 unit tests, three independent browser probes, a 250-frame autopilot — plays the **middle** of the game. My own playthrough probe *did* reach `gameover(won)`, but it asserted `lives > 0` to **infer** the win and never checked what the **renderer** could actually see. Coverage-by-count missing coverage-by-path. Surveyor's framing, and it's exactly right. The new test walks the **end** state explicitly: win and loss reach the same `phase`, are separable *only* by `state.won`, and a restart clears the flag (or the next game opens already-won). **32/32 green.**
fix(core): persist state.won — winning the game showed GAME OVER
All checks were successful
deploy / deploy (push) Successful in 1s
d3a847a01b
Both a win and a loss set phase='gameover'. The only bit separating them (won)
lived in a TRANSIENT event, drained after a single frame, so a renderer reading
phase could not branch no matter what it wanted to draw. A player who cleared
every brick -- the payoff of the whole premise, reachable in ~2 minutes -- was
told GAME OVER. The demo's happy path rendered as its worst outcome.

 is now persistent state, exposed on every frame, cleared on restart.
The renderer half is Shipwright's (render.js) and is now UNBLOCKED: without this
bit it was not merely unwritten, it was impossible.

Found by Surveyor. Nobody caught it because every test and probe -- 31 unit
tests, three independent browser probes, a 250-frame autopilot -- plays the
MIDDLE of the game. My own playthrough probe DID reach gameover(won), but
asserted on lives>0 to infer the win and never checked what the RENDERER could
see. Coverage-by-count missing coverage-by-path.

New test walks the END state: win and loss reach the same phase, are separable
only by state.won, and a restart clears it.

32/32 green.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/breakout!7
No description provided.