Multiplayer
Multiplayer matches use the same matches resource as two-player matches. Create the match with a configured playerCount, join each remaining seat, then open one socket per seat token.
Current multiplayer runtime support is Twin Suns for 2-4 players. Premier, Eternal, Sealed, Draft, and Trilogy still run with exactly 2 players.
Create a Twin Suns match
Twin Suns deck input uses leaders instead of leader. Each deck needs exactly two leaders, one base, at least 80 main-deck cards, no sideboard, and no duplicate canonical card identities.
The numeric values below are card ids. See card ids for the example card names.
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 twinSunsPlayerDeck = {
leaders: [46102, 46107], // Leia Organa; Darth Vader
base: 308, // Echo Base
cards: twinSunsMainDeck,
}
const twinSunsHostAccess = await simulatorClient.matches.create({
seed: 7,
format: 'twinSuns',
playerCount: 4,
hostDeck: twinSunsPlayerDeck,
})The creator receives seat 'one'. Later joins fill the next open seat in clockwise order: 'two', 'three', then 'four'.
const twinSunsSeatAccesses = [twinSunsHostAccess]
while (twinSunsSeatAccesses.length < twinSunsHostAccess.snapshot.playerCount) {
twinSunsSeatAccesses.push(
await simulatorClient.matches.join(twinSunsHostAccess, {
deck: twinSunsPlayerDeck,
}),
)
}Each access object has a different seatToken. Keep tokens private to the browser tab, worker, or server process that controls that seat.
Connect each seat
Open one MatchSession per player seat:
const twinSunsSessions = twinSunsSeatAccesses.map(twinSunsAccess => (
simulatorClient.session.create(twinSunsAccess)
))
await Promise.all(
twinSunsSessions.map(twinSunsSession => twinSunsSession.connect()),
)
for (const twinSunsSession of twinSunsSessions) {
twinSunsSession.ready()
}matches.get(matchId) returns the public projection. Use a connected seat socket when you need hand, resource, prompt, or private choice data for that player.
Render multiplayer snapshots
Snapshots expose the configured seat count and the seat-ordered player list:
matchSession.on('snapshot', matchSnapshot => {
console.log(matchSnapshot.playerCount)
console.log(matchSnapshot.players.map(playerState => playerState.seat))
})Setup and regroup prompts use multiplayer-specific prompt variants when more than two seats are configured:
multiplayerMulliganmultiplayerOpeningResourcesmultiplayerRegroupResource
These prompts contain decidedSeats. Render controls for the connected viewer only when that seat has not decided yet.
Twin Suns counters
Twin Suns uses shared counters during the action phase. The snapshot exposes availableCounters and twinSunsCounters so the active player can choose an available counter.
matchSession.sendCommand({
takeAvailableCounter: {
counter: 'blast',
},
})If the player takes the plan counter, the next prompt is choosePlanCard for that same seat. Submit one card from that seat's private hand:
matchSession.sendCommand({
choosePlanCard: {
cardId: 45, // Alliance X-Wing from this seat's hand
},
})In Twin Suns, takeInitiative is not valid. A player can pass only after that player has taken a counter for the round, or when no counters remain.
Support boundaries
format: 'twinSuns'supportsplayerCountfrom2through4.- Other formats support exactly
playerCount: 2. opponent: { kind: 'ai' }supports exactly 2 players and always fills seat Two.- Joining a full match returns the normal match-full error.
- Reconnect uses the same per-seat token flow as two-player matches.
Read next:
