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:
lobbyReady -> chooseInitiative -> mulligan -> openingResources -> actionServer order:
- For game 1, pick the initiative chooser from the match seed.
- For later two-player best-of-three games, prompt the prior game's loser; after a draw, prompt the player who held initiative.
- Prompt that player to choose who starts with initiative.
- Shuffle both decks.
- Draw each player's opening hand.
- Prompt mulligans for each player that can take one. The prompt goes out to every player at once and the answers are accepted in any order.
- Prompt each player to choose opening resources, again in any order.
- 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 shape | Effect |
|---|---|
Draw N more cards in your starting hand | Adds to that player's opening and mulligan hand size. |
Draw N less card(s) in your starting hand | Subtracts from that player's opening and mulligan hand size. |
You can't take a mulligan | Rejects chooseMulligan with takeMulligan: true. |
Choose N cards as your starting resources | Overrides 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:
{
"mulligan": {
"seatOneDecided": false,
"seatTwoDecided": false
}
}Every seat that has not decided may answer, in any order. That is a deliberate deviation from CR 2.1.e / 11.2.1, which sequence the mulligan from the initiative player — see "Known CR deviation" in RULES.md. The only rejection is a second answer from a seat that already decided.
For a normal keep:
// `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 match-local card instance ids from that seat's visible hand. Commands address individual cards in play by cardInstanceId, never by the shared cardId — see card ids:
// Find this seat's private view so hidden hand cards 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.
cardInstanceIds: (activeSeatState?.hand ?? [])
.slice(0, 2)
.map(handCard => handCard.cardInstanceId),
},
})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:
- Setup completes and round 1 action phase starts.
- Snapshot prompt becomes
resolveAbilities. - Resolve the pending ability from source card
19611(Nabat Village). - Snapshot prompt becomes
chooseAbilityCardswithmin: 3andmax: 3. - Choose 3 cards from hand.
- Engine puts those cards on the bottom of the deck in chosen order.
- Normal
actionprompt appears for the initiative player.
Client pattern:
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,
// These lists are always sent. They stay empty for an ability that
// captures nothing, has a single mode, and needs no target.
captureSelections: [],
modeIndices: [],
targets: [],
},
})
}
}
const cardChoice = matchSnapshot.pendingChoice
if (
typeof matchSnapshot.prompt === 'object'
&& matchSnapshot.prompt
&& 'chooseAbilityCards' in matchSnapshot.prompt
&& cardChoice?.type === 'abilityCard'
) {
// Candidate cards are already filtered to cards this ability can choose.
matchSession.sendCommand({
chooseAbilityCards: {
abilityId: matchSnapshot.prompt.chooseAbilityCards.abilityId,
cardInstanceIds: cardChoice.candidateCards
.slice(0, cardChoice.min)
.map(candidateCard => candidateCard.cardInstanceId),
// Credits are only spent by abilities that price a choice in Credits.
creditTokensToSpend: 0,
},
})
}Render this like any other pending ability. Do not assume the first action phase always starts with an action prompt.
Format variants
premier, eternal, peasant, preview, open, sealed, and draft share the same two-player setup and round runtime after deck validation; they differ only in which cards their deck validation accepts. 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.
