Bots that play on alone do it at a pace to watch, a round every two seconds after the first few, and a seat may hurry them to the end
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
99a0b23f59
commit
403232087d
@@ -13,6 +13,12 @@ import type { LedgerLine, Store } from './store';
|
||||
const CHAT_MAX_LENGTH = 300;
|
||||
/** 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;
|
||||
/** Bots' rounds played in one go, when nobody else owes a move, before the rest are paced. */
|
||||
const PLAYED_AT_ONCE = 4;
|
||||
/** Between paced bots' rounds: long enough to watch the table move. */
|
||||
const PACE_MS = 2000;
|
||||
/** Bots' rounds played between answers to everyone else, once hurried. */
|
||||
const HURRIED_STEP = 3;
|
||||
export { SPECTATOR };
|
||||
|
||||
export interface Seat {
|
||||
@@ -117,6 +123,10 @@ export class Rooms<State, Input> {
|
||||
private transfers = new Map<string, { roomId: string; seatId: SeatId; token: string; expiresAt: number }>();
|
||||
private failedClaims = 0;
|
||||
private failWindowStart = 0;
|
||||
/** Tables whose bots are playing on, paced: the next round's timer. */
|
||||
private pacing = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
/** Tables asked to play the rest at once. */
|
||||
private hurried = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private game: GameSpec<State, Input>,
|
||||
@@ -315,18 +325,60 @@ export class Rooms<State, Input> {
|
||||
this.botsSpeak(room, 'phase');
|
||||
}
|
||||
|
||||
/** Rounds only bots owe (an opening move, an extra turn) resolve at once; a human's waits for them. */
|
||||
private driveBots(room: Room<State>): void {
|
||||
/**
|
||||
* Rounds only bots owe (an opening move, an extra turn, a table the
|
||||
* people are out of) resolve without waiting; a human's waits for them.
|
||||
* A few are played at once; past that, the bots play on one round at a
|
||||
* time, so the table can be watched and the server answers everyone else
|
||||
* between rounds. Hurried, they play in larger steps.
|
||||
*/
|
||||
private driveBots(room: Room<State>, burst = PLAYED_AT_ONCE): void {
|
||||
if (this.pacing.has(room.id)) return;
|
||||
const bots = (state: State) => room.seats.filter((s) => s.bot && this.game.needsInput(state, s.id));
|
||||
let played = 0;
|
||||
while (room.state && !this.game.over(room.state) && this.waitingOn(room).length === 0 && bots(room.state).length > 0) {
|
||||
if (played === burst) return this.playOnLater(room);
|
||||
const inputs: Record<SeatId, unknown> = {};
|
||||
for (const seat of bots(room.state)) inputs[seat.id] = this.game.botInput(room.state, seat.id, this.heardBy(room, seat.id));
|
||||
this.commit(room, { t: 'turn', at: Date.now(), inputs });
|
||||
played += 1;
|
||||
}
|
||||
this.hurried.delete(room.id);
|
||||
const outcome = room.state && this.game.over(room.state);
|
||||
if (outcome && !room.over) this.commit(room, { t: 'over', at: Date.now(), winner: outcome.winner, reason: outcome.reason });
|
||||
}
|
||||
|
||||
/** The bots' next round, after a pause to watch it by, or at once when hurried. */
|
||||
private playOnLater(room: Room<State>): void {
|
||||
const hurried = this.hurried.has(room.id);
|
||||
const next = setTimeout(() => {
|
||||
this.pacing.delete(room.id);
|
||||
// A room forgotten and loaded again drives itself; this one is no longer the table.
|
||||
if (this.rooms.get(room.id) !== room) return;
|
||||
this.driveBots(room, hurried ? HURRIED_STEP : 1);
|
||||
// Played out: the bots have their say once, as after any round, which ends a game with parting words.
|
||||
if (!this.pacing.has(room.id)) this.botsSpeak(room, 'phase');
|
||||
}, hurried ? 0 : PACE_MS);
|
||||
next.unref();
|
||||
this.pacing.set(room.id, next);
|
||||
}
|
||||
|
||||
/** A seat at a table the bots are playing on asks for the rest at once. */
|
||||
hurry(room: Room<State>, token: string | undefined): void {
|
||||
this.seatOf(room, token);
|
||||
this.hurried.add(room.id);
|
||||
const waiting = this.pacing.get(room.id);
|
||||
if (!waiting) return;
|
||||
clearTimeout(waiting);
|
||||
this.pacing.delete(room.id);
|
||||
this.playOnLater(room);
|
||||
}
|
||||
|
||||
/** Whether the bots are playing on by themselves at this table, one paced round at a time. */
|
||||
playingOn(room: Room<State>): boolean {
|
||||
return this.pacing.has(room.id);
|
||||
}
|
||||
|
||||
/** The host seats a bot in an empty chair. */
|
||||
addBot(room: Room<State>, token: string | undefined): Seat {
|
||||
const host = this.seatOf(room, token);
|
||||
@@ -494,6 +546,7 @@ export class Rooms<State, Input> {
|
||||
// Private lines only to their sender and the seats they name; a note is a bot's own memory, never sent.
|
||||
chat: this.heardBy(room, seatId).map(({ note: _, ...line }) => line),
|
||||
parting: room.state && seated && this.game.parting && this.game.over(room.state) ? this.game.parting(room.chat, seatId) : [],
|
||||
playingOn: this.playingOn(room),
|
||||
audience: this.gallery.get(room.id) ?? 0,
|
||||
galleryTalk: room.galleryTalk,
|
||||
challenge: room.challenge,
|
||||
|
||||
Reference in New Issue
Block a user