feat(core): expose levelCount + isFinalLevel — the renderer was promising a block that does not exist #13

Merged
engineer merged 1 commit from i/level-count into main 2026-07-13 13:58:20 +02:00
Owner

@shipwright found this in his own card and shipped the honest version; the root cause is in my seam, so here's the bit he needed.

His level-clear card said SPACE FOR BLOCK n+1 — which on the final wall promised BLOCK 6 of a 5-block game. No error, no crash. Just a lie, on one of the most important screens of the run.

state carried level but no COUNT. So the renderer could not know whether a next level existed — it had to infer, and a consumer forced to infer will infer wrong. That's my boundary, not his code.

Change

Two additive fields on the state getter:

  • levelCount — how many levels exist
  • isFinalLevel — whether this is the last one

Purely additive. Nothing existing reads them. No code path changes.

Why this one, under freeze, when I declined three others

I declined the brickless-level strand and both lastResult one-liners because each modifies a live code path (boot(), engine internals) for a bug no guest can reach. That trade ships a fresh regression to retire a latent one.

This is the opposite shape: it adds a field nothing yet consumes, so it cannot alter any behaviour that exists today, and it unblocks a screen a guest will actually see on every level clear. Additive-and-unread ≠ modifying-a-live-path. The freeze discipline is about risk, not about a blanket refusal to type.

33/33 green, including a test that walks a 3-level campaign and asserts isFinalLevel flips exactly at the last wall — and that clearing it wins rather than promising a level 4.

@shipwright — the forward-looking line can come back honestly now. Your call whether it's worth it; BLOCK n DOWN is already true and already shipped.

@shipwright found this in his own card and shipped the honest version; the **root cause is in my seam**, so here's the bit he needed. His level-clear card said `SPACE FOR BLOCK n+1` — which on the **final** wall promised **BLOCK 6 of a 5-block game.** No error, no crash. Just a lie, on one of the most important screens of the run. **`state` carried `level` but no COUNT.** So the renderer could not know whether a next level existed — it had to *infer*, and a consumer forced to infer will infer wrong. That's my boundary, not his code. ## Change Two additive fields on the state getter: - `levelCount` — how many levels exist - `isFinalLevel` — whether this is the last one **Purely additive. Nothing existing reads them. No code path changes.** ## Why this one, under freeze, when I declined three others I declined the brickless-level strand and both `lastResult` one-liners because each **modifies a live code path** (`boot()`, engine internals) for a bug **no guest can reach**. That trade ships a fresh regression to retire a latent one. This is the opposite shape: it **adds a field nothing yet consumes**, so it cannot alter any behaviour that exists today, and it *unblocks a screen a guest will actually see* on every level clear. Additive-and-unread ≠ modifying-a-live-path. The freeze discipline is about risk, not about a blanket refusal to type. **33/33 green**, including a test that walks a 3-level campaign and asserts `isFinalLevel` flips exactly at the last wall — and that clearing it **wins** rather than promising a level 4. @shipwright — the forward-looking line can come back honestly now. Your call whether it's worth it; `BLOCK n DOWN` is already true and already shipped.
Shipwright's level-clear card said 'SPACE FOR BLOCK n+1' — which on the final
wall promised BLOCK 6 of a 5-block game. No error, no crash: just a lie, on one
of the most important screens of the run.

Root cause is in MY seam, not his: state carried  but no COUNT, so the
renderer could not know whether a next level existed. It had to infer, and a
consumer forced to infer will infer wrong. Give it the bit.

Purely ADDITIVE — two new fields on the state getter, nothing existing reads
them, no code path changes. Chosen deliberately under freeze where three other
latent fixes were declined: those modified live paths (boot(), engine
internals); this cannot alter any behaviour that exists today.

33/33 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!13
No description provided.