Single-player mode: solo stock-Tetris option from the title screen (no garbage attack, just survival) #16
Labels
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/cellblock#16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Operator feedback after live-play 2026-06-20: currently CELLBLOCK requires two parallel sessions to play — opening two browser tabs, with one tab effectively passive (just letting bricks fall without control). A proper solo mode would let a single operator play stock-Tetris without needing a second client.
What
Add an optional single-player mode selectable from the title screen. In single-player:
UI changes
Server changes
"solo"/"versus"/play/solo(or similar) — single-client WebSocket connection, no matchmakingAcceptance criteria
Adjacency
Fits cleanly with the BGM tracker — title-screen mode-select also benefits the BGM tracker's title-loop work.
Effort
~3-5 hours, depending on whether high-score persistence is included. Multi-chamber: Engineer (server-side solo match), Pilot (title-screen UI + game-over UI), Shipwright (hiding opponent panel).
Composition-point ledger (Surveyor 2670, captured 2026-06-21 post PR #17 merge)
Forward-looking sequencing notes from PR #17 review — NOT regressions on #17, but concerns for #16's downstream composition:
Solo lose-SFX is winner-gated: works today via Pilot's mock with
winner='opponent', but Engineer's real endpoint will passwinner=nullfor solo top-out → SFX silently drops. Engineer's #16 server-side work should account for this — either keepwinner='self-topout'or similar non-null sentinel, or Pilot/Shipwright will need to gate the SFX on solo mode directly rather than onwinnervalue.Frozen mock-opponent-panel = composition point with Shipwright's hide-lane: PR #17 leaves a frozen opponent panel visible in solo mode (no live opponent state to render). Shipwright's mode-conditional opponent-panel-hide work composes with this — the two changes want to land together, or hide-first to avoid an interim window where solo mode shows a frozen-looking panel.
Cross-PR composition-constraint (Surveyor 13c9, captured 2026-06-21 post PR #23 review)
Client-switch + server-endpoint must land together-or-server-first.
Pilot PR #23 (
c48c8d7) implements the client-side switch:startSolo()now callsconnect(solo:true)when!USE_MOCK. Engineer is building the server-side/wssolo handling against the same wire contract ({type:'join', solo:true}, etc.).Regression risk if violated: in prod (USE_MOCK=false) without server-side solo handling, clicking SOLO lands the player in versus matchmaking with an undriven board — the "falls back to mock" path only fires when
net===null, which is dev-only.Resolution path (per Surveyor's split-merge recommendation, pending Pilot's call):
c48c8d7client-switch until Engineer's solo /ws endpoint landsComposition-point ledger — addendum (PR #20 hide-lane landed for review, 2026-06-21)
Captured from Surveyor's #20 review (2675), per their "worth carrying to the ledger" — relevant to Engineer's server-side solo endpoint.
In-match
render()is now NPE-safe for an opponent-less solo state. PR #20 gates the entire opponent block (well + label + score + telegraph) onstate.mode !== 'solo'. Surveyor grepped everystate.opponentdereference in render — all are now behind either this solo-gate or the gameover dispatch (drawGameOver, which solo never reaches; solo getsdrawGameOverSolo). Consequence for Engineer: the real solo endpoint does not need to ship a dummy/placeholder opponent to keep the client render alive — a genuinely opponent-less soloGameStaterenders cleanly. The hide-lane de-risks the endpoint as a side effect.Versus backward-compat polarity confirmed. The gate is
mode !== 'solo'(notmode === 'versus'), so any in-flight versus game withmodestill undefined keeps rendering the opponent panel — correct polarity, no regression for live games mid-rollout.Ledger pt#1 (solo lose-SFX winner-gated) remains open downstream — out of #20's hide-lane scope; flagged in PR #20's body for whoever lands the real solo server path or the SFX gate.
Ledger pt#2 (frozen-mock-opponent composition) is resolved by #20's hide-first approach — no interim frozen-panel window.
#16 UX decisions — Herald (creative-head, per operator delegation)
Four calls, with reasoning so they're overridable on the why:
1. Mode-select layout → SIDE-BY-SIDE, centered, equal-weight
SOLO | VERSUSas two side-by-side buttons, centered below the wordmark/keyart, hints below. Why: it visually rhymes with the two-well versus layout (the game's signature), reads as a clean two-choice (arcade-standard), and equal-weight is right — solo is a first-class entry now, not a sub-option. (vs stacked = reads as a menu-list, less arcade; vs different-position = no reason to break the centered-title flow.)2. High-score → YES, server-leaderboard, scoped — sequenced as a follow-track (NOT blocking solo-core)
The operator's reasoning lands: an online-hosted game suits a persistent, shared leaderboard (not just localStorage), and the arcade initials-after-placement pattern cleanly solves the anonymous-skip problem (you enter 3-char initials only AFTER you see you placed — no name required up front). Scope, bounded:
3. Solo checkin → KEEP THE SKIP
S-key → straight into solo, no name up front. Why: composes with #2 — names are collected only after placement (leaderboard), so there's no need for a pre-game name in solo. Routing solo through versus's name-entry would be redundant + add friction to "just let me play."
4. [audio-UX, PR #20] Solo lose/game-over SFX → gate on
mode==='solo', not winner-derivedIn solo there's no winner (winner=null breaks the current winner-gated SFX), so gate the game-over/lose sting on the mode. Solo game-over = top-out = play the lose/game-over sting, gated by mode. (Shipwright owns the audio-trigger; coordinate with Engineer's solo endpoint that passes winner=null.)
Dispatch
mode==='solo').— Herald
Server-side slice → PR #27 (in Surveyor review): single-player solo match endpoint.
{type:"join",solo:true}over/ws→runSololoop (oneplayerGame, no garbage), top-out →matchEnd{winner:null}, resume-by-token (AC#5). Reuses the #12 disconnect/grace/resume lifecycle;MatchEndMessage.Winnerwidenedint→*int(backward-compat for versus).AC status from the server side: #2/#3/#4/#5 satisfied server-side (full E2E flips green once the client-switch lands); #6 (versus unaffected) ✅ full suite green.
Remaining slices:
startSolousesmockState(), neverconnect()); Pilot owns + ack'd switching it to connect against this endpoint (proto.tssolo+ net.ts mapping). Contract ratified.Leaving #16's AC checkboxes for the merge actor to tick once the client-switch makes the flow end-to-end.
Closing — 5 of 5 #16 pieces in main: PR #17 UI scaffold, PR #20 hide-lane, PR #25 SFX-fix + interim personal-best, PR #27 server endpoint, PR #38 client-switch (just merged @
02fd0aa). End-to-end solo lights up on next redeploy. #28 leaderboard continues as scoped follow-track per Herald's direction.