Duels between people: rooms, the hall, and a games ledger
The store plays any seat, locally against the bot or remotely through the server. A room has a four-letter code that is also its invite link; the hall opens or joins one and lists the seats this browser holds with whose move it is. The ledger and summary show every seat, threats name their wizard, and the board is one component shared by the hall and the room page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8366c9d7ff
commit
e1bda87987
+17
-18
@@ -8,7 +8,7 @@ import { chooseBotTurn } from '../../src/lib/game/bot';
|
||||
import { resolveTurn } from '../../src/lib/game/resolve';
|
||||
import { GESTURES, type Gesture } from '../../src/lib/game/spells';
|
||||
import { createGame, inDuel, type GameState, type TurnInput, type WizardId } from '../../src/lib/game/state';
|
||||
import { viewFor } from '../../src/lib/game/view';
|
||||
import { viewFor, type RoomView } from '../../src/lib/game/view';
|
||||
import type { LedgerLine, Store } from './store';
|
||||
|
||||
const SEAT_IDS: WizardId[] = ['A', 'B', 'C', 'D'];
|
||||
@@ -35,18 +35,6 @@ export interface Room {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** What a seated player is told. Other seats' pending inputs and tokens never leave the server. */
|
||||
export interface View {
|
||||
roomId: string;
|
||||
seq: number;
|
||||
size: number;
|
||||
me: WizardId;
|
||||
seats: { id: WizardId; name: string; bot: boolean }[];
|
||||
/** Seats that still have to move before the turn resolves. */
|
||||
waitingOn: WizardId[];
|
||||
state: GameState | null;
|
||||
}
|
||||
|
||||
export class RoomError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -60,6 +48,17 @@ function newId(bytes: number): string {
|
||||
return randomBytes(bytes).toString('base64url');
|
||||
}
|
||||
|
||||
/** Room codes: four letters or digits, none that read alike, easy to say aloud. */
|
||||
const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
function newCode(): string {
|
||||
const bytes = randomBytes(4);
|
||||
return [...bytes].map((b) => CODE_ALPHABET[b % CODE_ALPHABET.length]).join('');
|
||||
}
|
||||
|
||||
export function normalizeCode(raw: string): string {
|
||||
return raw.trim().toUpperCase();
|
||||
}
|
||||
|
||||
export class Rooms {
|
||||
private rooms = new Map<string, Room>();
|
||||
private listeners = new Map<string, Set<(room: Room) => void>>();
|
||||
@@ -72,8 +71,8 @@ export class Rooms {
|
||||
}
|
||||
|
||||
get(id: string): Room {
|
||||
const room = this.rooms.get(id);
|
||||
if (!room) throw new RoomError('No such duel.', 404);
|
||||
const room = this.rooms.get(id) ?? this.rooms.get(normalizeCode(id));
|
||||
if (!room) throw new RoomError('No duel answers to that code.', 404);
|
||||
return room;
|
||||
}
|
||||
|
||||
@@ -86,8 +85,8 @@ export class Rooms {
|
||||
|
||||
create(name: string, size: number): { room: Room; seat: Seat } {
|
||||
if (!Number.isInteger(size) || size < 2 || size > MAX_SEATS) throw new RoomError(`A duel seats 2 to ${MAX_SEATS} wizards.`);
|
||||
let id = newId(6);
|
||||
while (this.rooms.has(id)) id = newId(6);
|
||||
let id = newCode();
|
||||
while (this.rooms.has(id)) id = newCode();
|
||||
const room: Room = { id, size, createdAt: Date.now(), seats: [], state: null, pending: {}, seq: 0, updatedAt: Date.now() };
|
||||
this.rooms.set(id, room);
|
||||
this.commit(room, { t: 'room', id, seats: size, createdAt: room.createdAt });
|
||||
@@ -144,7 +143,7 @@ export class Rooms {
|
||||
}
|
||||
}
|
||||
|
||||
view(room: Room, seatId: WizardId): View {
|
||||
view(room: Room, seatId: WizardId): RoomView {
|
||||
return {
|
||||
roomId: room.id,
|
||||
seq: room.seq,
|
||||
|
||||
Reference in New Issue
Block a user