Deckbuilding & Formats
Use format on matches.create() to pick the deckbuilding rules for the match. It is required: the server never guesses a format for you.
// Use real card ids; the server canonicalizes variant prints.
const premierMainDeck = [
45, 35, 100, 105, 110, 115, 120, 125, 130,
135, 140, 145, 1228, 1230, 1232, 1243, 1244,
].flatMap(cardId => [cardId, cardId, cardId])
const premierMatchAccess = await simulatorClient.matches.create({
// Seed is optional, but useful for reproducible examples.
seed: '7',
format: 'premier',
playerCount: 2,
hostDeck: {
leader: 46102, // Leia Organa - Someone Who Loves You
leaders: [], // Twin Suns only; every other format uses `leader`.
base: 308, // Echo Base
cards: premierMainDeck,
sideboard: [1260, 1265, 1270],
},
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})The chosen format is stored on every snapshot:
console.log(premierMatchAccess.snapshot.format) // "premier"Supported values
| Wire value | Match runtime | Deck validation |
|---|---|---|
premier | Supported two-player runtime. | Constructed Premier rules. |
eternal | Supported two-player runtime. | Eternal constructed rules. |
peasant | Supported two-player runtime. | Eternal released cards, no suspensions, any Leader/Base rarity, Common/Uncommon/Special otherwise. |
preview | Supported two-player runtime. | Premier suspensions plus preview cards. |
open | Supported two-player runtime. | Every cached playable card, no suspensions. |
sealed | Supported two-player runtime. | Limited single-deck rules. |
draft | Supported two-player runtime. | Limited single-deck rules. |
trilogy | Supported two-player series runtime. | Three-deck Trilogy rules. |
twinSuns | Supported 2-4 player runtime. | Twin Suns singleton rules. |
Unsupported runtime shapes return HTTP 400 with unsupported_format. Every format except Twin Suns requires playerCount: 2; Twin Suns accepts 2 through 4.
import { EngineHttpError } from '@my-swu/simulator-client'
try {
// Non-Twin Suns formats currently run as two-player matches.
await simulatorClient.matches.create({
format: 'premier',
playerCount: 3,
hostDeck: {
leader: 46102, // Leia Organa - Someone Who Loves You
leaders: [],
base: 308, // Echo Base
cards: premierMainDeck,
sideboard: [],
},
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})
}
catch (error) {
if (error instanceof EngineHttpError) {
// The code tells the UI this is a format/runtime limit, not deck syntax.
console.log(error.status, error.code) // 400, "unsupported_format"
}
}Deck input
Every player submits a DeckInput:
type DeckInput = {
leader?: number | null
leaders: number[]
base?: number | null
cards: number[]
sideboard: number[]
}Rules:
leadermust be a leader card id.leaderscarries the Twin Suns multi-leader set. It is required on the wire, but every other format sends[]and usesleaderinstead. Sending both is rejected.basemust be a base card id.cardscontains main-deck card ids.sideboardis required on the wire. Send[]for formats that do not allow one; those formats reject a non-empty sideboard.- Main-deck and sideboard cards must be units, events, or upgrades.
- Card ids are canonicalized through the server's card-data cache before validation, so variant ids count with their canonical card identity.
See card ids for the difference between card ids and match-local runtime ids such as unitId.
If you have a my-swu JSON deck export with SET_001 card ids, convert it before match creation:
const deckJsonExport = {
leader: { id: 'SOR_001', count: 1 },
base: { id: 'SOR_025', count: 1 },
deck: [{ id: 'SOR_010', count: 50 }],
}
const importedDeck = await simulatorClient.cards.convertDeckJson(deckJsonExport)
await simulatorClient.matches.create({
format: 'premier',
playerCount: 2,
hostDeck: importedDeck,
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})The host deck, join deck, and AI opponent deck all validate against the same match format. A rejected deck fails the HTTP call before the match advances.
Premier and Eternal
Premier and Eternal validation enforce:
- Exactly one leader.
- Exactly one base.
- At least 50 main-deck cards.
- At most 10 sideboard cards.
- No more than 3 copies of a card across main deck plus sideboard.
- Main-deck and sideboard cards must be units, events, or upgrades.
Eternal additionally allows all released, non-preview cards except official Eternal suspended cards. The current suspended list rejects War Juggernaut and every canonical print of IG-2000.
Sideboard cards are hidden information. Snapshots expose only sideboardCount, not the sideboard card ids. Best-of-three Premier and Eternal matches accept sideboard format events between finished games.
Limited
sealed and draft matches use the current two-player match runtime with Limited deck validation:
// Limited decks use smaller minimums and have no sideboard.
const sealedMatchAccess = await simulatorClient.matches.create({
format: 'sealed',
playerCount: 2,
hostDeck: {
leader: 46102, // Leia Organa - Someone Who Loves You
leaders: [],
base: 308, // Echo Base
cards: Array.from({ length: 30 }, () => 45), // Alliance X-Wing
sideboard: [], // Limited decks reject a non-empty sideboard.
},
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})Limited validation enforces:
- Exactly one leader.
- Exactly one base.
- At least 30 main-deck cards.
- No sideboard.
- Main-deck cards must be units, events, or upgrades.
- No copy limit.
- With Limited provenance, the leader and main deck must come from the pool; the base may come from the pool or
commonBases.
For generated pools and draft pods, use Limited Events instead of creating a multiplayer match:
// Generate a draft pod first, then use its completed pool as provenance.
const draftEventAccess = await simulatorClient.limitedEvents.create({
kind: 'draft',
setCode: 'SOR',
playerCount: 4,
// Per-round booster sets for chaos draft; a plain draft leaves this empty.
draftRounds: [],
// Enables the per-pick countdown that auto-picks on timeout.
pickTimerEnabled: false,
seed: '7',
})
const draftSeatAccess = await simulatorClient.limitedEvents.join(draftEventAccess, {})
await simulatorClient.limitedEvents.applyCommand(draftSeatAccess, {
type: 'pickLeader',
// Leader choices come from the private viewer snapshot for this seat.
cardId: draftSeatAccess.snapshot.viewer!.leaderChoices[0]!,
})Limited Events support:
- Sealed events for 1-12 players, with 6 standard boosters per player.
- Draft events for 4-12 players, with 3 standard boosters per player.
- Standard booster shape: 1 leader, 1 base slot, 9 commons, 3 uncommons, 1 rare-or-legendary slot, and 1 wildcard/foil slot.
- Draft flow: leaders pass right twice, then non-leader packs pass left, right, and left.
- Hidden picks: active pack drafts show only the current pack to the viewer; drafted cards are visible during review gates and after completion.
- Common bases:
deckSource.commonBaseslists every common base from the selected set, available to every player in Draft and Sealed deckbuilding.
Completed Limited Events expose each player's leaders, pool, packs, and common bases through the viewer deckSource. Clients can use that source as provenance when submitting normal two-player draft or sealed match decks.
Trilogy
trilogy uses a three-deck set per player:
- Exactly 3 decks in
formatSetup. - No sideboards.
- Unique leaders and bases across the set.
- At least 50 main-deck cards per deck, plus or minus any leader or base card text that changes that deck's minimum deck size.
- No more than 3 combined main-deck copies of any card across the set, unless the card's own text allows more copies.
- Same Eternal card pool and suspension rules.
Before game 1, each seat bans one opponent deck, then selects one of their own available decks. Deck choices stay hidden until both seats have selected. In best-of-three play, a deck that wins a game cannot be selected again.
Twin Suns
twinSuns is the supported multiplayer match format. It accepts playerCount from 2 through 4 and uses leaders instead of leader:
const twinSunsMainDeck = [
15, 20, 25, 30, 35, 40, 45, 50, 70, 75, 80, 85, 90, 100, 105, 110,
115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180,
185, 190, 210, 215, 220, 240, 245, 250, 255, 260, 265, 267, 268, 269,
270, 272, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324,
325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
339, 340, 341, 342, 343, 344, 345, 346,
]
const twinSunsMatchAccess = await simulatorClient.matches.create({
format: 'twinSuns',
playerCount: 4,
hostDeck: {
leaders: [200, 46107], // Grand Moff Tarkin; Darth Vader
base: 308, // Echo Base
cards: twinSunsMainDeck,
sideboard: [], // Twin Suns decks have no sideboard.
},
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})Twin Suns validation enforces:
- Exactly two leaders.
- The two leaders' starting faceup sides cannot carry both the Heroism and the Villainy aspect between them. Leaders with neither aspect pair with either half, and a double-sided leader counts only the side it starts the game on.
- Exactly one base.
- At least 80 main-deck cards, plus or minus any leader or base card text that changes the minimum deck size.
- No sideboard.
- Main-deck cards must be units, events, or upgrades.
- No more than one copy of any canonical card identity across leaders, base, and main deck, unless a card's own text allows more copies.
See Multiplayer for joining every seat and driving Twin Suns counters.
Joining and AI
Seat two must join with a deck legal for the host match format:
// Join decks validate against the host match format.
await simulatorClient.matches.join(premierMatchAccess, {
deck: {
leader: 46107, // Darth Vader - Unstoppable
leaders: [],
base: 309, // Tarkintown
cards: premierMainDeck,
sideboard: [],
},
name: 'Guest',
authenticated: false,
historyIdentities: [],
})AI opponents use the same rule:
await simulatorClient.matches.create({
format: 'draft',
playerCount: 2,
// AI deck follows the same limited deck rules as the human host deck.
hostDeck: {
leader: 46102, // Leia Organa - Someone Who Loves You
leaders: [],
base: 308, // Echo Base
cards: Array.from({ length: 30 }, () => 45), // Alliance X-Wing
sideboard: [],
},
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [{
seat: 'two',
difficulty: 'easy',
trilogyFormatMode: 'auto',
deck: {
leader: 46107, // Darth Vader - Unstoppable
leaders: [],
base: 309, // Tarkintown
cards: Array.from({ length: 30 }, () => 35), // TIE/ln Fighter
sideboard: [],
},
}],
})Debug checklist
When deck validation fails:
- Check
EngineHttpError.code. - For
invalid_selection, readEngineHttpError.message; it names the rule that failed. - Confirm the requested
format. - Confirm
sideboardis[]forsealed,draft,trilogy, andtwinSuns. - Confirm Premier and Eternal copy counts include both
cardsandsideboard. - Confirm Eternal and Trilogy decks do not use unreleased, preview, or suspended cards.
- Confirm Trilogy setup has exactly 3 decks with no sideboards.
- Confirm every card id is from the card cache, not local invented fixture data.
