// Game rooms: the server is the authority. Each room holds one GameState and // the append-only command log (the seed + log IS the game — the basis for // replays and async play). Clients get per-player redacted views and events. import { applyCommand, createGame, redactEvent, viewFor, type Command, type GameEvent, type GameState, type GameView, type PlayerId, } from "@wizwar/engine"; export interface LoggedCommand { seq: number; playerId: PlayerId; command: Command; at: string; // ISO timestamp (server-side wall clock; not used by the engine) } export interface Room { id: string; hostId: PlayerId; players: PlayerId[]; // join order seed: number; state: GameState | null; // null until started log: LoggedCommand[]; events: GameEvent[]; // full history (unredacted — redact per recipient) } const rooms = new Map(); const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; function makeRoomCode(): string { let code = ""; for (let i = 0; i < 4; i++) { code += ROOM_CODE_ALPHABET[Math.floor(Math.random() * ROOM_CODE_ALPHABET.length)]; } return rooms.has(code) ? makeRoomCode() : code; } export function createRoom(hostId: PlayerId): Room { const room: Room = { id: makeRoomCode(), hostId, players: [hostId], seed: Math.floor(Math.random() * 0xffffffff), state: null, log: [], events: [], }; rooms.set(room.id, room); return room; } export function getRoom(id: string): Room | undefined { return rooms.get(id.toUpperCase()); } export function joinRoom(room: Room, playerId: PlayerId): string | null { if (room.state) return "game already started"; if (room.players.includes(playerId)) return null; // rejoin is fine if (room.players.length >= 4) return "room is full"; room.players.push(playerId); return null; } export function startGame(room: Room): { events: GameEvent[] } | { error: string } { if (room.state) return { error: "already started" }; const n = room.players.length; if (n !== 2 && n !== 4) return { error: "supported player counts: 2 or 4" }; const { state, events } = createGame({ playerIds: room.players, seed: room.seed, sets: ["basic"], }); room.state = state; room.events.push(...events); return { events }; } export function runCommand( room: Room, playerId: PlayerId, command: Command, ): { events: GameEvent[] } | { error: string } { if (!room.state) return { error: "game not started" }; const result = applyCommand(room.state, playerId, command); if (!result.ok) return { error: result.error }; room.state = result.state; room.log.push({ seq: room.log.length, playerId, command, at: new Date().toISOString(), }); room.events.push(...result.events); return { events: result.events }; } export function viewForPlayer(room: Room, playerId: PlayerId): GameView | null { return room.state ? viewFor(room.state, playerId) : null; } export function redactFor(events: GameEvent[], playerId: PlayerId): GameEvent[] { return events.map((e) => redactEvent(e, playerId)).filter((e): e is GameEvent => e !== null); }