Table talk may be private, to the seats named and never the gallery, and a line says the round it was said in

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:16:08 -04:00
co-authored by Claude Opus 5.5
parent 8e35d3a3a6
commit eca8c92964
9 changed files with 71 additions and 18 deletions
+24 -6
View File
@@ -9,8 +9,9 @@ import { cleanOptions, SPECTATOR, tableSize, type GameSpec, type SeatId, type Ta
import type { ChatLine, RoomView } from '../../src/lib/net/view';
import type { LedgerLine, Store } from './store';
/** The longest line of talk. A game of long negotiation raises this, and the talk box's maxlength with it. */
const CHAT_MAX_LENGTH = 300;
/** Lines of talk kept and sent; the ledger keeps them all. */
/** Lines of talk kept and sent; the ledger keeps them all. A game that runs for weeks of talk raises this. */
const CHAT_KEEP = 200;
export { SPECTATOR };
@@ -394,10 +395,18 @@ export class Rooms<State, Input> {
return this.game.settled ? this.game.settled(input as Input) : true;
}
/** Table talk, from a seat to everyone at the table and in the gallery. */
say(room: Room<State>, token: string | undefined, raw: unknown): void {
/**
* Table talk, from a seat: to everyone at the table and in the gallery,
* or, naming seats, privately to them alone. A private line is never
* shown to anyone else, the gallery included.
*/
say(room: Room<State>, token: string | undefined, raw: unknown, rawTo?: unknown): void {
const seat = this.seatOf(room, token);
this.commit(room, { t: 'chat', at: Date.now(), id: seat.id, text: this.cleanTalk(raw) });
const to = Array.isArray(rawTo)
? [...new Set(rawTo.filter((id): id is SeatId => typeof id === 'string' && id !== seat.id && room.seats.some((s) => s.id === id)))]
: [];
if (Array.isArray(rawTo) && rawTo.length && !to.length) throw new RoomError('Nobody at this table answers to that.');
this.commit(room, { t: 'chat', at: Date.now(), id: seat.id, text: this.cleanTalk(raw), ...(to.length ? { to } : {}) });
}
private cleanTalk(raw: unknown): string {
@@ -435,7 +444,8 @@ export class Rooms<State, Input> {
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,
// Private lines only to their sender and the seats they name; a note is the sender's own memory, never sent.
chat: room.chat.filter((line) => !line.to || line.id === seatId || (seated && line.to.includes(seatId))).map(({ note: _, ...line }) => line),
audience: this.gallery.get(room.id) ?? 0,
galleryTalk: room.galleryTalk,
challenge: room.challenge,
@@ -522,7 +532,15 @@ export class Rooms<State, Input> {
room.resolved += 1;
break;
case 'chat':
room.chat.push({ id: line.id, text: line.text, at: line.at, ...(line.name ? { name: line.name } : {}) });
room.chat.push({
id: line.id,
text: line.text,
at: line.at,
...(room.state ? { turn: this.game.turn(room.state) } : {}),
...(line.name ? { name: line.name } : {}),
...(line.to ? { to: line.to } : {}),
...(line.note !== undefined ? { note: line.note } : {})
});
if (room.chat.length > CHAT_KEEP) room.chat.splice(0, room.chat.length - CHAT_KEEP);
break;
}