Wire online multiplayer: game rooms, protocol, playable Svelte client
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c607ad5c7
commit
36b3ffe9a6
@@ -8,3 +8,4 @@ export * from "./board";
|
||||
export * from "./cards";
|
||||
export * from "./setups";
|
||||
export * from "./game";
|
||||
export * from "./view";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user