Skip to content

Match Lifecycle ​

A match is created over HTTP, optionally joined over HTTP, then driven through one SDK MatchSession per seat.

Create ​

ts
// Match creation validates the host deck before issuing seat credentials.
const 
premierHostDeck
= {
leader
: 46102, // Leia Organa - Someone Who Loves You
leaders
: [], // Twin Suns only; Premier uses the single `leader` above.
base
: 308, // Echo Base
cards
:
Array
.
from
({
length
: 50 }, () => 45), // Alliance X-Wing
sideboard
: [],
} const
hostAccess
= await
simulatorClient
.
matches
.
create
({
// Fixed seed keeps setup and shuffles deterministic.
seed
: '7',
format
: 'premier',
// Twin Suns supports 2-4 players; every other format is exactly 2.
playerCount
: 2,
hostDeck
:
premierHostDeck
,
// `hostName` shows on the host seat in every viewer's snapshot.
hostName
: 'Host',
hostAuthenticated
: false,
historyIdentities
: [],
aiOpponents
: [],
})

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 ​

ts
// Seat two submits its own legal deck and receives its own private token.
const 
premierGuestDeck
= {
leader
: 46107, // Darth Vader - Unstoppable
leaders
: [],
base
: 309, // Tarkintown
cards
:
Array
.
from
({
length
: 50 }, () => 35), // TIE/ln Fighter
sideboard
: [],
} const
guestAccess
= await
simulatorClient
.
matches
.
join
(
hostAccess
, {
deck
:
premierGuestDeck
,
name
: 'Guest',
authenticated
: false,
historyIdentities
: [],
})

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'.

Spectate ​

A viewer who should watch without taking a seat spectates instead of joining.

ts
// Spectating takes no seat, so it works on a match that is already full.
const 
spectatorAccess
= await
matchSession
.
spectateMatch
(
hostAccess
.
matchId
, {
name
: 'Commentator',
authenticated
: false,
})
console
.
log
(
spectatorAccess
.
spectatorToken
)

The response carries a spectatorToken instead of a seatToken, and the snapshot is the public projection — no seat's hand, deck order, or other hidden state. simulatorClient.matches.spectate() is the same call without the socket, for a viewer that only wants one point-in-time state.

A spectator connection is read-only on the socket too. Every frame that mutates the match — ready, command, formatEvent, undo, chat — is answered with read_only_spectator rather than applied, so a spectator UI should render without offering those controls.

Session ​

ts
// 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 ​

ts
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:

EventPayloadUse
connectionState'idle' | 'connecting' | 'open' | 'closed'Transport lifecycle.
welcomeWelcomePayloadSeat identity.
snapshotGameStateAuthoritative UI state.
eventGameEventTimeline, animations, notifications.
errorErrorPayloadEngine rejection or SDK decode failure.
pongPongPayloadPing latency checks.
messageServerMessageRaw typed envelope.

Wait for state ​

ts
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 ​

ts
// 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 ​

ts
matchSession
.
close
()

close() detaches socket listeners and emits connectionState: 'closed' once.

Released under the MIT License.