A table's options and its size are read one way, in spec.ts, by the server, the tally and the replay; a game may seat fewer for some options

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:13:54 -04:00
co-authored by Claude Opus 5.5
parent faa2845e70
commit 6133a2469a
7 changed files with 64 additions and 23 deletions
+7 -16
View File
@@ -5,7 +5,7 @@
// game beyond the GameSpec it is given.
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
import { SPECTATOR, type GameSpec, type SeatId, type TableOptions } from '../../src/lib/game/spec';
import { cleanOptions, SPECTATOR, tableSize, type GameSpec, type SeatId, type TableOptions } from '../../src/lib/game/spec';
import type { ChatLine, RoomView } from '../../src/lib/net/view';
import type { LedgerLine, Store } from './store';
@@ -182,21 +182,12 @@ export class Rooms<State, Input> {
create(name: string, size: number, rawOptions?: unknown): { room: Room<State>; seat: Seat; token: string } {
const { minSeats } = this.game;
if (!Number.isInteger(size) || size < minSeats || size > this.maxSeats) {
throw new RoomError(`A table seats ${minSeats} to ${this.maxSeats} players.`);
const options = cleanOptions(this.game, rawOptions);
const most = tableSize(this.game, options);
if (!Number.isInteger(size) || size < minSeats || size > most) {
throw new RoomError(`A table seats ${minSeats} to ${most} players.`);
}
return this.open(name, size, this.cleanOptions(rawOptions));
}
/** The host's choices, kept to the options the game declares and the choices it offers; the rest default. */
private cleanOptions(raw: unknown): TableOptions {
const given = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>;
const options: TableOptions = {};
for (const o of this.game.options ?? []) {
const v = given[o.key];
options[o.key] = typeof v === 'string' && o.choices.some((c) => c.value === v) ? v : o.default;
}
return options;
return this.open(name, size, options);
}
/** A table with nobody seated yet. */
@@ -528,7 +519,7 @@ export class Rooms<State, Input> {
private replay(id: string, lines: LedgerLine[]): Room<State> | null {
const first = lines[0];
if (!first || first.t !== 'room') return null;
const room = this.blank(id, first.seats, first.createdAt, first.rematchOf ?? null, first.expected ?? [], this.cleanOptions(first.options), lines.length);
const room = this.blank(id, first.seats, first.createdAt, first.rematchOf ?? null, first.expected ?? [], cleanOptions(this.game, first.options), lines.length);
for (const line of lines) {
this.apply(room, line);
if (line.t === 'turn' || line.t === 'input') room.updatedAt = line.at;