Skip to content

Playing Against an AI

The simulator can auto-fill seat Two with an AI opponent at match creation time. You ship a deck for the 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 seat One.

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

Add an opponent block to matches.create(). Seat Two is filled automatically — you do not call matches.join().

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',
hostDeck
: {
leader
: 46102, // Leia Organa - Someone Who Loves You
base
: 308, // Echo Base
cards
:
premierMainDeck
,
},
opponent
: {
kind
: 'ai',
// Preset names map to server-side AI policies.
difficulty
: 'medium',
deck
: {
leader
: 46107, // Darth Vader - Unstoppable
base
: 309, // Tarkintown
cards
:
premierMainDeck
,
}, }, })

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().

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

Deck requirements

  • opponent.deck is required when opponent is present. 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

  • Seat Two only. The AI always plays seat Two; seat One is the human host. Symmetric AI-vs-AI (both seats) is not wired through the HTTP API. Use the native Rust batch runner described below for AI-vs-AI runs.
  • Two-player matches only. AI setup rejects playerCount values other than 2; use human joins for Twin Suns multiplayer.
  • One AI per match. There is no mid-match handover between AI and human seats.
  • 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.