Skip to content

Setup & Variant Rules

Setup is driven by the same snapshots and commands as the rest of the match. Clients should follow snapshot.prompt and avoid hardcoding Premier defaults when card text changes setup.

Setup flow

After both seats call ready(), setup advances through this prompt chain:

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

Server order:

  1. Pick the initiative chooser from the match seed.
  2. Prompt that player to choose who starts with initiative.
  3. Shuffle both decks.
  4. Draw each player's opening hand.
  5. Prompt mulligans for each player that can take one.
  6. Prompt each player to choose opening resources.
  7. Start round 1 action phase.

Use openingHandDrawn, mulliganResolved, and openingResourcesChosen events for timelines or animations. Use snapshots as current state.

Setup text

The server reads setup-modifying text from canonical leader and base cards. Current shared support covers:

Text shapeEffect
Draw N more cards in your starting handAdds to that player's opening and mulligan hand size.
Draw N less card(s) in your starting handSubtracts from that player's opening and mulligan hand size.
You can't take a mulliganRejects chooseMulligan with takeMulligan: true.
Choose N cards as your starting resourcesOverrides opening resource count when a live card uses that shape.

Examples from the canonical card-data cache:

  • Colossus (19576) draws 5 instead of the default 6 and mulligans back to 5.
  • Nabat Village (19611) draws 9, cannot mulligan, then resolves its first action phase setup trigger.

Mulligan handling

The mulligan prompt shape does not list which player is allowed to mulligan. It lists which seats have decided:

json
{
  "mulligan": {
    "seatOneDecided": false,
    "seatTwoDecided": false
  }
}

For a normal keep:

ts
// `false` is a keep decision and is valid even when mulligan is forbidden.
matchSession
.
sendCommand
({
chooseMulligan
: {
takeMulligan
: false,
}, })

If a card says that player cannot mulligan, takeMulligan: true is rejected. Send false for that seat to continue setup.

Opening resources

When the prompt becomes openingResources, choose card ids from that seat's visible hand:

ts
// Find this seat's private view so hidden hand card ids are available.
const 
activeSeatState
=
matchSnapshot
.
players
.
find
(
player
=>
player
.
seat
=== 'one')
matchSession
.
sendCommand
({
chooseOpeningResources
: {
// Example chooses the first two visible cards; real UI should use player input.
cardIds
:
activeSeatState
?.
hand
?.
slice
(0, 2) ?? [],
}, })

Default setup requires 2 resources. The engine validates the exact count for that player's setup profile. If future card text changes the count, submitting the wrong number returns invalid_selection.

After a seat chooses resources, its private snapshot shows those cards in the resource row. Opponent snapshots keep resource identities redacted according to hidden-information rules.

First action phase triggers

Some bases have text that resolves after setup but before the first action. The engine now has an explicit first-action-phase trigger window.

For Nabat Village:

  1. Setup completes and round 1 action phase starts.
  2. Snapshot prompt becomes resolveAbilities.
  3. Resolve the pending ability from source card 19611 (Nabat Village).
  4. Snapshot prompt becomes chooseAbilityCards with min: 3 and max: 3.
  5. Choose 3 cards from hand.
  6. Engine puts those cards on the bottom of the deck in chosen order.
  7. Normal action prompt appears for the initiative player.

Client pattern:

ts
if (
  typeof 
matchSnapshot
.
prompt
=== 'object'
&&
matchSnapshot
.
prompt
&& 'resolveAbilities' in
matchSnapshot
.
prompt
) { // Pending ability ids are stable handles for the command payload. const
pendingAbility
=
matchSnapshot
.
pendingAbilities
[0]
if (
pendingAbility
) {
matchSession
.
sendCommand
({
resolveAbility
: {
abilityId
:
pendingAbility
.
id
,
}, }) } } if ( typeof
matchSnapshot
.
prompt
=== 'object'
&&
matchSnapshot
.
prompt
&& 'chooseAbilityCards' in
matchSnapshot
.
prompt
) { // Candidate card ids are already filtered to cards this ability can choose. const
cardChoice
=
matchSnapshot
.
pendingAbilityCardChoice
matchSession
.
sendCommand
({
chooseAbilityCards
: {
abilityId
:
matchSnapshot
.
prompt
.
chooseAbilityCards
.
abilityId
,
cardIds
:
cardChoice
?.
candidateCardIds
.
slice
(0,
cardChoice
.
min
) ?? [],
}, }) }

Render this like any other pending ability. Do not assume the first action phase always starts with an action prompt.

Format variants

premier, eternal, sealed, and draft share the same two-player setup and round runtime after deck validation. trilogy adds pre-game deck bans and deck selection before setup starts. twinSuns uses the multiplayer runtime.

See Deckbuilding & Formats for request shapes and validation rules.

Released under the MIT License.