Skip to content

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.

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

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

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

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

  • multiplayerMulligan
  • multiplayerOpeningResources
  • multiplayerRegroupResource

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.

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

ts
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' supports playerCount from 2 through 4.
  • 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:

Released under the MIT License.