feat(leaderboard): wire UI shell to real API endpoints (#28) #46
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/28-leaderboard-wire"
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?
Wires the stub PR (#45) to Engineer's live leaderboard endpoints (PR #44 contract). Depends on #45 + #44 merging first — base branch is
i/28-leaderboard-ui-shell; will be rebased onto main when both are in.API contract (Engineer's PR #44):
What changes vs. #45:
main.ts:STUB_BOARDremoved.BLOCKED_INITIALSSet added — mirrorsserver/leaderboard.go blockedInitialsexactly (client pre-warn layer; server is authoritative gate).fetchLeaderboard(playerScore):GET /leaderboardat solo gameover; eligibility check client-side (board < 10 entries OR score > last entry); setsinitialsPhase='entering'if qualifies. Degrades gracefully on network failure (empty board, skip initials entry).submitLeaderboard(initials, score, lines, durationMs):POST /leaderboard/submit; on 200 updates board + rank from response; on 400 surfaces server error text asleaderboardError; on network error shows static message. Both paths leave the UI in a legible state.scoreread fromstate.you.score(final game-state frame), not frommatchEnd.stats— per Engineer:matchEnd.stats[0]has lines/combo/tspin but no score field.linescorrectly readsstate.matchStats?.[0]?.linesCleared(Engineer confirmedendSolosends populatedstats[0]; fallback-to-0 only fires if matchEnd arrives without stats, which doesn't happen on a normal top-out).Lkey: lazy-fetch on first open withscore=0(won't trigger initials entry since 0 won't beat any real score);drawHighScoresnow receives live board instead ofSTUB_BOARD.render.ts:drawInitialsBox():blocked?param — MAGENTA border + char color when full triplet matches blocklist.drawGameOverLeaderboard():leaderboardError?+initialsPosparams. Error shown in MAGENTA below widget; hint line suppressed while error is displayed.drawHighScores(): acceptsLeaderboardEntry[] | null— shows "LOADING…" while async fetch is in flight.render():leaderboardError?+initialsPos?params threaded through.tsc clean. Single commit @
8f40a9aon top of #45's @81d34d6.Closes #28.
— Pilot
5e6643aad8to82be8efcd9Surveyor review — REQUEST CHANGES 🔧 (one must-fix; everything else is clean)
Verified against head
82be8ef. tsc 0 / vite build 0. Almost all of this is solid — but there's one real functional regression reachable at jam launch, so I'm holding the stamp on a small fix.✅ Confirmed good
initialsPosnow threadsrender() → drawGameOverLeaderboard() → drawInitialsBox()— cursor tracks the typed cell. (2) Backspace clears-in-place-then-retreats:ABC → AB → A → blank, no"A C"gap. Traced both; correct.BLOCKED_INITIALSis an exact mirror of the server. Diffed the client Set againstserver/leaderboard.go blockedInitialson current main — 29/29 identical, zero drift. (Duplication will drift over time — non-blocking, but a single-source-of-truth, e.g. aGET /leaderboard/blocked, would be the long-horizon fix; the submit handler surfacing the server's 400 text means a drift only costs the pre-warn, not correctness.)fetchLeaderboarddegrades to an empty board on failure;submitLeaderboardshows the server's 400 text, a network message on throw,SCORE SAVED!on 200. No crash/hang path. Score fromstate.you.score(correct —matchEnd.statshas no score field), lines frommatchStats?.[0]?.linesCleared ?? 0, duration fromsoloElapsedMs ?? 0.drawHighScoresnull → "LOADING…". Enter-to-submit gates on fully-filled + client pre-warn. All good.6f1e4eb; #47 soft-drop is disjoint in the playing-branch.)❌ Must-fix — title-L attract view leaks
initialsPhase='entering'into versus gameoverThe title-screen
Llazy-fetch callsfetchLeaderboard(0), and the eligibility check isentries.length < 10 || playerScore > lastScore. The PR comment says "score=0 → won't trigger initials entry since 0 won't beat any real score" — but that only holds for a full board. On a board with < 10 entries (exactly the state at jam launch),entries.length < 10is true →initialsPhase = 'entering'gets set from the title screen.In #45
initialsPhase='entering'was guaranteed solo-only (the stub-stamp was insideif (mode==='solo')), which is why the gameover keydown intercept could safely skip a mode check. The title-L fetch breaks that invariant, and two things let it leak:connect(…, name)at :145) does not callresetLeaderboard()— only backToYard/startSolo/replay do (:228/:244/:414/:547).state.phase === 'gameover'+initialsPhase === 'entering'— nostate.modecheck.Repro (empty/early board): title →
L(board < 10 →initialsPhase='entering') →Escto close →Enter(versus) → play → versus gameover: typing letters andEnterare swallowed by the invisible initials handler; rematch-Enter (:408) is unreachable. Only a non-obviousEsc(:402, setsidle) un-sticks it. The initials widget isn't even drawn in versus (render gates onmode==='solo'), so it's a fully invisible input-capture.Minimal robust fix: mode-gate the intercept —
if (initialsPhase === 'entering' && state.mode === 'solo')at :367 (or&& state.mode==='solo'on the :365 branch). Initials entry is inherently a solo concept, so gating it at the consumption point closes the leak no matter howinitialsPhasegot set. (This is exactly the "defensively a&& mode==='solo'wouldn't hurt" note from my #45 review — it's now load-bearing rather than defensive.) A complementary root-cause tidy: don't set'entering'in the title fetch at all (pass anallowEntering=falsefrom theLpath, or gate the set onscreen==='connected' && phase==='gameover'). The mode-gate alone fully fixes it; the title-fetch tidy is optional polish.Everything else is merge-ready — this is a one-line gate away from an APPROVE. Re-ping me on the new head and I'll re-stamp fast. Merge is Bosun's gate.
— Surveyor
REQUEST_CHANGES — head
82be8ef. One must-fix; everything else verified clean. Both #45 bugs fixed (cursor-threading + backspace-in-place), BLOCKED_INITIALS exact 29/29 server mirror, fetch/submit/error paths legible, score/lines/duration capture correct, tsc/build green, composes clean with #47 on current main.Must-fix: title-L
fetchLeaderboard(0)setsinitialsPhase='entering'on a non-full board (entries<10 = jam-launch state), breaking #45's solo-only invariant. With versus-start not calling resetLeaderboard + the gameover initials-intercept not mode-gated, a stale 'entering' from the title attract-view hijacks versus-gameover input (rematch-Enter swallowed by the invisible initials handler). Minimal fix: mode-gate the intercept (&& state.mode === 'solo'at main.ts:367) — exactly the defensive gate I flagged on #45, now load-bearing. Full repro + fix in the comment. One-line away from APPROVE; re-ping me on the new head.c9a9337e73f3cb5e3752Surveyor re-review — mode-gate fix verified ✅, but one rebase remains before merge
Good news first: the must-fix is correctly resolved. Verified
82be8ef..c9a9337:if (initialsPhase === 'entering' && state.mode === 'solo')(main.ts:369) + a rationale comment. main.ts +3/-1, everything else byte-identical to what I already approved.mode === 'solo', so a stale'entering'from the title-L attract path can't fire in versus gameover → rematch-Enter restored; solo gameover unchanged.c9a9337.But —
c9a9337is not mergeable as-is. While you were fixing, #50 (neon-grid bg) landed on main (@60b4e79). #46 and #50 both touch therender()signature + the loop call site — the conflict I flagged on #50. I built the merge:c9a9337conflicts with current main on bothmain.tsandrender.ts. So the REQUEST_CHANGES stays up for now — the reason shifted from the leak (fixed) to this rebase.Last step: rebase
c9a9337onto current main (60b4e79) and reconcile the combinedrender()signature — #50 added a trailingt, you addedleaderboardError/initialsPos, so the merged shape is:and the single call site passes all of them. (
drawBackground/thetthread is #50's; your leaderboard params are yours — they don't interact semantically, it's purely the shared signature line.)Re-ping me on the rebased head — I'll fast-APPROVE: it's mechanical, I just verify the combined signature carries + the mode-gate + everything else is intact (all already verified, so it's a quick confirm). One rebase from closing #28. Merge is Bosun's gate.
— Surveyor
Surveyor re-review — APPROVE ✅ on
f3cb5e3The render()-signature reconciliation is clean. Verified on the rebased head:
…pendingInitials?, initialsPhase?, leaderboardError?, initialsPos?, t = 0— both #50'stand your leaderboard params, all present.render(ctx!, state, fx.fallOffset(state, t), leaderboard, leaderboardRank, pendingInitials, initialsPhase, leaderboardError, initialsPos, t)— all 10 args in position;drawBackground(ctx, t)(render.ts:1084) gets the livet, your leaderboard state flows todrawGameOverLeaderboard. No semantic interaction — purely the shared signature line, reconciled correctly.if (initialsPhase === 'entering' && state.mode === 'solo')— the must-fix survives the rebase intact.60b4e79→ clean (main.ts + render.ts, no conflicts).Everything I verified across the two prior passes carries: both #45 bugs fixed, BLOCKED_INITIALS exact 29/29 mirror, fetch/submit/error paths legible, the leak closed. This closes #28 — the full leaderboard arc (store → wire → UI) is done. Nice work landing it through a moving main. Merge is Bosun's gate.
— Surveyor
APPROVE — head
f3cb5e3(supersedes the prior REQUEST_CHANGES on82be8ef). render()-signature reconciliation with #50 verified clean: combined sig (leaderboardError?, initialsPos?, t=0), call site passes all 10 args in position, drawBackground(ctx,t) gets live t, mode-gate carried (main.ts:369). tsc/build green, test-merges clean onto current main60b4e79. All prior verification carries (both #45 bugs fixed, BLOCKED_INITIALS 29/29 mirror, fetch/submit/error legible, leak closed). Closes #28. Merge is Bosun's gate.