Table options: the host's choices when opening a table, declared by the game, recorded on the room line
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
2ab6dc4b5f
commit
63bd154798
@@ -64,6 +64,11 @@ README.
|
|||||||
where the keeper roosts. It lives in the about panel; the colophons, the
|
where the keeper roosts. It lives in the about panel; the colophons, the
|
||||||
rules page and the guide each carry a one-line mail link.
|
rules page and the guide each carry a one-line mail link.
|
||||||
|
|
||||||
|
- **Table options** are the host's choices when opening a table (a
|
||||||
|
variant, a side, a length): the game declares them in `GameSpec.options`,
|
||||||
|
the hall renders them as selects, the room line records them, and
|
||||||
|
`create` receives them. A game with one way to play declares none.
|
||||||
|
|
||||||
## The engine
|
## The engine
|
||||||
|
|
||||||
- Pure and deterministic: `create(names, seed, rules)` and
|
- Pure and deterministic: `create(names, seed, rules)` and
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// The front door for games between people. Plain HTTP for actions, a
|
// The front door for games between people. Plain HTTP for actions, a
|
||||||
// websocket only to say "something changed, fetch the view again".
|
// websocket only to say "something changed, fetch the view again".
|
||||||
//
|
//
|
||||||
// POST /api/rooms {name, size?} create a room and take seat A
|
// POST /api/rooms {name, size?, options?} create a room and take seat A; options are the game's table choices
|
||||||
// POST /api/rooms/:id/join {name} take the next seat
|
// POST /api/rooms/:id/join {name} take the next seat
|
||||||
// POST /api/rooms/:id/bot {token} the host seats a bot, before the game begins
|
// POST /api/rooms/:id/bot {token} the host seats a bot, before the game begins
|
||||||
// POST /api/rooms/:id/begin {token} the host begins with the players seated so far
|
// POST /api/rooms/:id/begin {token} the host begins with the players seated so far
|
||||||
@@ -188,7 +188,7 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void>
|
|||||||
if (parts.length === 2 && req.method === 'POST') {
|
if (parts.length === 2 && req.method === 'POST') {
|
||||||
if (!doors.allow(clientOf(req))) throw new RoomError('Too many rooms opened from here just now; try again later.', 429);
|
if (!doors.allow(clientOf(req))) throw new RoomError('Too many rooms opened from here just now; try again later.', 429);
|
||||||
const body = await readBody(req);
|
const body = await readBody(req);
|
||||||
const { room, seat, token } = rooms.create(String(body.name ?? ''), Number(body.size ?? game.minSeats));
|
const { room, seat, token } = rooms.create(String(body.name ?? ''), Number(body.size ?? game.minSeats), body.options);
|
||||||
return send(res, 201, { seat: seat.id, token, view: rooms.view(room, seat.id) });
|
return send(res, 201, { seat: seat.id, token, view: rooms.view(room, seat.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// game beyond the GameSpec it is given.
|
// game beyond the GameSpec it is given.
|
||||||
|
|
||||||
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
||||||
import { SPECTATOR, type GameSpec, type SeatId } from '../../src/lib/game/spec';
|
import { SPECTATOR, type GameSpec, type SeatId, type TableOptions } from '../../src/lib/game/spec';
|
||||||
import type { ChatLine, RoomView } from '../../src/lib/net/view';
|
import type { ChatLine, RoomView } from '../../src/lib/net/view';
|
||||||
import type { LedgerLine, Store } from './store';
|
import type { LedgerLine, Store } from './store';
|
||||||
|
|
||||||
@@ -41,6 +41,8 @@ export interface Room<State> {
|
|||||||
rematch: { to: string; by: SeatId } | null;
|
rematch: { to: string; by: SeatId } | null;
|
||||||
/** The table this one is the rematch of. */
|
/** The table this one is the rematch of. */
|
||||||
rematchOf: string | null;
|
rematchOf: string | null;
|
||||||
|
/** The host's choices for this table, every declared option filled. */
|
||||||
|
options: TableOptions;
|
||||||
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>;
|
||||||
@@ -158,26 +160,37 @@ export class Rooms<State, Input> {
|
|||||||
return seat;
|
return seat;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(name: string, size: number): { room: Room<State>; seat: Seat; token: string } {
|
create(name: string, size: number, rawOptions?: unknown): { room: Room<State>; seat: Seat; token: string } {
|
||||||
const { minSeats } = this.game;
|
const { minSeats } = this.game;
|
||||||
if (!Number.isInteger(size) || size < minSeats || size > this.maxSeats) {
|
if (!Number.isInteger(size) || size < minSeats || size > this.maxSeats) {
|
||||||
throw new RoomError(`A table seats ${minSeats} to ${this.maxSeats} players.`);
|
throw new RoomError(`A table seats ${minSeats} to ${this.maxSeats} players.`);
|
||||||
}
|
}
|
||||||
return this.open(name, size);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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[], 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, state: null, pending: {}, chat: [], seq, updatedAt: createdAt };
|
return { id, size, createdAt, seats: [], galleryTalk: false, challenge: null, expected, rematch: null, rematchOf, options, 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. */
|
||||||
private open(name: string, size: number, follows?: { rematchOf: string; expected: string[] }): { room: Room<State>; seat: Seat; token: string } {
|
private open(name: string, size: number, options: TableOptions, follows?: { rematchOf: string; expected: string[] }): { room: Room<State>; seat: Seat; token: string } {
|
||||||
let id = newCode();
|
let id = newCode();
|
||||||
while (this.rooms.has(id) || this.store.exists(id)) id = newCode();
|
while (this.rooms.has(id) || this.store.exists(id)) id = newCode();
|
||||||
const room = this.blank(id, size, Date.now(), follows?.rematchOf ?? null, follows?.expected ?? []);
|
const room = this.blank(id, size, Date.now(), follows?.rematchOf ?? null, follows?.expected ?? [], options);
|
||||||
this.rooms.set(id, room);
|
this.rooms.set(id, room);
|
||||||
this.commit(room, { t: 'room', id, seats: size, createdAt: room.createdAt, ...(follows ? { rematchOf: follows.rematchOf, expected: follows.expected } : {}) });
|
this.commit(room, { t: 'room', id, seats: size, createdAt: room.createdAt, options, ...(follows ? { rematchOf: follows.rematchOf, expected: follows.expected } : {}) });
|
||||||
return { room, ...this.sit(room, name, false) };
|
return { room, ...this.sit(room, name, false) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +245,7 @@ export class Rooms<State, Input> {
|
|||||||
if (!room.state || !this.game.over(room.state)) throw new RoomError('The game is not over yet.', 409);
|
if (!room.state || !this.game.over(room.state)) throw new RoomError('The game is not over yet.', 409);
|
||||||
if (room.rematch) return { roomId: room.rematch.to, seat: null, token: null, created: false };
|
if (room.rematch) return { roomId: room.rematch.to, seat: null, token: null, created: false };
|
||||||
const others = room.seats.filter((s) => !s.bot && s.id !== caller.id).map((s) => s.name);
|
const others = room.seats.filter((s) => !s.bot && s.id !== caller.id).map((s) => s.name);
|
||||||
const next = this.open(caller.name, room.size, { rematchOf: room.id, expected: others });
|
const next = this.open(caller.name, room.size, room.options, { rematchOf: room.id, expected: others });
|
||||||
for (const bot of room.seats.filter((s) => s.bot)) this.sit(next.room, bot.name, true);
|
for (const bot of room.seats.filter((s) => s.bot)) this.sit(next.room, bot.name, true);
|
||||||
this.commit(room, { t: 'rematch', at: Date.now(), to: next.room.id, by: caller.id });
|
this.commit(room, { t: 'rematch', at: Date.now(), to: next.room.id, by: caller.id });
|
||||||
return { roomId: next.room.id, seat: next.seat, token: next.token, created: true };
|
return { roomId: next.room.id, seat: next.seat, token: next.token, created: true };
|
||||||
@@ -397,6 +410,7 @@ export class Rooms<State, Input> {
|
|||||||
expected: this.awaited(room),
|
expected: this.awaited(room),
|
||||||
rematch: room.rematch,
|
rematch: room.rematch,
|
||||||
rematchOf: room.rematchOf,
|
rematchOf: room.rematchOf,
|
||||||
|
options: room.options,
|
||||||
keeper: this.keeper.name,
|
keeper: this.keeper.name,
|
||||||
keeperZone: this.keeper.zone
|
keeperZone: this.keeper.zone
|
||||||
};
|
};
|
||||||
@@ -464,7 +478,7 @@ export class Rooms<State, Input> {
|
|||||||
room.seats = room.seats.filter((s) => s.id !== line.id);
|
room.seats = room.seats.filter((s) => s.id !== line.id);
|
||||||
break;
|
break;
|
||||||
case 'start':
|
case 'start':
|
||||||
room.state = this.game.create(Object.fromEntries(room.seats.map((s) => [s.id, s.name])), line.seed, line.rules ?? 1);
|
room.state = this.game.create(Object.fromEntries(room.seats.map((s) => [s.id, s.name])), line.seed, line.rules ?? 1, room.options);
|
||||||
break;
|
break;
|
||||||
case 'turn':
|
case 'turn':
|
||||||
if (!room.state) return;
|
if (!room.state) return;
|
||||||
@@ -481,7 +495,7 @@ export class Rooms<State, Input> {
|
|||||||
private replay(id: string, lines: LedgerLine[]): Room<State> | null {
|
private replay(id: string, lines: LedgerLine[]): Room<State> | null {
|
||||||
const first = lines[0];
|
const first = lines[0];
|
||||||
if (!first || first.t !== 'room') return null;
|
if (!first || first.t !== 'room') return null;
|
||||||
const room = this.blank(id, first.seats, first.createdAt, first.rematchOf ?? null, first.expected ?? [], lines.length);
|
const room = this.blank(id, first.seats, first.createdAt, first.rematchOf ?? null, first.expected ?? [], this.cleanOptions(first.options), lines.length);
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
this.apply(room, line);
|
this.apply(room, line);
|
||||||
if (line.t === 'turn' || line.t === 'input') room.updatedAt = line.at;
|
if (line.t === 'turn' || line.t === 'input') room.updatedAt = line.at;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { SeatId } from '../../src/lib/game/spec';
|
|||||||
|
|
||||||
export type LedgerLine =
|
export type LedgerLine =
|
||||||
/** `rematchOf` and `expected` mark a table opened for a rematch: whose it follows, and whose seats are held. */
|
/** `rematchOf` and `expected` mark a table opened for a rematch: whose it follows, and whose seats are held. */
|
||||||
| { t: 'room'; id: string; seats: number; createdAt: number; rematchOf?: string; expected?: string[] }
|
| { t: 'room'; id: string; seats: number; createdAt: number; rematchOf?: string; expected?: string[]; options?: Record<string, string> }
|
||||||
/** A seat holds only the hash of its token; the token itself goes once to the browser that earned it. Ledgers written before hashing carry `token` and are read once more. */
|
/** A seat holds only the hash of its token; the token itself goes once to the browser that earned it. Ledgers written before hashing carry `token` and are read once more. */
|
||||||
| { t: 'seat'; id: SeatId; name: string; tokenHash?: string; token?: string; bot: boolean }
|
| { t: 'seat'; id: SeatId; name: string; tokenHash?: string; token?: string; bot: boolean }
|
||||||
/** One seat's move for the round in progress, so a restart keeps it. The turn line that follows carries every input again. */
|
/** One seat's move for the round in progress, so a restart keeps it. The turn line that follows carries every input again. */
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
let name = $state(playerName());
|
let name = $state(playerName());
|
||||||
let code = $state('');
|
let code = $state('');
|
||||||
|
/** The host's choices for the next table, from the game's declared options. */
|
||||||
|
let options = $state<Record<string, string>>(Object.fromEntries((game.options ?? []).map((o) => [o.key, o.default])));
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
let seats = $state(allSeats());
|
let seats = $state(allSeats());
|
||||||
@@ -157,9 +159,21 @@
|
|||||||
<span>Your name</span>
|
<span>Your name</span>
|
||||||
<input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="e.g. Morwenna" autocomplete="nickname" />
|
<input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="e.g. Morwenna" autocomplete="nickname" />
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="commit primary" disabled={!ready || busy} onclick={() => open(() => Room.createWithBot(name.trim()))}>Play now against the bot</button>
|
{#if game.options?.length}
|
||||||
|
<div class="options">
|
||||||
|
{#each game.options as o (o.key)}
|
||||||
|
<label class="option">
|
||||||
|
<span>{o.label}</span>
|
||||||
|
<select bind:value={options[o.key]}>
|
||||||
|
{#each o.choices as c (c.value)}<option value={c.value}>{c.label}</option>{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="commit primary" disabled={!ready || busy} onclick={() => open(() => Room.createWithBot(name.trim(), options))}>Play now against the bot</button>
|
||||||
<div class="ways">
|
<div class="ways">
|
||||||
<button type="button" class="quiet" disabled={!ready || busy} onclick={() => open(() => Room.create(name.trim(), game.seatIds.length))}>Open a table</button>
|
<button type="button" class="quiet" disabled={!ready || busy} onclick={() => open(() => Room.create(name.trim(), game.seatIds.length, options))}>Open a table</button>
|
||||||
<form class="joinform" onsubmit={join}>
|
<form class="joinform" onsubmit={join}>
|
||||||
<span class="or">or join one</span>
|
<span class="or">or join one</span>
|
||||||
<input type="text" class="code" bind:value={code} maxlength="4" placeholder="CODE" autocapitalize="characters" autocomplete="off" aria-label="room code" />
|
<input type="text" class="code" bind:value={code} maxlength="4" placeholder="CODE" autocapitalize="characters" autocomplete="off" aria-label="room code" />
|
||||||
@@ -551,4 +565,17 @@
|
|||||||
.family li + li {
|
.family li + li {
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
}
|
}
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem 1.2rem;
|
||||||
|
margin: 0.2rem 0 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// withholds the round in progress, and a rules revision. Replace this file
|
// withholds the round in progress, and a rules revision. Replace this file
|
||||||
// with your own game and keep the GameSpec shape.
|
// with your own game and keep the GameSpec shape.
|
||||||
|
|
||||||
import { SPECTATOR, type GameSpec, type Outcome, type SeatId } from './spec';
|
import { SPECTATOR, type GameSpec, type Outcome, type SeatId, type TableOptions } from './spec';
|
||||||
|
|
||||||
export const CURRENT_RULES = 1;
|
export const CURRENT_RULES = 1;
|
||||||
export const TARGET = 3;
|
export const TARGET = 3;
|
||||||
@@ -41,7 +41,7 @@ function random(seed: number): number {
|
|||||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGame(names: Record<SeatId, string>, seed = Date.now(), rules = CURRENT_RULES): State {
|
export function createGame(names: Record<SeatId, string>, seed = Date.now(), rules = CURRENT_RULES, _options?: TableOptions): State {
|
||||||
const seats = Object.keys(names);
|
const seats = Object.keys(names);
|
||||||
return {
|
return {
|
||||||
rules,
|
rules,
|
||||||
|
|||||||
@@ -21,9 +21,23 @@ export interface Outcome {
|
|||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A choice the host makes when opening a table: a variant, a side, a length. */
|
||||||
|
export interface TableOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
choices: { value: string; label: string }[];
|
||||||
|
/** The value a table gets when the host chooses nothing. */
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The host's choices for a table, by option key; only keys the game declares get through. */
|
||||||
|
export type TableOptions = Record<string, string>;
|
||||||
|
|
||||||
export interface GameSpec<State, Input> {
|
export interface GameSpec<State, Input> {
|
||||||
/** Seats in table order; the table takes at most this many. */
|
/** Seats in table order; the table takes at most this many. */
|
||||||
seatIds: SeatId[];
|
seatIds: SeatId[];
|
||||||
|
/** What the host may choose when opening a table; empty for a game with one way to play. */
|
||||||
|
options?: TableOption[];
|
||||||
minSeats: number;
|
minSeats: number;
|
||||||
/** Names the server draws for bots, in preference order. */
|
/** Names the server draws for bots, in preference order. */
|
||||||
botNames: string[];
|
botNames: string[];
|
||||||
@@ -35,7 +49,7 @@ export interface GameSpec<State, Input> {
|
|||||||
*/
|
*/
|
||||||
currentRules: number;
|
currentRules: number;
|
||||||
|
|
||||||
create(names: Record<SeatId, string>, seed: number, rules: number): State;
|
create(names: Record<SeatId, string>, seed: number, rules: number, options?: TableOptions): State;
|
||||||
/** Resolve one round. Must be a pure function of its arguments: the ledger is replayed through it. */
|
/** Resolve one round. Must be a pure function of its arguments: the ledger is replayed through it. */
|
||||||
resolve(state: State, inputs: Record<SeatId, Input>): State;
|
resolve(state: State, inputs: Record<SeatId, Input>): State;
|
||||||
/** Whether this seat's input is needed before the next resolution. */
|
/** Whether this seat's input is needed before the next resolution. */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// The browser's side of the game server: a few JSON calls, and the seats this
|
// The browser's side of the game server: a few JSON calls, and the seats this
|
||||||
// browser holds, remembered so a shared link never has to carry a token.
|
// browser holds, remembered so a shared link never has to carry a token.
|
||||||
|
|
||||||
import type { SeatId } from '$lib/game/spec';
|
import type { SeatId, TableOptions } from '$lib/game/spec';
|
||||||
import type { RoomView } from './view';
|
import type { RoomView } from './view';
|
||||||
|
|
||||||
export interface HeldSeat {
|
export interface HeldSeat {
|
||||||
@@ -132,7 +132,7 @@ export function makeApi<State, Input>() {
|
|||||||
type Seated = { seat: SeatId; token: string; view: RoomView<State> };
|
type Seated = { seat: SeatId; token: string; view: RoomView<State> };
|
||||||
type View = RoomView<State>;
|
type View = RoomView<State>;
|
||||||
return {
|
return {
|
||||||
create: (name: string, size: number) => call<Seated>('POST', '/api/rooms', { name, size }),
|
create: (name: string, size: number, options?: TableOptions) => call<Seated>('POST', '/api/rooms', { name, size, options }),
|
||||||
join: (roomId: string, name: string) => call<Seated>('POST', `/api/rooms/${roomId}/join`, { name }),
|
join: (roomId: string, name: string) => call<Seated>('POST', `/api/rooms/${roomId}/join`, { name }),
|
||||||
addBot: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/bot`, { token }),
|
addBot: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/bot`, { token }),
|
||||||
begin: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/begin`, { token }),
|
begin: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/begin`, { token }),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// through GameSpec; a game's own store wraps or extends this one.
|
// through GameSpec; a game's own store wraps or extends this one.
|
||||||
|
|
||||||
import { game } from '$lib/game';
|
import { game } from '$lib/game';
|
||||||
import { SPECTATOR, type SeatId } from '$lib/game/spec';
|
import { SPECTATOR, type SeatId, type TableOptions } from '$lib/game/spec';
|
||||||
import type { Input, State } from '$lib/game';
|
import type { Input, State } from '$lib/game';
|
||||||
import { makeApi, rememberSeat, watchRoom, type HeldSeat } from './client';
|
import { makeApi, rememberSeat, watchRoom, type HeldSeat } from './client';
|
||||||
import { markTalkSeen } from './talk';
|
import { markTalkSeen } from './talk';
|
||||||
@@ -152,15 +152,15 @@ export class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Open a table on the server, seated as its host; it begins when the host says. */
|
/** Open a table on the server, seated as its host; it begins when the host says. */
|
||||||
static async create(name: string, size: number): Promise<Room> {
|
static async create(name: string, size: number, options?: TableOptions): Promise<Room> {
|
||||||
const seated = await api.create(name, size);
|
const seated = await api.create(name, size, options);
|
||||||
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
|
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
|
||||||
return new Room(seated.view, seated.token);
|
return new Room(seated.view, seated.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A room with a bot seated and the game begun, so a solo game is a ledger like any other. */
|
/** A room with a bot seated and the game begun, so a solo game is a ledger like any other. */
|
||||||
static async createWithBot(name: string): Promise<Room> {
|
static async createWithBot(name: string, options?: TableOptions): Promise<Room> {
|
||||||
const seated = await api.create(name, 2);
|
const seated = await api.create(name, 2, options);
|
||||||
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
|
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
|
||||||
await api.addBot(seated.view.roomId, seated.token);
|
await api.addBot(seated.view.roomId, seated.token);
|
||||||
return new Room(await api.begin(seated.view.roomId, seated.token), seated.token);
|
return new Room(await api.begin(seated.view.roomId, seated.token), seated.token);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// What the server tells a viewer about a room. The game's own state is
|
// What the server tells a viewer about a room. The game's own state is
|
||||||
// whatever GameSpec.view returned for that viewer; nothing else leaves.
|
// whatever GameSpec.view returned for that viewer; nothing else leaves.
|
||||||
|
|
||||||
import type { Outcome, SeatId } from '../game/spec';
|
import type { Outcome, SeatId, TableOptions } from '../game/spec';
|
||||||
|
|
||||||
/** One line of table talk: from a seat, or from the Peanut Gallery when the host allows it (id SPECTATOR, signed with `name`). */
|
/** One line of table talk: from a seat, or from the Peanut Gallery when the host allows it (id SPECTATOR, signed with `name`). */
|
||||||
export interface ChatLine {
|
export interface ChatLine {
|
||||||
@@ -40,6 +40,8 @@ export interface RoomView<State> {
|
|||||||
rematch: { to: string; by: SeatId } | null;
|
rematch: { to: string; by: SeatId } | null;
|
||||||
/** The table this one is the rematch of. */
|
/** The table this one is the rematch of. */
|
||||||
rematchOf: string | null;
|
rematchOf: string | null;
|
||||||
|
/** The host's choices for this table, every declared option filled. */
|
||||||
|
options: TableOptions;
|
||||||
/** The keeper of the site, and where they live, for a table that calls them. */
|
/** The keeper of the site, and where they live, for a table that calls them. */
|
||||||
keeper: string;
|
keeper: string;
|
||||||
keeperZone: string;
|
keeperZone: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user