Catan
Catan is a classic game of trade and strategy. It utilizes the svelte-component integration type and renders fully within the host platform via src/CatanGame.svelte.
Tutorials
Bootstrapping your first Catan node
To run Catan locally and see the game board render:
- Ensure your local Nakama container is running (the platform utilizes the
settlers_matchproxy). - Start the frontend host via
vite dev. - Navigate to
/play/catanin your browser. The platform will mountCatanGame.svelteand inject the required session variables automatically.
How-To Guides
How to configure scenarios and extensions
Extensions are loaded based on the initialConfig.scenario passed by the lobby.
If you want to force test an extension locally without a lobby:
- In
CatanGame.svelte, override theinitialConfigprop mock. - Set
initialConfig.scenario = "seafarers". - The component’s
onMountblock will synchronize this with theGameStateand re-invokegameController.initExtensions()to load the correct ruleset.
Reference
Component Injection Contract
The CatanGame.svelte component accepts standard platform properties:
hostUpdate: Callback for sending match lifecycle events (status: 'lobby' | 'playing' | 'finished').initialConfig: Prop containing game settings likevictoryPointsandscenario.platformSession/platformSocket: Nakama networking instances.
Network Opcodes
All network events transmit through nakamaManager.sendMatchData using the OpCode enum. Opcodes include BUILD, ROLL, TRADE_OFFER, TRADE_CANCEL, BANK_TRADE, BUY_DEV_CARD, and END_TURN.
Ensure you do not collide with reserved platform bus opcodes (e.g. CONFIG_UPDATE = 11, START_GAME = 12).
Explanation
The Kernel + Extension Architecture
Catan uses a data-driven Kernel + Extension pattern. The base rules run automatically, but additional scenarios (like Seafarers or Cities & Knights) are registered into the ExtensionRegistry. Hooks such as onSevenRoll or canBuild allow extensions to override or augment the base game logic seamlessly.
State Management
State is tracked through Svelte 5 Runes in GameState.svelte.ts. UI components derive values reactively from this single source of truth and only issue action requests through the GameController or RobberController.
Security & Gotchas ⚠️
During the integration and platform alignment, several critical security and reactivity hazards were addressed. Developers modifying Catan MUST ensure they do not reintroduce these vulnerabilities:
1. Network Build Validation Bypass
- The Hazard: When receiving an
OpCode.BUILDfrom the network, previously theapplyBuildFromNetworkmethod skipped positional and resource validation (validate: false). A malicious client could send spoofed packets to build roads/cities for free anywhere on the grid. - The Fix:
applyBuildFromNetworknow mandates thevalidate: trueflag. The controller strictly verifies resource deductions and adjacency constraints for all incoming network builds.
2. Turn Order Hijacking
- The Hazard: The
endTurn(playerId)function previously executed without verifying that the requestingplayerIdwas actually the currently active player. This allowed malicious users to emitOpCode.END_TURNout of turn, skipping opponents’ turns instantly. - The Fix:
endTurnnow assertsif (endingPlayerId !== game.state.turn.playerId) return;. Additionally, turn skipping is heavily guarded during the 5-6 player Special Building Phase.
3. Bleeding Globals across Matches
- The Hazard: Because
catanmounts directly into the DOM (no iframe sandbox), leaving a match and joining a new one reused the exportedgamesingleton fromGameState.svelte.ts. This caused stale properties (likewinnerorstartedflags) to bleed into fresh sessions, causing instant lobby drops. - The Fix: The
CatanGame.svelteentry point implements anonDestroyhook that manually wipes the globalgame.state(resetting arrays, winners, and flags) when the component unmounts.
4. Client-Authoritative Scoring
- The Hazard: The client used to calculate its own victory state and directly emit
{ status: 'finished', winner: 'player_id' }to the host viahostUpdate. - The Fix: The client is restricted to sending pure status events. The authoritative server evaluates actual match completion and leaderboard logic.
5. UI State Mutation Leakage
- The Hazard: UI components (e.g.,
Lobby.svelte,TurnTimer.svelte,Dice.svelte) directly mutatedgame.stateproperties or emitted rawsendMatchDatapayloads, bypassing central game controllers. This led to state desyncs and bypassed validation. - The Fix: All UI mutations are strictly routed through
GameController.tsandSetupController.ts. The UI is purely derived and reacts to the Svelte 5$statechanges.
6. Map Generation Adjacency Exploits
- The Hazard: The
generateMap()function previously used a standard shuffle for number tokens, allowing 6s and 8s to spawn adjacent to each other, breaking core game balance. - The Fix:
generateMap()now implements an explicit validation and fallback loop viahasAdjacentRedNumbersto strictly prohibit 6s and 8s from touching.
7. Infinite Bank Supply (Scarcity Bypass)
- The Hazard: The bank distributed requested resources indefinitely, ignoring the strict 19-card physical limit per resource type.
- The Fix:
GameStatetracksbankSupply: Record<Resource, number>. The production loop calculates total demand and enforces the Scarcity Rule (no one receives resources if total demand exceeds supply and multiple players are involved).