A game may give its bots words: they speak as a round opens and when spoken to, remember by notes nobody sees, and read the talk when they move

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:18:46 -04:00
co-authored by Claude Opus 5.5
parent 8c99126aee
commit 99a0b23f59
6 changed files with 126 additions and 5 deletions
+1
View File
@@ -51,6 +51,7 @@
<div class="verdict">
<h2>{g.over.winner === room.me ? 'You win.' : g.over.winner ? `${g.players[g.over.winner].name} wins.` : 'A draw.'}</h2>
<p>{g.over.reason}</p>
{#each room.view.parting as last, i (i)}<p><em>{room.nameOf(last.id)}</em> {last.text}</p>{/each}
<a class="commit" href="/">Back to the hall</a>
</div>
{:else if room.spectating}
+40 -1
View File
@@ -13,6 +13,29 @@
/** A seat at the table: a single letter from GameSpec.seatIds. */
export type SeatId = string;
/**
* A line of table talk as a game reads it: who, what, when, the round it was
* said in, to whom (absent: everyone), and a bot's note on its own line.
*/
export interface PressLine {
id: SeatId;
text: string;
at: number;
/** The seats a private line was said to. */
to?: SeatId[];
/** The round the line was said in, from one; absent before the game begins. */
turn?: number;
/** What a bot noted about its own line; kept in the ledger, never shown. */
note?: unknown;
}
/** A bot's line of talk: to the people it names, with a note for its own memory. */
export interface BotLine {
to: SeatId[];
text: string;
note?: unknown;
}
/** The viewer with no seat: the Peanut Gallery. Views built for it show only what every seat could see. */
export const SPECTATOR: SeatId = '';
@@ -91,7 +114,23 @@ export interface GameSpec<State, Input> {
turn(state: State): number;
/** What one viewer may see. The server sends nothing else. */
view(state: State, viewer: SeatId): State;
botInput(state: State, seat: SeatId): Input;
/**
* A bot's move. `press` is the talk its seat has heard (the table's, and
* private lines to it or from it, notes included), for a game whose bots
* keep what was agreed; a function of the state and the press, which the
* turn line records, so a replay never asks again.
*/
botInput(state: State, seat: SeatId, press?: PressLine[]): Input;
/**
* What a bot says: when a round opens ('phase'), and when a person has
* just written to it, or said a first word to the table ('spoken').
* `press` is what its seat has heard; `people` the seats played by people.
* Each line goes on the ledger, to the people it names and no one else.
* Absent, bots say nothing and a private line to one is refused.
*/
botPress?(state: State, seat: SeatId, press: PressLine[], people: SeatId[], trigger: 'phase' | 'spoken'): BotLine[];
/** The bots' last words to `viewer` once the game is over, read from the whole of the talk. */
parting?(press: PressLine[], viewer: SeatId): { id: SeatId; text: string }[];
/** Only the shapes the engine understands get through; the engine validates the rest. */
cleanInput(raw: unknown): Input;
/** Why this seat may not make this move now, or null when it may. The server answers with the reason. */
+2
View File
@@ -44,6 +44,8 @@ export interface RoomView<State> {
/** 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[];
/** The bots' last words to this seat once the game is over, for a game that gives them some. */
parting: { id: SeatId; text: string }[];
/** How many watch from the Peanut Gallery. */
audience: number;
/** The host lets the gallery talk. */