A bot whose move opens the game, or is owed after a restore, plays it at once; the over line is written once
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
23b72ddc6d
commit
ef617a50c2
@@ -43,6 +43,8 @@ export interface Room<State> {
|
|||||||
rematchOf: string | null;
|
rematchOf: string | null;
|
||||||
/** The host's choices for this table, every declared option filled. */
|
/** The host's choices for this table, every declared option filled. */
|
||||||
options: TableOptions;
|
options: TableOptions;
|
||||||
|
/** The over line has been written. */
|
||||||
|
over: boolean;
|
||||||
state: State | null;
|
state: State | null;
|
||||||
/** Inputs received for the round in progress, by seat. */
|
/** Inputs received for the round in progress, by seat. */
|
||||||
pending: Record<SeatId, unknown>;
|
pending: Record<SeatId, unknown>;
|
||||||
@@ -113,7 +115,10 @@ export class Rooms<State, Input> {
|
|||||||
) {
|
) {
|
||||||
for (const id of store.roomIds()) {
|
for (const id of store.roomIds()) {
|
||||||
const room = this.replay(id, store.read(id));
|
const room = this.replay(id, store.read(id));
|
||||||
if (room) this.rooms.set(id, room);
|
if (!room) continue;
|
||||||
|
this.rooms.set(id, room);
|
||||||
|
// A table that stopped with a bot's move owed picks up where it left off.
|
||||||
|
if (room.state) this.driveBots(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +136,10 @@ export class Rooms<State, Input> {
|
|||||||
private load(id: string): Room<State> | undefined {
|
private load(id: string): Room<State> | undefined {
|
||||||
if (!/^[A-Z0-9]{4}$/.test(id)) return undefined;
|
if (!/^[A-Z0-9]{4}$/.test(id)) return undefined;
|
||||||
const room = this.replay(id, this.store.read(id));
|
const room = this.replay(id, this.store.read(id));
|
||||||
if (room) this.rooms.set(id, room);
|
if (!room) return undefined;
|
||||||
return room ?? undefined;
|
this.rooms.set(id, room);
|
||||||
|
if (room.state) this.driveBots(room);
|
||||||
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Forget rooms nobody has touched or watched for a while; their ledgers stay on disk. */
|
/** Forget rooms nobody has touched or watched for a while; their ledgers stay on disk. */
|
||||||
@@ -181,7 +188,7 @@ export class Rooms<State, Input> {
|
|||||||
|
|
||||||
/** A table with nobody seated yet. */
|
/** A table with nobody seated yet. */
|
||||||
private blank(id: string, size: number, createdAt: number, rematchOf: string | null, expected: string[], options: TableOptions, seq = 0): Room<State> {
|
private blank(id: string, size: number, createdAt: number, rematchOf: string | null, expected: string[], options: TableOptions, seq = 0): Room<State> {
|
||||||
return { id, size, createdAt, seats: [], galleryTalk: false, challenge: null, expected, rematch: null, rematchOf, options, state: null, pending: {}, chat: [], seq, updatedAt: createdAt };
|
return { id, size, createdAt, seats: [], galleryTalk: false, challenge: null, expected, rematch: null, rematchOf, options, over: false, state: null, pending: {}, chat: [], seq, updatedAt: createdAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open a table with its first seat taken; a rematch names the table it follows and holds seats for its players. */
|
/** Open a table with its first seat taken; a rematch names the table it follows and holds seats for its players. */
|
||||||
@@ -299,6 +306,20 @@ export class Rooms<State, Input> {
|
|||||||
if (room.state) throw new RoomError('The game has already begun.', 409);
|
if (room.state) throw new RoomError('The game has already begun.', 409);
|
||||||
if (room.seats.length < this.game.minSeats) throw new RoomError(`The game needs at least ${this.game.minSeats} players.`, 409);
|
if (room.seats.length < this.game.minSeats) throw new RoomError(`The game needs at least ${this.game.minSeats} players.`, 409);
|
||||||
this.commit(room, { t: 'start', seed: newSeed(), rules: this.game.currentRules });
|
this.commit(room, { t: 'start', seed: newSeed(), rules: this.game.currentRules });
|
||||||
|
// A game whose first move belongs to a bot must not wait for a human who is not owed a move.
|
||||||
|
this.driveBots(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 {
|
||||||
|
const bots = (state: State) => room.seats.filter((s) => s.bot && this.game.needsInput(state, s.id));
|
||||||
|
while (room.state && !this.game.over(room.state) && this.waitingOn(room).length === 0 && bots(room.state).length > 0) {
|
||||||
|
const inputs: Record<SeatId, unknown> = {};
|
||||||
|
for (const seat of bots(room.state)) inputs[seat.id] = this.game.botInput(room.state, seat.id);
|
||||||
|
this.commit(room, { t: 'turn', at: Date.now(), inputs });
|
||||||
|
}
|
||||||
|
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 host seats a bot in an empty chair. */
|
/** The host seats a bot in an empty chair. */
|
||||||
@@ -352,18 +373,10 @@ export class Rooms<State, Input> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resolve(room: Room<State>): void {
|
private resolve(room: Room<State>): void {
|
||||||
const bots = (state: State) => room.seats.filter((s) => s.bot && this.game.needsInput(state, s.id));
|
|
||||||
const inputs: Record<SeatId, unknown> = { ...room.pending };
|
const inputs: Record<SeatId, unknown> = { ...room.pending };
|
||||||
for (const seat of bots(room.state!)) inputs[seat.id] = this.game.botInput(room.state!, seat.id);
|
for (const seat of room.seats) if (seat.bot && this.game.needsInput(room.state!, seat.id)) inputs[seat.id] = this.game.botInput(room.state!, seat.id);
|
||||||
this.commit(room, { t: 'turn', at: Date.now(), inputs });
|
this.commit(room, { t: 'turn', at: Date.now(), inputs });
|
||||||
// A round only bots owe (an extra turn, say) resolves at once; a human's waits for them.
|
this.driveBots(room);
|
||||||
while (room.state && !this.game.over(room.state) && this.waitingOn(room).length === 0 && bots(room.state).length > 0) {
|
|
||||||
const inputs: Record<SeatId, unknown> = {};
|
|
||||||
for (const seat of bots(room.state)) inputs[seat.id] = this.game.botInput(room.state, seat.id);
|
|
||||||
this.commit(room, { t: 'turn', at: Date.now(), inputs });
|
|
||||||
}
|
|
||||||
const outcome = room.state && this.game.over(room.state);
|
|
||||||
if (outcome) this.commit(room, { t: 'over', at: Date.now(), winner: outcome.winner, reason: outcome.reason });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Table talk, from a seat to everyone at the table and in the gallery. */
|
/** Table talk, from a seat to everyone at the table and in the gallery. */
|
||||||
@@ -456,7 +469,9 @@ export class Rooms<State, Input> {
|
|||||||
private apply(room: Room<State>, line: LedgerLine): void {
|
private apply(room: Room<State>, line: LedgerLine): void {
|
||||||
switch (line.t) {
|
switch (line.t) {
|
||||||
case 'room':
|
case 'room':
|
||||||
|
break;
|
||||||
case 'over':
|
case 'over':
|
||||||
|
room.over = true;
|
||||||
break;
|
break;
|
||||||
case 'seat':
|
case 'seat':
|
||||||
room.seats.push({ id: line.id, name: line.name, tokenHash: line.tokenHash ?? (line.token ? hashToken(line.token) : ''), bot: line.bot });
|
room.seats.push({ id: line.id, name: line.name, tokenHash: line.tokenHash ?? (line.token ? hashToken(line.token) : ''), bot: line.bot });
|
||||||
|
|||||||
Reference in New Issue
Block a user