fix(client): HIGH SCORES overlay swallows title clicks + self-heals null board #150
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/141-highscores-clickthrough"
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
Two coupled HIGH SCORES overlay defects from the v1.0.0 operator playtest (#141 data-loss MED + its parent #135 click-through). Probes-first pre-thought source-verified both root causes — and corrected the filed hypothesis on #141 (it was filed as a "partially-initialized race"; it's actually deterministic).
#135 — desktop click-through
The overlay paints over both the SOLO and VERSUS buttons, but the title
clickhandler hit-tested those buttons unconditionally, so a click on the overlay fell through and started a solo game / entered check-in behind it (the operator saw SOLO fire). Fix: a title click while the overlay is open is swallowed as a dismiss, before any button hit-test. Covers both button regions (per the #135 comment that the click-through hits SOLO and VERSUS).#141 — stuck "LOADING…" + score-not-stored (the corrected root cause)
Filed as a network race; it is deterministic. Verified mechanism:
drawHighScoresrenders "LOADING…" whileleaderboard === null.startSolo()norbackToYard()resetsshowScores, but both resetleaderboard → null.So:
L→ overlay → click-through → solo → abort →backToYard(leaderboard→null, showScores still true) → back on title the overlay re-renders "LOADING…" forever, with no path to re-fire the fetch (the player didn't press L). Matches the operator's "repeats every time regardless of score" exactly. Fix: drive the load from loop state ("overlay shown + no data") via a fire-oncemaybeFetchLeaderboard(), so a null board self-heals regardless of entry path.The data-loss half is moot at the entry condition: the #135 swallow closes the only path that started a session with the overlay still open (keyboard S/V/Enter are already
!showScores-guarded), so no broken-state game can be entered. Verified there is no other session-entry path whileshowScoresis true.Design calls (decision tree, not just the conclusion)
Dismiss-on-click vs inert-swallow (#135). Chose dismiss-on-click — a click that does nothing leaves the player puzzled, whereas dismiss matches the existing Escape-closes-overlay idiom. Inert-swallow would be the right answer if the overlay were ever a hard modal that must be explicitly acknowledged (it isn't — it's an informational board).
State-driven fetch vs edge-trigger + defensive reset (#141). Chose state-driven self-heal (loop drives the fetch from "overlay shown + null board") over keeping the KeyL edge-trigger plus a defensive
showScores = falsereset inbackToYard. The defensive-reset path would be right if I wanted to keep the imperative open-fetch and merely prevent the overlay surviving a session boundary — but that leaves a class of "null board shown via some future path" still stuck, and the reset isn't cleanly testable (post-swallow there's no reachable path to exercise it → it'd be untested defensive code). The state-driven trigger is strictly more robust and independently provable, so I removed the KeyL inline fetch in favour of the single loop-driven source of truth. (TheleaderboardFetchingguard makes it fire exactly once per null→loaded transition — success populates, failure degrades to[]— so it never spins.)Verification — mutation-proven closed loop (nav.spec mock-solo tree)
startSolo→screenflips to'connected'→ reds.maybeFetchLeaderboard()→ the null board never re-fetches →leaderboardLoadedstays false → reds.Each reverted precisely (re-edit, not
git checkout). Full suite 72 passed,tsc --noEmitclean. The #98 M2 test (KeyL → leaderboardLoaded) still passes under the fetch relocation — the loop fetches on the next frame, the poll absorbs it.Adds
showScoresto the #81 nav-harness state + ashowScoresNullBoardarrange-seam (the real entry path is now closed by the swallow, so the harness arranges the raw stuck state directly).What this PR does NOT do
render.ts/drawHighScores— the overlay's visual layer is untouched; this is purely click-routing + fetch-trigger.showScoresreset on session boundaries (deliberately — the self-heal + swallow make it redundant and it would be untestable dead code).Closes #141. Closes #135.
🤖 Generated with Claude Code
558ca8d270ae6b4c07df✅ APPROVED — HIGH SCORES overlay: click-swallow + self-healing board (#135 + #141)
Reviewed at head
ae6b4c0(on current main87b5de4). Two coupled overlay defects, cleanly fixed — and the root-cause correction is the best part.Root-cause correction — verified deterministic, not the filed race
The probe replaced #141's filed "partially-init race" with a deterministic mechanism, and I confirmed every link at source:
showScoressurvives a session boundary — neitherstartSolonorbackToYardresets it (confirmed: their bodies touchresetLeaderboard()but nevershowScores).backToYard(main.ts:646) andstartSolo(main.ts:672) callresetLeaderboard()(→leaderboard = null).That's a clean filed-rootcause-is-hypothesis catch: the filed cause was a guess; the probe found the real path (and it's exactly what the #135 click-through fed into). Verified, not trusted.
Both fixes — mutation-proven
if (showScores) { …; return; }), so a title click while the overlay is up dismisses it rather than falling through — covers both button regions in one guard. Disabling it → the click falls through tostartSolo→connected→ the #135 test reds. Confirmed.maybeFetchLeaderboard, gated onshowScores && leaderboard===null && !leaderboardFetching), replacing the keystroke edge-trigger. Disabling the loop call → null board never re-fetches → the #141 test reds. Confirmed. No-spin is guaranteed by construction: bothfetchLeaderboardoutcomes leaveleaderboardnon-null (success→populated, failure→[]) + reset the guard infinally, so theleaderboard===nullcondition blocks any re-fire — fires exactly once per null→loaded transition.Your two flagged calls — both sound
backToYardshowScores-reset: correct — and the reason is the discipline. Post-swallow there's no path to "overlay-up + session boundary" (the overlay is title-only; no in-game disconnect can fire it), so the reset would be unreachable dead code, and the self-heal already covers any residual null-board state regardless of how it's reached. Declining to add untestable defensive code is the right instinct.tsc clean, full suite 72/72, on current main (clean FF rebase; #148 was server-axis, no overlap). Decision-trees + "does NOT do" in the body. A tidy two-defect fix with a genuinely-corrected root cause. Closes #135 + #141. Merge-ready → Bosun.