A move names the round it was made for; a stale one is refused

The room counts rounds played and sends the count in its view; a page
sends it back with each move, and the server refuses a move whose count
is behind (a slow connection, a second click, a stale tab), or that has
no count at all (an old page, told to reload). Before, such a move was
taken as the move for the next round. Found in Dip's acceptance run.

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-25 22:19:45 -04:00
co-authored by Claude Opus 5.5
parent f0d06db108
commit 41349036d1
5 changed files with 23 additions and 6 deletions
+1 -1
View File
@@ -154,7 +154,7 @@ export const api = {
view: (roomId: string, token: string) => call<RoomView>('GET', `/api/rooms/${roomId}?token=${encodeURIComponent(token)}`),
/** The gallery's view: no token, no seat, nothing any wizard could not see. */
watch: (roomId: string) => call<RoomView>('GET', `/api/rooms/${roomId}`),
turn: (roomId: string, token: string, input: TurnInput) => call<RoomView>('POST', `/api/rooms/${roomId}/turn`, { token, input }),
turn: (roomId: string, token: string, input: TurnInput, resolved: number) => call<RoomView>('POST', `/api/rooms/${roomId}/turn`, { token, input, resolved }),
say: (roomId: string, token: string, text: string) => call<RoomView>('POST', `/api/rooms/${roomId}/say`, { token, text }),
/** A word from the gallery, signed with a name, when the host allows it. */
sayFromGallery: (roomId: string, name: string, text: string) => call<RoomView>('POST', `/api/rooms/${roomId}/say`, { name, text }),
+5 -1
View File
@@ -229,6 +229,8 @@ export interface Remote extends HeldSeat {
expected: string[];
rematch: RoomView['rematch'];
rematchOf: string | null;
/** Turns played so far, as the server last said. */
resolved: number;
keeper: string;
keeperZone: string;
}
@@ -780,7 +782,7 @@ export class Duel {
this.sending = true;
r.error = '';
try {
this.apply(await api.turn(r.roomId, r.token, yours));
this.apply(await api.turn(r.roomId, r.token, yours, r.resolved));
} catch (e) {
r.error = e instanceof Error ? e.message : 'The move did not reach the server.';
} finally {
@@ -864,6 +866,7 @@ export class Duel {
r.expected = view.expected;
r.rematch = view.rematch;
r.rematchOf = view.rematchOf;
r.resolved = view.resolved;
markTalkSeen(r.roomId, view.chat.length);
const wasStarted = r.started;
r.started = view.state !== null;
@@ -1021,6 +1024,7 @@ function remoteFrom(view: RoomView, token: string): Remote {
expected: view.expected,
rematch: view.rematch,
rematchOf: view.rematchOf,
resolved: view.resolved,
keeper: view.keeper,
keeperZone: view.keeperZone
};
+2
View File
@@ -36,6 +36,8 @@ export interface RoomView {
seats: { id: WizardId; name: string; bot: boolean }[];
/** Seats that still have to move before the turn resolves. */
waitingOn: WizardId[];
/** Turns played so far, time stops included; gestures are sent with the count they were made against. */
resolved: number;
state: GameState | null;
chat: ChatLine[];
/** How many watch from the Peanut Gallery. */