Engine Concepts
Short primer on the wire contract used by swu-simulator. Read this before the WebSocket or HTTP references.
Match access
A player session is identified by three values:
matchId— public match identifier (UUID).seat—oneortwo.seatToken— private token authorizing one seat on one match.
matches.create() and matches.join() return all three plus the first private snapshot for that seat. Treat seatToken like an auth credential — never expose it to the opponent or log it publicly.
Snapshot vs event
The socket stream sends five kinds of server messages:
welcome— confirms seat identity on the socket.snapshot— full current projected game state for that seat.event— incremental public gameplay event.error— protocol or game rejection.pong— response to a clientping.
App rule:
- Use
snapshotas the source of truth for current UI state. - Use
eventfor timelines, animations, and toast-style updates.
Hidden information
Snapshots are seat-aware.
- Your own hand, resource row, and opponent-visible state are present.
- Opponent private hidden zones are redacted on your snapshot.
- Sideboards stay hidden. Snapshots expose
sideboardCount, not sideboard card ids. - The public
GET /api/matches/{id}projection redacts private zones for both seats.
Card ids vs runtime ids
Fields named cardId, cardIds, leader, leaders, base, cards, and sideboard contain card ids from the simulator card cache. Fields like unitId, upgradeId, and abilityId are match-local runtime handles. See card ids for examples and lookup guidance.
Match format
Every snapshot includes format, the value chosen on matches.create(). Supported runtime formats are premier, eternal, sealed, draft, trilogy, and twinSuns. Non-Twin Suns formats currently require exactly two players; Twin Suns accepts 2-4 players.
See Deckbuilding & Formats for deck rules, sideboards, Limited validation, and unsupported-format handling.
Enum shapes
The Rust server uses serde's default enum representations. Two conventions show up on the wire:
ClientMessage,ServerMessage— internally tagged withtype.GameCommand,PromptState,GameEvent— externally tagged.
Concrete examples:
{ "type": "ready" }{ "type": "command", "payload": "pass" }{ "type": "command", "payload": { "chooseMulligan": { "takeMulligan": false } } }"lobbyReady"{ "chooseInitiative": { "seat": "one" } }The generated TypeScript types match these shapes exactly. The runtime validators reject anything else.
Prompt lifecycle
Every snapshot carries a prompt value. It tells the UI which player input the engine is waiting for. Common transitions:
lobbyReady → chooseInitiative → mulligan → openingResources →
action (loop) → regroupResource → action (next round) … → gameOverPair snapshot.prompt with the playActions, actionAbilities, and availableAttacks arrays on the same snapshot to render legal input.
Setup-modifying leader/base text can change opening hand size, mulligan availability, and opening resource count. First-action-phase triggers can also insert resolveAbilities before the first action prompt. See Setup & Variant Rules.
