Game replay + settlement confirmation#18
Conversation
Backend:
- GET /games/{id}: full game detail (wallets, stake, result, time control,
settlement_status, result_hash) + the move list. Public + read-rate-limited;
axum routes the static /games/live ahead of this dynamic /games/{id}.
- persistence: game_detail(id) + game_moves(id) queries.
Replay (finished games):
- /game/[id] fetches the game once and branches: a finished game renders a
navigable replay (GameReplay), an in-progress one the live spectator
(LiveSpectator, extracted from the old page). Replay reconstructs the
position/last-move/check at every ply via chessops and offers a clickable move
list, ←/→/Home/End keyboard nav, transport buttons, and per-move clocks —
reusing the same Chessboard/PlayerBar/board helpers as the live views.
- Reachable from the profile game-history "Review" link and the live
spectator's game-over "Review game" action.
Settlement confirmation:
- After a wagered game ends, SeatGame polls GET /games/{id} until settlement is
settled/failed and resolves the banner to "Settled on-chain ✓ — you won X
USDC" / draw / loss, or a "delayed — recover on-chain" state with an escrow
link. The replay of a settled wagered game shows the same outcome.
- Factored a shared contractUrl(chainId, address) explorer helper (used by the
settlement link and SiteFooter, replacing its inline map).
Verified: tsc + production next build clean; server tests green; the endpoint
checked against real casual + settled-wager games; replay navigation (click,
keyboard, transport), per-move clocks, check/last-move/material, and the
settled-wager banner all confirmed live in-browser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Review of the replay+settlement PR found no correctness bugs; these address the
low-severity items worth acting on:
- Money clarity: the wager UI quoted the gross pot credit (2*stake - rake =
"1.99 USDC") as what you "win"/"net", but that includes your own returned
stake — the real profit is stake - rake ("+0.99"). Replaced payoutForStake
with a profitForStake helper and reworded the stake modal, the pre-game
callout, and the settled banner to the honest profit figure (win +0.99 /
draw 0 / lose -stake). Removed the now-unused payoutForStake.
- Replay keyboard nav no longer hijacks Arrow/Home/End while a text input is
focused.
- GET /games/{id} doc-comment corrected: it returns any game's (already-public)
detail, not only finished games.
Left as-is (cosmetic, documented): material() shows a phantom captured pawn
after a promotion — a correct fix needs move history.
Verified: tsc + production next build clean; the stake modal shows
"Win +0.99 USDC" for a 1 USDC stake in the browser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewed (multi-angle) — no correctness bugs, 3 low-severity fixes appliedRan a rigorous correctness review over the new endpoint, replay indexing, settlement-poll lifecycle, routing, DB types, and fetch/unmount races. All verified correct — no high/medium bugs. Also spot-checked live: Applied the low-severity items worth acting on (commit
Left as-is (cosmetic, documented):
|
Multi-angle review of the replay+settlement PR found no high/med correctness
bugs in the happy paths; these address the real items it surfaced:
- Aborted wagered games (escrow_open_failed / seat_dispatch_failed) route to
replay but abort_game never clears settlement_status, so GameReplay showed a
permanent, false "Settling on-chain…" plus the raw internal reason code
("Game over · escrow_open_failed"). Now an aborted game renders "Game didn't
start — your stake was refunded" with no settlement line and no internal code.
- Payout copy consistency: the SiteFooter "Fees & payouts" panel and the home
"How wagering works" step 3 still used the gross "both stakes minus the fee"
framing this PR replaced everywhere else (the ~2x overstatement escrow.ts now
warns against). Reworded both to the profit framing ("take your opponent's
stake, less the fee; your own stake comes back").
- LiveSpectator: a game already finished at fetch time (DB race / reaped room)
dead-ended on "not live" with no way forward — added a "Load replay" button.
- Defensive: fetchGame validates the response shape (status string + moves
array) before use; GameReplay pushes a null last-move highlight for any move
it couldn't apply, so a (hypothetical) bad UCI can't point at an unplayed move.
Flagged as follow-ups (not in this PR): extract the LiveSpectator/SeatGame
spectator reducer into a shared hook (pre-existing duplication, risky to refactor
here); add an oracle-verified badge to replay (needs persisting server_sig).
Verified: tsc + production next build clean; in-browser, an aborted staked game
shows "Game didn't start — refunded" (no settling banner / raw code), a settled
240-move game still replays with "Settled on-chain ✓", and footer + step 3 read
the profit framing (no gross copy remains).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewed (multi-angle: correctness + complexity/consistency)Traced the endpoint, replay indexing, live-vs-replay routing, settlement poll, DB types, and every fee/payout string. No high/med correctness bug in the happy paths — verified: Correctness
Consistency (the one I'd insist on)
Flagged, not fixed (follow-ups):
|
Both review follow-ups:
1. Dedupe the spectator-frame reducer. LiveSpectator and SeatGame each carried
their own copy of the board state + the game_start/opponent_moved/clock_sync/
game_over switch (the legality-guarded move-application logic — a real
board-corruption risk if the copies drift). Extracted it into
lib/useSpectatorBoard: one hook owns fen/moves/lastUci/inCheck/clock/result/
verified + the reducer; each view keeps its own socket, status, and terminal
flag (their lifecycles genuinely differ — SeatGame also drives an engine
seat), feeding frames in via applyFrame and getting game_over back through a
callback.
2. Oracle-verified badge on the replay. The permanent finished-game view was the
only place lacking the "✓ signed by oracle" trust signal the live/seat views
show. The oracle signature over result_hash is now signed once and persisted
(migration 0010 + finish_and_enqueue), exposed via GET /games/{id}, and
GameReplay verifies it (recover signer == oracle) to show the same badge.
Verified: full Rust suite + production next build green; live-spectated a game
through the new hook (moves/clocks/turn/last-move all update), and its replay
shows "✓ Verified — signed by oracle 0x7099…79c8" with result_sig persisted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both review follow-ups landed (commit
|
Two product gaps closed on top of the recent board/UX work: reviewing finished games, and confirming a wager actually settled.
Backend
GET /games/{id}— full game detail (wallets, stake, result, time control,settlement_status,result_hash) + the move list. Public and read-rate-limited; axum routes the static/games/liveahead of this dynamic/games/{id}.game_detail(id)+game_moves(id)queries.Replay (finished games)
/game/[id]fetches the game once and branches: a finished game renders a navigable replay (GameReplay), an in-progress one the live spectator (LiveSpectator, extracted from the old page).Chessboard/PlayerBar/boardhelpers as the live views.Settlement confirmation
SeatGamepollsGET /games/{id}until settlement issettled/failedand resolves the banner to "Settled on-chain ✓ — you won X USDC" / draw / loss, or a "delayed — recover on-chain" state with an escrow link (per the chosen scope: status + guidance + link, no in-app recover button).contractUrl(chainId, address)explorer helper (used by the settlement link andSiteFooter, replacing its inline map).Verification
tsc+ productionnext buildclean, server tests green. Verified live in-browser against real casual and settled-wager games:🤖 Generated with Claude Code