feat(render): NEXT preview shows the next-3 piece queue (#53) #55

Merged
bosun merged 2 commits from i/53-next-queue into main 2026-06-21 15:26:14 +02:00
Owner

Closes #53. Polish-queue item (C), after #31 (soft-drop) and #49/#50 ((B) animated bg).

The server already exposes the upcoming-piece queue on the wire (PlayerStateWire.nextQueue, via Engineer #39 + Carpenter #42/#43), but the client only rendered a single NEXT piece. This threads the next-3 through state → render so players can plan ahead.

Changes (consumer-side threading, top to bottom)

  • state.tsPlayerView.nextQueue?: TetrominoType[].
  • net.tsplayerFromWire maps the wire nextQueue (drops pre-spawn '' gaps; stays undefined when the wire omits it).
  • mock.ts — the ?mock engine now keeps a real 3-deep queue (fillQueue + shift on spawn) so its preview truthfully predicts spawn order — not a cosmetic decoration.
  • render.tsdrawNext renders up to 3 pieces stacked.

No render() signature change — the queue rides PlayerView, so this is independent of the #46/#50 render()-param reconciliation.

Design call (decision tree, not just the conclusion)

Chose equal-size vertical 3-stack (cell=14, half-cell gap), top = soonest, in one NEXT panel:

  • Equal-size is right here — clearest read of a 3-queue, unambiguous, and fits the HUD column above the COMBO popup (panel 216→322; COMBO at 350; HOLD sits beside it, untouched).
  • Graduated emphasis (big next-up + 2 smaller, the modern-guideline look) would be right if we had more vertical headroom or wanted to bias attention to the immediate piece — but it's tighter against COMBO and adds visual complexity for a 3-deep queue. Flagging it as a merge-then-tune call for Herald's eye (alpha/scale/emphasis are cheap to revisit, same as the bg knobs).
  • Pieces are left-aligned within the panel, consistent with the existing HOLD box (neither centers).

Graceful degradation

drawNext falls back to the single-next preview when nextQueue is absent/empty — pre-spawn frames and any server build that omits the queue render exactly as before (no regression). Covered by construction (view.next ? [view.next] : []).

Validation (closed loop, on ?mock)

tools/capture-next.js (committed, re-runnable):

NEXT slots before hard-drop: S J L
NEXT slots after  hard-drop: J L S
AC: 3 distinct pieces render: PASS
AC: queue advances FIFO on spawn (slot1/2 → slot0/1): PASS

The FIFO check is the spawn-order proof: the head S is consumed into the active piece, J L shift up, a fresh piece enters the tail — the preview predicts what actually spawns, observed at the render layer (colour-sampled from the canvas). Plus tsc clean + vite build clean. Screenshot of the 3-stack layout sent to the operator for Herald's aesthetic sign-off.

What this PR does NOT do

  • No graduated-emphasis layout (deferred to Herald tuning, above).
  • No NEXT preview for the opponent well — it has no HUD/NEXT panel today (only you calls drawHud); out of scope.
  • Doesn't touch the server/wire — pure consumer-side.

🤖 Generated with Claude Code

Closes #53. Polish-queue item (C), after #31 (soft-drop) and #49/#50 ((B) animated bg). The server already exposes the upcoming-piece queue on the wire (`PlayerStateWire.nextQueue`, via Engineer #39 + Carpenter #42/#43), but the client only rendered a single NEXT piece. This threads the next-3 through `state → render` so players can plan ahead. ### Changes (consumer-side threading, top to bottom) - **state.ts** — `PlayerView.nextQueue?: TetrominoType[]`. - **net.ts** — `playerFromWire` maps the wire `nextQueue` (drops pre-spawn `''` gaps; stays `undefined` when the wire omits it). - **mock.ts** — the `?mock` engine now keeps a **real 3-deep queue** (`fillQueue` + `shift` on spawn) so its preview truthfully predicts spawn order — not a cosmetic decoration. - **render.ts** — `drawNext` renders up to 3 pieces stacked. *No `render()` signature change* — the queue rides `PlayerView`, so this is independent of the #46/#50 render()-param reconciliation. ### Design call (decision tree, not just the conclusion) Chose **equal-size vertical 3-stack** (cell=14, half-cell gap), top = soonest, in one NEXT panel: - **Equal-size is right here** — clearest read of a 3-queue, unambiguous, and fits the HUD column above the COMBO popup (panel 216→322; COMBO at 350; HOLD sits beside it, untouched). - **Graduated emphasis** (big next-up + 2 smaller, the modern-guideline look) **would be right if** we had more vertical headroom or wanted to bias attention to the immediate piece — but it's tighter against COMBO and adds visual complexity for a 3-deep queue. Flagging it as a **merge-then-tune** call for Herald's eye (alpha/scale/emphasis are cheap to revisit, same as the bg knobs). - Pieces are left-aligned within the panel, **consistent with the existing HOLD box** (neither centers). ### Graceful degradation `drawNext` falls back to the single-`next` preview when `nextQueue` is absent/empty — pre-spawn frames and any server build that omits the queue render exactly as before (no regression). Covered by construction (`view.next ? [view.next] : []`). ### Validation (closed loop, on `?mock`) `tools/capture-next.js` (committed, re-runnable): ``` NEXT slots before hard-drop: S J L NEXT slots after hard-drop: J L S AC: 3 distinct pieces render: PASS AC: queue advances FIFO on spawn (slot1/2 → slot0/1): PASS ``` The FIFO check is the spawn-order proof: the head `S` is consumed into the active piece, `J L` shift up, a fresh piece enters the tail — the preview predicts what actually spawns, observed at the render layer (colour-sampled from the canvas). Plus `tsc` clean + `vite build` clean. Screenshot of the 3-stack layout sent to the operator for Herald's aesthetic sign-off. ### What this PR does NOT do - No graduated-emphasis layout (deferred to Herald tuning, above). - No NEXT preview for the **opponent** well — it has no HUD/NEXT panel today (only `you` calls `drawHud`); out of scope. - Doesn't touch the server/wire — pure consumer-side. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Thread the server's nextQueue wire (proto.ts) through to the HUD so the
NEXT panel shows the next 3 upcoming pieces instead of one.

- state.ts: add PlayerView.nextQueue?: TetrominoType[]
- net.ts: map wire nextQueue → view (drop pre-spawn '' gaps; undefined
  when the wire omits it so the single-next fallback still applies)
- mock.ts: keep a real 3-deep queue (fillQueue + shift on spawn) so the
  ?mock preview truthfully predicts spawn order
- render.ts: drawNext renders up to 3 stacked (top = soonest), degrading
  to the single-next preview when nextQueue is absent/empty

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
chore(tools): capture-next.js — #53 next-3 render + spawn-order probe
All checks were successful
test / server (pull_request) Successful in 16s
test / client (pull_request) Successful in 27s
dfee6f71d7
Single-feature validation harness for the next-3 NEXT queue: screenshots
the 3-stack layout, then samples slot colours / hard-drops / re-samples
to assert the queue advances FIFO (preview predicts spawn order). README
row added. Kept with its feature PR so the reviewer can re-run it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
shipwright force-pushed i/53-next-queue from dfee6f71d7
All checks were successful
test / server (pull_request) Successful in 16s
test / client (pull_request) Successful in 27s
to dfb5ca8833
All checks were successful
test / server (pull_request) Successful in 16s
test / client (pull_request) Successful in 24s
2026-06-21 15:21:46 +02:00
Compare
Owner

Surveyor review — APPROVE

Verified at head dfb5ca883347eeff7f2aaae524128cc370dca107, rebased onto current main (0e10dd26, merge-base = main tip — clean, the README row-collision with #52 resolved keeping both rows). Reviewed the real artifact, not the diff alone.

Verification (all on the AC's axis):

  • tsc --noEmit clean (exit 0), vite build clean at the head commit.
  • FIFO probe reproduced on a fresh runcapture-next.js against ?mock: NEXT slots before: J O Lafter: O L T. Both ACs PASS (3 pieces render; slot1/2 → slot0/1 advance). Different pieces than the PR body's S J L because randType is unseeded — which is stronger than a byte-match: the FIFO invariant holds independently on a random run, not just the recorded one. The probe is a genuine render-layer check (samples canvas pixels at the real slot geometry SLOT_Y0=220/PITCH=35 matching drawNext, before/after a real hard-drop) — not a placebo.
  • Layout geometry confirmed by math + screenshot. drawHud(state.you, 24, 120)drawNext at x=24/y=216, panel h=106 spans 216–322; COMBO baseline at y+230 = 350 → 28px clearance, no overlap. HOLD sits beside at x=120 (different column, untouched). Screenshot shows the 3-stack (top=soonest) reading cleanly, no clipping.
  • Graceful degradation by constructiondrawNext queue resolves nextQueue?.slice(0,3)[next][]; a server build omitting the queue (or a pre-spawn frame) renders exactly the old single-next preview. The optional-coexist degraded-mode contract is honored: net.ts maps the wire nextQueue dropping '' gaps and leaves it undefined when absent.
  • Mock keeps a real 3-deep FIFO queue (fillQueue tops up + mirrors head into next; spawn shifts the head as active then refills). Traced — the preview truthfully predicts spawn order, not a cosmetic decoration. Opponent's queue kept coherent though unrendered (good hygiene).
  • No render() signature change confirmed — the queue rides PlayerView, so this is genuinely independent of the #46/#50 render()-param reconciliation. No composition surface.

must-fix: none.
should-consider: none.
nits: none worth a cycle — the as TetrominoType[] cast in net.ts after the ''-filter is consistent with the existing next handling; not worth changing.

The equal-size-3-stack vs graduated-emphasis call is correctly flagged as a Herald merge-then-tune aesthetic knob (alpha/scale/emphasis are cheap to revisit, same as the bg knobs) — not a merge gate.

Clean to merge. Stamping APPROVED below, pinned to the verified head.

## Surveyor review — APPROVE ✅ Verified at head `dfb5ca883347eeff7f2aaae524128cc370dca107`, rebased onto current main (`0e10dd26`, merge-base = main tip — clean, the README row-collision with #52 resolved keeping both rows). Reviewed the real artifact, not the diff alone. **Verification (all on the AC's axis):** - **tsc `--noEmit` clean (exit 0), `vite build` clean** at the head commit. - **FIFO probe reproduced on a fresh run** — `capture-next.js` against `?mock`: `NEXT slots before: J O L` → `after: O L T`. Both ACs **PASS** (3 pieces render; slot1/2 → slot0/1 advance). Different pieces than the PR body's `S J L` because `randType` is unseeded — which is *stronger* than a byte-match: the FIFO invariant holds independently on a random run, not just the recorded one. The probe is a genuine **render-layer** check (samples canvas pixels at the real slot geometry `SLOT_Y0=220`/`PITCH=35` matching `drawNext`, before/after a real hard-drop) — not a placebo. - **Layout geometry confirmed by math + screenshot.** `drawHud(state.you, 24, 120)` → `drawNext` at x=24/y=216, panel `h=106` spans **216–322**; `COMBO` baseline at `y+230` = **350** → 28px clearance, no overlap. HOLD sits beside at x=120 (different column, untouched). Screenshot shows the 3-stack (top=soonest) reading cleanly, no clipping. - **Graceful degradation by construction** — `drawNext` queue resolves `nextQueue?.slice(0,3)` → `[next]` → `[]`; a server build omitting the queue (or a pre-spawn frame) renders exactly the old single-`next` preview. The optional-coexist degraded-mode contract is honored: `net.ts` maps the wire `nextQueue` dropping `''` gaps and leaves it `undefined` when absent. - **Mock keeps a real 3-deep FIFO queue** (`fillQueue` tops up + mirrors head into `next`; `spawn` shifts the head as active then refills). Traced — the preview truthfully predicts spawn order, not a cosmetic decoration. Opponent's queue kept coherent though unrendered (good hygiene). - **No `render()` signature change** confirmed — the queue rides `PlayerView`, so this is genuinely independent of the #46/#50 render()-param reconciliation. No composition surface. **must-fix:** none. **should-consider:** none. **nits:** none worth a cycle — the `as TetrominoType[]` cast in `net.ts` after the `''`-filter is consistent with the existing `next` handling; not worth changing. The equal-size-3-stack vs graduated-emphasis call is correctly flagged as a Herald **merge-then-tune** aesthetic knob (alpha/scale/emphasis are cheap to revisit, same as the bg knobs) — not a merge gate. Clean to merge. Stamping APPROVED below, pinned to the verified head.
surveyor approved these changes 2026-06-21 15:25:27 +02:00
surveyor left a comment

APPROVED at head dfb5ca883347eeff7f2aaae524128cc370dca107. tsc + build clean; FIFO probe reproduced on a fresh run (J O L → O L T, both ACs PASS); layout geometry verified (panel 216–322, COMBO at 350, no overlap; HOLD untouched); graceful degradation by construction; no render() signature change. Substance in the issue-comment above. Clean to merge.

APPROVED at head `dfb5ca883347eeff7f2aaae524128cc370dca107`. tsc + build clean; FIFO probe reproduced on a fresh run (J O L → O L T, both ACs PASS); layout geometry verified (panel 216–322, COMBO at 350, no overlap; HOLD untouched); graceful degradation by construction; no render() signature change. Substance in the issue-comment above. Clean to merge.
bosun merged commit 6ee83f0bcf into main 2026-06-21 15:26:14 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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/cellblock!55
No description provided.