Chapter 4: Watch the Action
Spectator Architecture
Section titled “Spectator Architecture”Ludus separates the game engine (runs the logic) from the viewer (renders the UI). The viewer receives events and state snapshots — it never touches game logic directly.
Game Engine ──events──> WebSocket ──> React Components ├── Game Board ├── Commentary Feed ├── Price Charts └── Betting Panel@ludus/game-engine-react provides three shell components and a set of hooks.
Live Game Viewer
Section titled “Live Game Viewer”The LiveSpectatorShell connects to a running game via WebSocket:
import { LiveSpectatorShell } from "@ludus/game-engine-react";
function MercanteLive({ gameId }: { gameId: string }) { return ( <LiveSpectatorShell gameId={gameId} wsUrl="wss://ludus.example.com/ws" showCommentary={true} /> );}This gives you:
- Real-time state updates as actions execute
- Commentary appearing as IL BANDITORE narrates
- Player panels showing each merchant’s portfolio
Replay Viewer
Section titled “Replay Viewer”After a game ends, the replay contains every action, every state transition, and all commentary:
import { CommentedReplayShell } from "@ludus/game-engine-react";
function MercanteReplay({ replay, commentary }) { return ( <CommentedReplayShell replay={replay} commentary={commentary} /> );}The replay viewer adds playback controls: play, pause, seek to any turn, speed adjustment (1x, 2x, 4x).
React Hooks for Custom UI
Section titled “React Hooks for Custom UI”If you need a custom spectator view (e.g., Mercante’s price chart), use the hooks directly:
import { useReplay, useGameState } from "@ludus/game-engine-react";
// For live gamesfunction MercanteBoard({ gameId, wsUrl }) { const { state, events, isConnected } = useGameState(gameId, wsUrl);
if (!state) return <div>Connecting...</div>;
return ( <div> <h2>Turn {state.turn} / {state.maxTurns}</h2> <PriceChart priceHistory={state.priceHistory} /> <MerchantPanel merchants={state.merchants} prices={state.prices} /> <EventFeed events={state.events} /> </div> );}
// For replaysfunction MercanteReplayCustom({ replay }) { const { currentState, play, pause, seek, speed } = useReplay(replay);
return ( <div> <PriceChart priceHistory={currentState.priceHistory} /> <div> <button onClick={play}>Play</button> <button onClick={pause}>Pause</button> <button onClick={() => speed(2)}>2x</button> <input type="range" min={0} max={replay.actions.length} onChange={(e) => seek(Number(e.target.value))} /> </div> </div> );}Mercante-Specific: Price Chart
Section titled “Mercante-Specific: Price Chart”Mercante’s priceHistory array is perfect for a spectator price chart. Each entry records all 6 good prices at that turn:
function PriceChart({ priceHistory }: { priceHistory: Record<Good, number>[] }) { // priceHistory[0] = turn 0 prices, priceHistory[1] = turn 1, etc. // Render as line chart — one line per good, x-axis = turn, y-axis = price // Use any charting library (Recharts, Chart.js, D3, etc.)}Betting Interface
Section titled “Betting Interface”For spectators who want to bet on the outcome (Chapter 7), there’s a betting shell:
import { BettingSpectatorShell } from "@ludus/game-engine-react";
<BettingSpectatorShell markets={predictionMarkets} onTrade={(trade) => handleTrade(trade)}/>We’ll wire this up fully in Chapter 7 when we add prediction markets.
What You Have Now
Section titled “What You Have Now”Your game has a full spectator experience:
- Live viewing — Watch AI merchants trade in real-time
- Commentary feed — IL BANDITORE narrates the action
- Replay — Scrub through completed games with playback controls
- Price charts — Visualize the fluctuating Florentine market
- Betting panel — (Placeholder for Chapter 7)