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
@@ -13,6 +13,8 @@
|
||||
|
||||
let name = $state(playerName());
|
||||
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 error = $state('');
|
||||
let seats = $state(allSeats());
|
||||
@@ -157,9 +159,21 @@
|
||||
<span>Your name</span>
|
||||
<input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="e.g. Morwenna" autocomplete="nickname" />
|
||||
</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">
|
||||
<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}>
|
||||
<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" />
|
||||
@@ -551,4 +565,17 @@
|
||||
.family li + li {
|
||||
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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// withholds the round in progress, and a rules revision. Replace this file
|
||||
// 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 TARGET = 3;
|
||||
@@ -41,7 +41,7 @@ function random(seed: number): number {
|
||||
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);
|
||||
return {
|
||||
rules,
|
||||
|
||||
@@ -21,9 +21,23 @@ export interface Outcome {
|
||||
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> {
|
||||
/** Seats in table order; the table takes at most this many. */
|
||||
seatIds: SeatId[];
|
||||
/** What the host may choose when opening a table; empty for a game with one way to play. */
|
||||
options?: TableOption[];
|
||||
minSeats: number;
|
||||
/** Names the server draws for bots, in preference order. */
|
||||
botNames: string[];
|
||||
@@ -35,7 +49,7 @@ export interface GameSpec<State, Input> {
|
||||
*/
|
||||
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(state: State, inputs: Record<SeatId, Input>): State;
|
||||
/** 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
|
||||
// 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';
|
||||
|
||||
export interface HeldSeat {
|
||||
@@ -132,7 +132,7 @@ export function makeApi<State, Input>() {
|
||||
type Seated = { seat: SeatId; token: string; view: RoomView<State> };
|
||||
type View = RoomView<State>;
|
||||
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 }),
|
||||
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 }),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// through GameSpec; a game's own store wraps or extends this one.
|
||||
|
||||
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 { makeApi, rememberSeat, watchRoom, type HeldSeat } from './client';
|
||||
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. */
|
||||
static async create(name: string, size: number): Promise<Room> {
|
||||
const seated = await api.create(name, size);
|
||||
static async create(name: string, size: number, options?: TableOptions): Promise<Room> {
|
||||
const seated = await api.create(name, size, options);
|
||||
rememberSeat(seated.view.roomId, { seat: seated.seat, token: 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. */
|
||||
static async createWithBot(name: string): Promise<Room> {
|
||||
const seated = await api.create(name, 2);
|
||||
static async createWithBot(name: string, options?: TableOptions): Promise<Room> {
|
||||
const seated = await api.create(name, 2, options);
|
||||
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
|
||||
await api.addBot(seated.view.roomId, 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
|
||||
// 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`). */
|
||||
export interface ChatLine {
|
||||
@@ -40,6 +40,8 @@ export interface RoomView<State> {
|
||||
rematch: { to: string; by: SeatId } | null;
|
||||
/** The table this one is the rematch of. */
|
||||
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. */
|
||||
keeper: string;
|
||||
keeperZone: string;
|
||||
|
||||
Reference in New Issue
Block a user