Skip to content

Playing Against an AI ​

The simulator can auto-fill non-host seats with AI opponents at match creation time. You ship a deck for each AI, pick a difficulty preset, and the server takes care of joining, readying, and driving the AI's decisions — your client code only needs to manage the host seat.

This page assumes you already know the basics from Getting Started and the End-to-End Tutorial.

When to use this ​

  • Tutorials, bots, and "vs. computer" modes where a second human is not available.
  • Deterministic regression fixtures (pick a seed + difficulty: 'easy' and the match is fully reproducible).
  • Smoke tests against the live engine that do not require orchestrating two seats in the test harness.

Requesting an AI opponent ​

List the AI seats in aiOpponents on matches.create(). Every listed seat is filled automatically — you do not call matches.join() for it.

ts
import { 
createSimulatorClient
} from '@my-swu/simulator-client'
const
simulatorClient
=
createSimulatorClient
({
baseUrl
: 'http://127.0.0.1:4000' })
// Use a real, format-legal deck; the server validates host and AI decks. 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
hostAccess
= await
simulatorClient
.
matches
.
create
({
// Seed fixes shuffle order and AI choices for reproducible tests.
seed
: '42',
format
: 'premier',
playerCount
: 2,
hostDeck
: {
leader
: 46102, // Leia Organa - Someone Who Loves You
leaders
: [], // Twin Suns only; Premier uses the single `leader` above.
base
: 308, // Echo Base
cards
:
premierMainDeck
,
sideboard
: [],
},
hostName
: 'Host',
hostAuthenticated
: false,
historyIdentities
: [],
aiOpponents
: [{
// Seat this AI controls. Two-player matches always use seat Two.
seat
: 'two',
// Preset names map to server-side AI policies.
difficulty
: 'medium',
// Required: how this AI answers Trilogy bans and deck picks. See below.
trilogyFormatMode
: 'auto',
deck
: {
leader
: 46107, // Darth Vader - Unstoppable
leaders
: [],
base
: 309, // Tarkintown
cards
:
premierMainDeck
,
sideboard
: [],
}, }], })

The response is the same MatchAccessResponse you get for a human-vs- human match — hostAccess.seat is 'one', hostAccess.seatToken is the host token, hostAccess.snapshot is the host-private initial snapshot. Seat Two's token stays server-side; clients never need it.

From here the rest of the flow is identical to the tutorial:

ts
const 
matchSession
=
simulatorClient
.
session
.
create
(
hostAccess
)
await
matchSession
.
connect
()
// Human seat still needs to signal ready; the AI seat is already handled.
matchSession
.
ready
()

The AI seat is already ready by the time connect() returns, so the match advances to the opening phase as soon as you call ready().

Undo behavior ​

When seat One calls undo(), the server rolls back through the human's latest undo checkpoint. If the AI answered after that action, the same undo request also consumes the AI response checkpoints and restores the human prompt. The server temporarily pauses the AI after the rollback and resumes that undo-created pause after the next successful human ready, game command, or format event.

Use setAiPaused(true) only for manual AI control. Manual pauses stay paused until the client explicitly sends setAiPaused(false).

Difficulty presets ​

PresetDecision policy
'easy'Uniform random over the legal move set. Fast, weak.
'medium'Shallow MCTS search. Modest compute per decision.
'hard'Deeper MCTS with a wall-clock budget.
'unfair'Longest search budget per decision. Strongest preset.

Internal knobs (iteration counts, time budgets) are intentionally hidden behind these presets so they can be tuned server-side without an SDK rebuild.

Medium, Hard and Unfair share bounded choice generation, including optional abilities, Exploit payments and multi-target selections. Difficulty changes search effort and policy. Medium and Hard sample unknown draw order; Unfair sees hidden cards and actual deck order. Twin Suns seats evaluate their own outcome, including shared victories. See the coverage audit for tested contracts and evaluation limits.

Trilogy format mode ​

trilogyFormatMode is required on every entry in aiOpponents. It decides how that AI answers the out-of-game Trilogy ban and deck-selection prompts, which run before and between Trilogy games:

ModeBehavior
'auto'The AI picks with the matchup-probe chooser.
'random'Deterministic legal random choices derived from match state.
'manual'The human opponent submits the AI's ban and select choices.

The field has no effect outside Trilogy, but it is not optional: an AI seat without a declared policy is skipped by the Trilogy driver, so the match would stall waiting on choices nobody makes.

Deck requirements ​

  • deck is required on every aiOpponents entry. The server does not ship default AI decks.
  • The AI deck is validated with the same format rules as the host deck. An invalid AI deck fails the create call with a 400 rather than a silent background error. See Deckbuilding & Formats.

Scope and limitations ​

  • Non-host seats only. Seat One is the human host. In a two-player match the AI plays seat Two; a Twin Suns match may list every non-host seat in aiOpponents. Symmetric AI-vs-AI (including the host seat) is not wired through the HTTP API — use the native Rust batch runner described below.
  • No mid-match handover. A seat is AI-controlled or human for the whole match.
  • In-memory sessions. AI matches live in the same in-memory registry as human matches and are lost on server restart.

Offline batch runs ​

For headless AI-vs-AI benchmarking, the repository ships a Rust binary that runs matches without the HTTP/WS layer:

bash
cargo run --bin ai_match -- --p1 random --p2 mcts:1000

See the AI module source for the full CLI flags.

Released under the MIT License.