A settled move may be taken back as a draft while the round still waits on others

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-29 12:26:32 -04:00
co-authored by Claude Opus 5.5
parent af25b672f1
commit 59494aa29f
3 changed files with 10 additions and 4 deletions
+5 -2
View File
@@ -430,7 +430,8 @@ export class Rooms<State, Input> {
/**
* Record a seat's move on the ledger, so a restart keeps it; resolve the round once nobody else is awaited.
* A draft (an input the game does not call settled) may be replaced until it is settled.
* A draft (an input the game does not call settled) may be replaced, and a settled move taken back
* as a draft, until the round resolves.
* `resolved` is how many rounds the page had seen played when the move was made: a move that
* arrives after its round was played (a slow connection, a second click, a stale tab) is refused
* rather than taken as the move for the next round.
@@ -442,8 +443,10 @@ export class Rooms<State, Input> {
if (typeof resolved !== 'number') throw new RoomError('This page is out of date. Reload it to see the table as it stands.', 409);
if (resolved !== room.resolved) throw new RoomError('That move was made for a turn that has already been played. Your page will catch up.', 409);
if (!this.waitingOn(room).includes(seat.id)) throw new RoomError('It is not your move.', 409);
if (!this.game.needsInput(room.state, seat.id)) throw new RoomError('It is not your move.', 409);
const input = this.game.cleanInput(raw);
// A settled move is final; a draft may replace it while the round still waits on others.
if (seat.id in room.pending && this.settled(room.pending[seat.id]) && this.settled(input)) throw new RoomError('Your move is already in.', 409);
const why = this.game.validate?.(room.state, seat.id, input);
if (why) throw new RoomError(why);
this.commit(room, { t: 'input', at: Date.now(), id: seat.id, input });