Skip to content

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:

ts
matchSession
.
on
('snapshot',
matchSnapshot
=> {
// Replace local UI state with the server-authoritative projection.
state
.
value
=
matchSnapshot
})

Use events for secondary UI:

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

ts
// Each seat must signal ready before setup prompts begin.
hostSession
.
ready
()
guestSession
.
ready
()

Setup prompts usually advance through:

txt
lobbyReady -> chooseInitiative -> mulligan -> openingResources -> action

Multiplayer setup uses prompt variants keyed by decided seats instead of two fixed booleans:

txt
multiplayerMulligan -> multiplayerOpeningResources -> action

Card text can change setup. Do not hardcode default hand size, mulligan availability, or opening resource count; follow the prompt and the visible snapshot.

Prompt fields identify what the engine waits for:

ts
// Narrow on this value before reading prompt-specific fields.
const 
prompt
=
matchSnapshot
.
prompt

Pair the prompt with the legal option arrays on the same snapshot:

  • playActions
  • availableAttacks
  • actionAbilities
  • leaderAction
  • pending ability choice fields

Send commands

Scalar command:

ts
// Scalar commands cover simple actions such as passing.
matchSession
.
sendCommand
('pass')

Tagged command:

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

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

ts
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

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

Released under the MIT License.