From 8e35d3a3a6802ddde95ef20f1329fda9f5949979 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 29 Sep 2026 12:15:05 -0400 Subject: [PATCH] A seat's view carries its own move in waiting and its move in the last round it played Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- CONVENTIONS.md | 5 ++++- template/server/src/rooms.test.ts | 13 +++++++++++++ template/server/src/rooms.ts | 8 +++++++- template/src/lib/net/view.ts | 4 ++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 682faa3..77c7ae1 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -132,7 +132,10 @@ if it were never there. - `seatsFor(options)`: fewer seats for some table options. - `settled(input)`: whether an input is final. A game with drafts answers false until the player says it is done; a draft may be replaced, and the - round waits for every seat's settled input. + round waits for every seat's settled input. A seat's view carries its + own input in waiting (`yourInput`), so a page reopened shows the draft, + and its input in the last round it played (`yourLastInput`), so the page + can tell it what became of its own choice. - `tally(state)`: the game's own lines for the tally, from a finished game. - `preferences`, `options`: the preferences panel's settings and the host's table choices (above). diff --git a/template/server/src/rooms.test.ts b/template/server/src/rooms.test.ts index ec6f63c..a9ded26 100644 --- a/template/server/src/rooms.test.ts +++ b/template/server/src/rooms.test.ts @@ -63,4 +63,17 @@ describe('rooms', () => { rooms.submit(room, bo, { pick: 1, final: true }, 1); expect(() => rooms.submit(room, bo, { pick: 2 }, 1)).toThrow(/not your move/); }); + + it("shows a seat its own move in waiting and its last move played, and nobody else's", () => { + const { rooms, room, ann, bo } = table(); + rooms.submit(room, ann, { pick: 5 }, 0); + expect(rooms.view(room, 'A').yourInput).toEqual({ pick: 5 }); + expect(rooms.view(room, 'B').yourInput).toBeNull(); + expect(rooms.view(room, '').yourInput).toBeNull(); + rooms.submit(room, bo, { pick: 2 }, 0); + expect(rooms.view(room, 'A').yourInput).toBeNull(); + expect(rooms.view(room, 'A').yourLastInput).toEqual({ pick: 5 }); + expect(rooms.view(room, 'B').yourLastInput).toEqual({ pick: 2 }); + expect(rooms.view(room, '').yourLastInput).toBeNull(); + }); }); diff --git a/template/server/src/rooms.ts b/template/server/src/rooms.ts index 7aad504..96e8a12 100644 --- a/template/server/src/rooms.ts +++ b/template/server/src/rooms.ts @@ -48,6 +48,8 @@ export interface Room { state: State | null; /** Inputs received for the round in progress, by seat. */ pending: Record; + /** Each seat's input in the last round it played, kept through rounds it had no part in; each seat is shown only its own. */ + played: Record; chat: ChatLine[]; /** Rounds played so far; a move names the count it was made against. */ resolved: number; @@ -192,7 +194,7 @@ export class Rooms { /** A table with nobody seated yet. */ private blank(id: string, size: number, createdAt: number, rematchOf: string | null, expected: string[], options: TableOptions, seq = 0): Room { - return { id, size, createdAt, seats: [], galleryTalk: false, challenge: null, expected, rematch: null, rematchOf, options, over: false, state: null, pending: {}, chat: [], resolved: 0, seq, updatedAt: createdAt }; + return { id, size, createdAt, seats: [], galleryTalk: false, challenge: null, expected, rematch: null, rematchOf, options, over: false, state: null, pending: {}, played: {}, chat: [], resolved: 0, seq, updatedAt: createdAt }; } /** Open a table with its first seat taken; a rematch names the table it follows and holds seats for its players. */ @@ -418,6 +420,7 @@ export class Rooms { /** The view for a seat, or for the gallery when the seat is SPECTATOR. */ view(room: Room, seatId: SeatId): RoomView { + const seated = seatId !== SPECTATOR; return { roomId: room.id, seq: room.seq, @@ -430,6 +433,8 @@ export class Rooms { resolved: room.resolved, over: room.state ? this.game.over(room.state) : null, state: room.state ? this.game.view(room.state, seatId) : null, + yourInput: seated && seatId in room.pending ? room.pending[seatId] : null, + yourLastInput: seated && seatId in room.played ? room.played[seatId] : null, chat: room.chat, audience: this.gallery.get(room.id) ?? 0, galleryTalk: room.galleryTalk, @@ -512,6 +517,7 @@ export class Rooms { case 'turn': if (!room.state) return; room.state = this.game.resolve(room.state, line.inputs as Record); + room.played = { ...room.played, ...(line.inputs as Record) }; room.pending = {}; room.resolved += 1; break; diff --git a/template/src/lib/net/view.ts b/template/src/lib/net/view.ts index 812dc99..16c5933 100644 --- a/template/src/lib/net/view.ts +++ b/template/src/lib/net/view.ts @@ -29,6 +29,10 @@ export interface RoomView { turn: number | null; over: Outcome | null; state: State | null; + /** This seat's input for the round in progress, as the server holds it (a draft, or a settled move); null for none, and for the gallery. */ + yourInput: unknown; + /** This seat's input in the last round it played, for telling it what became of its own choice; null for none, and for the gallery. */ + yourLastInput: unknown; chat: ChatLine[]; /** How many watch from the Peanut Gallery. */ audience: number;