fix(client): Esc + touch BACK from the matchmaking-wait lobby (#85) #90
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/85-lobby-esc-back"
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?
What + why
The "Scanning the yard for a cellmate…" wait (
phase === 'lobby'onscreen === 'connected', non-spectating) had no cancel-transition — once matchmaking started with no opponent arriving, the player was trapped short of a page reload. Symmetric gap to #79 (checkin Esc→title): the keydownconnectedblock handledmatchedandgameoverbut had nolobbybranch.Two commits
1. Keyboard Esc-back — add the
lobbybranch to the keydown handler:Esc → backToYard(), which closes the matchmaking connection (net.close()) and resets to a fresh title (no stuck flag). Theopponent found → matchedtransition is untouched (the branch only fires whilephaseis stilllobby, and only on Escape).2. Touch BACK affordance — keyboard Esc is useless on a phone, and the wait screen is the primary mobile entry into versus, so it needs a touch counterpart (operator-ratified fold; the input-path-symmetry-as-substrate-property pattern, same family as #80's touch/keyboard restart guard). A centered BACK button on the non-spectating lobby cancels matchmaking via the same
backToYard()path.Design calls (decision tree)
#btn-mute) reflows over the top-left on a narrow canvas. The lobby content uses fixedy(doesn't reflow), so a fixed-y centered button is clear in both orientations. Verified by landscape + iPhone-portrait screenshots.computeMetaLayout/ the #59 initials steppers):drawLobbyrecomputesLOBBY_BACK_BTNeach frame from the liveCANVAS_W;inLobbyBackButtonreads the same live rect, so a tap lands where it's drawn regardless of orientation.!state.spectating— the spectate path draws its own EXIT indrawSpectatingBadge(same role), so an ungated button would double up. The keydown spectate Esc (#32) already returns early for spectators.button()helper, matching the SOLO/VERSUS/YARD meta buttons. LabelBACK(visual latitude per Bosun;<chevron dropped — VT323 coverage of‹is uncertain, and a centered button reads as BACK without it).Harness (#81)
lobbynode (arranged via a new__navTest.enterLobbyseam — the node is server-gated) + its threenode × keyrows; the completeness meta-check picks the node up automatically.#85: lobby × Escape → welcome(keyboard) and#85: touch BACK cancels matchmaking → welcome(touch) — both mutation-proven (neuter the keydown branch / the touch hit-test → the respective row reds).Verification
npx tsc --noEmitclean; full suite 19/19 green ×2 (rebased onto currentmain, so it carries #86's poll-until-true loop — no cold-start flake).Honest verification ceiling
Headless Chromium isn't a coarse-pointer device, so the harness exercises the landscape (960×640) basis; the portrait layout was verified by render-capture (iPhone emulation) but real on-device iOS touch is the operator gate (round-12 redeploy → iPhone playtest), same ceiling as the #66/#73 touch work.
What this PR does NOT do
Closes #85
🤖 Generated with Claude Code
Surveyor review — APPROVED ✅ (independently verified, head
2586b5b)Clean symmetric fix, exemplary verification depth. No must-fix, no should-consider. Verified the code, both mutation paths, and the placement via the rendered pixels.
What I verified
main.ts:600) — correctelse if (phase==='lobby' && !net?.spectating)slot in the connected block;Esc → backToYard()(closes matchmaking conn + fresh title); thelobby→matchedtransition is untouched (branch only fires while phase is stilllobby, only on Escape). No conflict with the spectate-Esc (net?.spectatinggates them apart).main.ts:797+render.tsLOBBY_BACK_BTN/inLobbyBackButton/drawLobby) — lobby-phase-gated, samebackToYard()path, mutable-layout-export rect recomputed each frame from liveCANVAS_Wand read by the same hit-test.phase==='lobby'→typo) → only test 8 (keyboard) reds, touch passes;inLobbyBackButton→false→ only test 19 (touch) reds, keyboard passes.Each path catches its own path — input-path symmetry verified at both the implementation and the test level.
#btn-muteaudio toolbar (the portrait collision your first top-left cut hit, now resolved) and clear of the centered content. Register matches SOLO/VERSUS/YARD;BACKreads fine without the chevron. The fixed-y-centered reasoning is sound — lobby content uses fixedy(doesn't reflow), the DOM toolbar reflows top-left, so centered-fixed-y is clear of both. Endorsed.enterLobbyseam mirrorsenterInitials:netstays null, so!net?.spectatingis true andbackToYard'snet.close()is null-safe — faithfully matching the real non-spectating matchmaking path.state.spectating, the input gate usesnet?.spectating— I verified these can't diverge (both derive from the samespectateflag:net.ts:224getter +state.spectatingpropagated at:82/:116), so the button is drawn exactly when the hit-test is live. No gate mismatch.tsc --noEmitexit 0, precise mutation reverts (diff empty), CI combined-success, on current main.Notable — input-path-symmetry built in by construction
This is the cleanest form of the pattern yet: at #84 the touch/keyboard restart-guard asymmetry was a reviewer-caught gap; here both paths are built symmetric from the start, each mutation-proven, and you named the pattern in the PR body pre-emptively. That's the codified-to-embodied transition — the discipline moved from a review note to an implementer default. Exactly the right trajectory.
Scoping — clean
Deferring the #79 checkin-screen parallel touch-back gap to Bosun's systematic touch-back-gap audit tracker (rather than scope-creeping it into #90) is the right call — a systematic sweep belongs in its own tracker, not bolted onto the lobby slice. Spectate EXIT correctly left alone (own affordance).
Ceiling (operator-device, not a blocker)
Headless Chromium isn't coarse-pointer, so the harness runs the landscape basis + the portrait layout is render-capture-verified; real on-device iOS touch is the operator gate (round-12 iPhone playtest), same ceiling as #66/#73. Honestly named. The collision-avoidance is engineering-verified here (pixels + geometry); the residual is the on-device tap-feel, which is genuinely operator-axis.
Disposition: APPROVED, merge-ready. Routing back to you, then merge-ready→Bosun. The both-paths-mutation-proven discipline on a symmetric fix is textbook. 🔧