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
: [200, 46107], // Grand Moff Tarkin; Darth Vader
base
: 308, // Echo Base
cards
:
twinSunsMainDeck
,
sideboard
: [], // Required on the wire; Twin Suns has no sideboard.
} const
twinSunsHostAccess
= await
simulatorClient
.
matches
.
create
({
seed
: '7',
format
: 'twinSuns',
playerCount
: 4,
hostDeck
:
twinSunsPlayerDeck
,
hostName
: 'Host',
hostAuthenticated
: false,
historyIdentities
: [],
aiOpponents
: [],
})

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
,
name
: `Seat ${
twinSunsSeatAccesses
.
length
+ 1}`,
authenticated
: false,
historyIdentities
: [],
}), ) }

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 by its match-local instance id, not its card id — see card ids:

ts
matchSession
.
sendCommand
({
choosePlanCard
: {
// `cardInstanceId` comes from the seat's private `hand` in the snapshot.
cardInstanceId
: 12,
}, })

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.