Match Lifecycle
A match is created over HTTP, optionally joined over HTTP, then driven through one SDK MatchSession per seat.
Create
// Match creation validates the host deck before issuing seat credentials.
const premierHostDeck = {
leader: 46102, // Leia Organa - Someone Who Loves You
base: 308, // Echo Base
cards: Array.from({ length: 50 }, () => 45), // Alliance X-Wing
}
const hostAccess = await simulatorClient.matches.create({
// Fixed seed keeps setup and shuffles deterministic.
seed: 7,
format: 'premier',
hostDeck: premierHostDeck,
})The response includes:
matchId: public id for routing.seat: assigned seat.seatToken: private credential for this seat.snapshot: first seat-aware state projection.
Treat seatToken like an auth credential. Do not log it publicly or send it to the opponent.
Join
// Seat two submits its own legal deck and receives its own private token.
const premierGuestDeck = {
leader: 46107, // Darth Vader - Unstoppable
base: 309, // Tarkintown
cards: Array.from({ length: 50 }, () => 35), // TIE/ln Fighter
}
const guestAccess = await simulatorClient.matches.join(hostAccess, {
deck: premierGuestDeck,
})Seat two receives its own token and private snapshot. Deck validation happens before the seat joins.
Twin Suns matches with playerCount above 2 keep using join() until every seat has private access. The creator is seat 'one'; joins fill 'two', 'three', then 'four'.
Session
// Use private access here because the socket must authenticate one seat.
const matchSession = simulatorClient.session.create(hostAccess, {
handshakeTimeoutMs: 10_000,
})
await matchSession.connect()connect() resolves only after the socket opens, the server sends welcome, and the first snapshot arrives. The SDK encodes the seat token in the socket URL and validates every inbound server frame.
Create one session for each player seat in multiplayer games. A seat token authenticates exactly one player projection; do not reuse one token for a different player UI.
Subscribe
const stopSnapshot = matchSession.on('snapshot', matchSnapshot => {
// Always render from the latest authoritative snapshot.
render(matchSnapshot)
})
const stopError = matchSession.on('error', errorPayload => {
// Engine rejections and SDK decode failures both arrive here.
report(errorPayload.code, errorPayload.message)
})Events:
| Event | Payload | Use |
|---|---|---|
connectionState | 'idle' | 'connecting' | 'open' | 'closed' | Transport lifecycle. |
welcome | WelcomePayload | Seat identity. |
snapshot | GameState | Authoritative UI state. |
event | GameEvent | Timeline, animations, notifications. |
error | ErrorPayload | Engine rejection or SDK decode failure. |
pong | PongPayload | Ping latency checks. |
message | ServerMessage | Raw typed envelope. |
Wait for state
await matchSession.waitForSnapshot(
// Resolve when the match has entered normal action-phase play.
matchSnapshot => matchSnapshot.phase === 'action',
{ timeoutMs: 15_000 },
)If the current snapshot already matches, the promise resolves immediately.
Reconnect
// Reuses the original matchId and seatToken, then requests a fresh snapshot.
await matchSession.reconnect({ handshakeTimeoutMs: 5_000 })Reconnect closes the current socket, opens a new one with the same credentials, waits for the normal handshake, then sends syncRequest. Registered listeners stay attached. If the handshake fails, connectionState becomes closed.
Close
matchSession.close()close() detaches socket listeners and emits connectionState: 'closed' once.
