Driving Gameplay
The server owns rules and legal actions. Client UIs should render from snapshot, then submit one GameCommand that matches the current prompt.
Source of truth
Use snapshots as authoritative state:
matchSession.on('snapshot', matchSnapshot => {
// Replace local UI state with the server-authoritative projection.
state.value = matchSnapshot
})Use events for secondary UI:
matchSession.on('event', gameEvent => {
// Events are good for logs and animations, not long-lived state.
timeline.value.push(gameEvent)
})Events are useful for logs and animation, but the snapshot is the state to render after every transition.
Ready and setup
After both seats are connected:
// Each seat must signal ready before setup prompts begin.
hostSession.ready()
guestSession.ready()Setup prompts usually advance through:
lobbyReady -> chooseInitiative -> mulligan -> openingResources -> actionMultiplayer setup uses prompt variants keyed by decided seats instead of two fixed booleans:
multiplayerMulligan -> multiplayerOpeningResources -> actionCard text can change setup. Do not hardcode default hand size, mulligan availability, or opening resource count; follow the prompt and the visible snapshot.
Render legal input
Prompt fields identify what the engine waits for:
// Narrow on this value before reading prompt-specific fields.
const prompt = matchSnapshot.promptPair the prompt with the legal option arrays on the same snapshot:
playActionsavailableAttacksactionAbilitiesleaderAction- pending ability choice fields
Send commands
Scalar command:
// Scalar commands cover simple actions such as passing.
matchSession.sendCommand('pass')Tagged command:
// Tagged commands carry prompt-specific payloads.
matchSession.sendCommand({
chooseMulligan: {
takeMulligan: false,
},
})The SDK validates outbound command envelopes before sending. Malformed commands throw SdkValidationError synchronously.
Out-of-game commands
Format workflows use sendFormatEvent() over the socket or matches.applyFormatEvent() over HTTP:
// Socket form updates the connected seat.
matchSession.sendFormatEvent({ type: 'readyNextGame' })
// HTTP form is useful for tools that already hold match access.
await simulatorClient.matches.applyFormatEvent(matchAccess, {
type: 'sideboard',
deck: sideboardDeck,
})Use format events for sideboarding, best-of-series readiness, Trilogy bans and deck selection, and modeled Draft picks.
Twin Suns counters
Twin Suns replaces takeInitiative with the shared counter action. Read availableCounters, then submit takeAvailableCounter for one available counter:
matchSession.sendCommand({
takeAvailableCounter: {
counter: 'initiative',
},
})If the counter is plan, follow the next choosePlanCard prompt with a card from that seat's private hand.
Sync and ping
// Sync asks the server to resend the current snapshot.
matchSession.sync()
// Ping echoes the nonce in a later pong event for latency tracking.
matchSession.ping('host-latency-1')sync() requests a fresh snapshot. ping() receives a pong event with the same nonce, which lets apps measure round-trip latency.
