Settle no-shows as forfeits; auto-stop dead-engine gauntlets#15
Merged
Conversation
Two protections for wagered games where a player's engine never actually plays. They share the room.rs / GameOutcome plumbing, so they land together. No-show forfeit: a wagered game that spawns but never starts (one side never readies within the 60s reap window) now settles on-chain at the reap instead of leaving both stakes locked behind the contract's 24h claimTimeout. The side that showed up and readied wins the stake via settleGame; if neither readied, both are refunded (draw). Adds a Forfeit game-end reason and lets finish() settle a never-started game (empty PGN, ply 0, so it is never rated). As a side effect a dispatched-but-dead tournament bot now forfeits its pairing instead of drawing it, matching the existing dispatch-time forfeit. Gauntlet no-move guard: if a gauntlet seat loses a game without ever making a move (dead/offline/hung engine, including a no-show forfeit), its session auto-stops so a broken engine can't bleed the stake game after game. A real loss (moves were played) does not stop it. Enforced server-side -- queue_join rejects (409) any re-queue against a stopped session, so the guard holds even if the client keeps trying -- with a client panel that explains the stop and offers a clean restart. GameOutcome now carries the ply count so record_outcome can tell a contested loss from a no-move forfeit (White plays iff ply>=1, Black iff ply>=2). Both confirmed as intended: a pre-dispatch agent vanish still refunds as a draw, and forfeit wins are raked like any other win. Tests: +2 room unit tests (reap forfeit-winner selection) and +2 matchmaking tests (auto-stop on a no-move loss vs. keep running after a contested loss). Full Rust suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Addresses four issues found reviewing the prior commit. Reap no longer confiscates a present player's stake. reap_forfeit_winner now forfeits only to a side that is still connected AND readied while its opponent is genuinely absent (not connected); every ambiguous case -- nobody ready, both present but one slow to ready, or a side that readied then dropped its connection -- refunds both. Previously it decided real money from the monotonic ready flags alone, so a disconnected player could win a present opponent's stake, and a present-but-slow player lost theirs. Gauntlet stop backstop is now enforced at match time, not just at queue entry. queue_join re-checks BOTH sides' session status right before opening a wagered game, so a stopped session (owner-stopped or auto-stopped after a no-move forfeit) can no longer be dragged into a new game via a ticket that was already waiting in the queue -- and the entry-gate/auto-stop race can't slip an extra staked game through either. Web: on a 409 the gauntlet loop now reflects the stop locally instead of depending on a stats refetch that could fail and strand the UI on a spinner. Reap unit tests rewritten for the presence-aware signature (present+ready vs. absent forfeits; ambiguous cases refund). Full Rust suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the second review pass over the reap fix. Finding 1 (a regression from the reap fix): a gauntlet engine that connects but never readies (hung at init) reaped as a draw, and the loss-only auto-stop never fired -- so the session looped forever on refunded draws, bleeding oracle gas and never progressing. GameOutcome now carries each seat's "showed up" (connected AND readied) bit; record_outcome stops a session on a never-started draw only when ITS OWN seat no-showed, sparing an opponent that did show up (both made 0 moves, so plies alone can't tell them apart). Zero-move losses still auto-stop as before. Finding 4: added a test that parks a waiting ticket, stops its session, then pairs an opponent and asserts no game is created -- guarding the pair-time backstop against a future regression that drops the check. Finding 2 left as-is by decision (an engine that plays a move then hangs is out of the "never makes a move" spec) with the misleading "hung engine" comment corrected to state the zero-move-only scope. Full Rust suite green (36 server tests, +2 new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues from the third review pass. Idempotent finish_and_enqueue (money + data integrity). A wagered game aborted at seat dispatch can be resurrected by a late-connecting agent whose WebSocket keeps the room alive long enough to reap; the reap would then overwrite the 'aborted' row and enqueue a SECOND settlement -- a phantom win/loss in player_stats, or a stake confiscation if the abort's inline refund had failed (the abort refund has no retry; the reap's forfeit does). The UPDATE now finalizes only a non-terminal game (status NOT IN finished/aborted) and skips the settlement_outbox enqueue when no row changed, so a reap can never override an abort. showed_up now keys on latched readiness (wready/bready) instead of occupied && ready. A gauntlet seat that readied then briefly dropped its socket before the game started is no longer mistaken for a no-show and auto-stopped; only a seat that NEVER readied (dead/hung at init) stops. Full Rust suite green (36 server tests). The persistence idempotency guard is not covered by a local unit test (needs DATABASE_URL) -- the SQL runs under CI Postgres. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Two protections for wagered games where a player's engine never actually plays. Both came out of a review of whether
ChessEscrowcovers every game mode — the contract does; these close server-side gaps in how it's driven. They share theroom.rs/GameOutcomeplumbing, so they land together.1. No-show forfeit (settle at reap instead of waiting 24h)
Previously, a wagered game that spawned but never started (one side never readied within the 60s reap window) was reaped with
winner: Noneand did not settle the escrow — both stakes sat locked behind the contract's 24hclaimTimeout, and the player who showed up was never told they'd won.Now the reap settles on-chain immediately:
settleGame(winner = the side that showed up). The no-show loses its stake to the player who showed up.winner = None→ both refunded (draw).Adds
GameEndReason::Forfeit, and letsfinish()settle a never-started game (empty PGN,ply 0, so it's never rated). Side effect: a dispatched-but-dead tournament bot now forfeits its pairing instead of drawing it — consistent with the existing dispatch-time forfeit.2. Gauntlet no-move guard (stop a dead engine from bleeding the stake)
If a gauntlet seat loses a game without ever making a move (dead/offline/hung engine, including a no-show forfeit), its session auto-stops so it can't lose the stake game after game. A genuine loss (moves were played) does not stop it.
record_outcomeusing the ply count (White plays iffply ≥ 1, Black iffply ≥ 2— the same invariant behind the existingrated = ply ≥ 2rule).queue_joinrejects (409) any re-queue against a stopped session, so the guard holds even if a buggy client keeps trying.Confirmed design decisions
abort_started_gameunchanged) — at that point nobody has connected, so there's no confirmed "player who showed up".settleGamepath).Tests
+2room unit tests — reap forfeit-winner selection (readied side wins; neither → refund).+2matchmaking tests — auto-stop on a no-move loss (both colors) vs. keep running after a contested loss.node_modulesisn't installed in this worktree, and the auto-stop UI isn't reachable in an isolated preview (needs the live staked flow).Risk / rollout
settleGamealways has an open game to settle; settlement goes through the existing durable outbox →settleGamepath (idempotent — a double-settle revertsAlreadySettledand is treated as success).🤖 Generated with Claude Code