Server: room registry with 4-letter codes, host/join/start flow, the authoritative command loop (seed + append-only command log per room — the replay/async foundation), and per-player redacted views and events broadcast after every change. Client: lobby, SVG board (floors, walls, doors, homes, color-keyed treasures and wizard tokens matching the physical set's six colors, warp arrows), click-to-move, click-to-punch, card hand with tooltips from verified card text, cast flow with number card attachment and waterbolt split, edge-click targeting for wall spells, counteract-or-pass prompt, discard flow, end-turn draw selector, and a humanized event log. Verified end-to-end over real websockets with two clients: join, start, private deals, moves, turn sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.0 KiB
TypeScript
74 lines
2.0 KiB
TypeScript
// Per-player projection of GameState: everything public, plus YOUR hand.
|
|
// The server sends this after every state change; clients never see the
|
|
// deck order or other players' hands.
|
|
|
|
import { type AssembledBoard } from "./board";
|
|
import { type CardInstance } from "./cards";
|
|
import {
|
|
boardView,
|
|
type CastStack,
|
|
type GameState,
|
|
type PlayerId,
|
|
type TreasureState,
|
|
type TurnState,
|
|
} from "./game";
|
|
|
|
export interface PlayerPublicView {
|
|
id: PlayerId;
|
|
position: { x: number; y: number };
|
|
home: { x: number; y: number };
|
|
life: number;
|
|
alive: boolean;
|
|
handCount: number;
|
|
carriedTreasureId: string | null;
|
|
lostTurns: number;
|
|
extraTurns: number;
|
|
}
|
|
|
|
export interface GameView {
|
|
you: PlayerId;
|
|
phase: GameState["phase"];
|
|
winner: PlayerId | null;
|
|
turn: TurnState;
|
|
activePlayerId: PlayerId;
|
|
/** Board with dynamic wall changes already merged in. */
|
|
board: AssembledBoard;
|
|
players: PlayerPublicView[];
|
|
yourHand: CardInstance[];
|
|
treasures: TreasureState[];
|
|
deckCount: number;
|
|
discardCount: number;
|
|
/** Cards on the stack are face-up: the whole exchange is public. */
|
|
stack: CastStack | null;
|
|
pendingDiscard: PlayerId | null;
|
|
}
|
|
|
|
export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
|
const you = state.players.find((p) => p.id === playerId);
|
|
return {
|
|
you: playerId,
|
|
phase: state.phase,
|
|
winner: state.winner,
|
|
turn: state.turn,
|
|
activePlayerId: state.players[state.turn.activeIndex]!.id,
|
|
board: boardView(state),
|
|
players: state.players.map((p) => ({
|
|
id: p.id,
|
|
position: p.position,
|
|
home: p.home,
|
|
life: p.life,
|
|
alive: p.alive,
|
|
handCount: p.hand.length,
|
|
carriedTreasureId: p.carriedTreasureId,
|
|
lostTurns: p.lostTurns,
|
|
extraTurns: p.extraTurns,
|
|
})),
|
|
yourHand: you ? [...you.hand] : [],
|
|
treasures: state.treasures.map((t) => ({ ...t })),
|
|
deckCount: state.deck.length,
|
|
discardCount: state.discard.length,
|
|
stack: state.stack,
|
|
pendingDiscard: state.pendingDiscard,
|
|
};
|
|
}
|