How the AI plays
Two different machines share the work. A tree search decides every action inside a fight. A small transformer, trained by reinforcement learning, decides everything between fights. Neither ever sees information a human player could not.
How the transformer was trained and promoted is on its own page. This page describes what runs at decision time, as deployed.
Two halves
The split is strict. The transformer never plays a card, and it gives the search no priors: the search starts every decision from a uniform prior over legal actions. Combat is where exact lookahead pays most, because one core plays about 59,000 random fights a second (benchmarks). Run-level choices such as which card to take or which path to walk have consequences tens of floors away, which is where a learned policy earns its keep.
A typical Ironclad run at Ascension 9 contains about 335 combat decisions (56% in common fights, 20% in elites, 24% in bosses). In September 2026 they took roughly 570,000 simulations in total; the current version plays a decision with only one legal move without a search, so it spends somewhat fewer.
Combat search
The combat agent is Monte-Carlo tree search over the Rust engine
(sts2-rs/crates/sts2-mcts). A fresh tree is built for every
decision, seeded deterministically from the run seed, so a run replays
exactly.
Actions
Each edge is one atomic action: play a card at a target, drink a potion, resolve a choice, or end the turn. There are no whole-turn macro actions. Identical cards in hand collapse to one edge, and untried actions are expanded in the order of an expert play-priority table, so the first visits go to plausible moves.
Selection
Plain UCT: each child scores Q + c·√(ln N / n), and every
child is visited once before any is visited twice. The exploration
constant depends on the budget: 0.8 below 500 simulations, 1.41 up to
999, and 2.5 from 1,000 up, which covers every deployed budget. A PUCT
variant with learned priors exists but is off in the deployed lane.
Chance nodes
Randomness is modelled, not sampled away. Any action that advances one of the combat's random streams (a random target, a random card, an enemy move roll, a shuffle, or a draw from an unknown part of the pile) becomes a chance node. Its outcomes are widened progressively: a node with v visits may hold up to ⌈C·(v+1)α⌉ distinct outcomes, and outcomes that look identical to the player are merged. A chance node backs up the plain mean of its children, expectimax-style.
End-of-turn is the widest node, because the enemy turn and the next draw both resolve there. Raising its widening constants (C = 5.79, α = 0.44) gained +6.35 pp full-run win rate on 2,000 paired runs (p = 8.2e-10). Spending the same extra compute on plain extra simulations gained nothing measurable. Every search on this page uses these constants: training, the promotion gates, the published win rates, the real-game advisor and the browser hint.
Transpositions and rollouts
A transposition table merges positions reached by different orders of play, so the search is a DAG; it merges about 12% of node creations. Each new leaf is played out by a heuristic rollout: a softmax (τ = 32) over a card-priority table ported from the STS1 agent sts_lightspeed, emptying the hand before ending the turn, for up to 200 plies.
Choosing the move
- If the solver proved a child wins, play it.
- Otherwise take the highest mean value. Among children within 0.02 of the best, the most-visited one wins.
- Two guards: ending the turn is swapped for an equally good block card when the enemy's announced damage exceeds current block, or for an equally good 0-cost play.
Budgets
| Where | Simulations | Threads |
|---|---|---|
| Published simulator win rates | 8,000 | 1 per search; parallel across seeds |
| Every promotion gate | 1,000 · 4,000 at bosses | 1 per search; parallel across seeds |
| Spire Mentor advisor in the real game | 8,000 | up to 8, root-parallel |
| Workbench hint in the browser | 2,000 | 1, in a Web Worker |
| RL training rollouts | 250 · 1,000 at bosses | 1 per search |
Root-parallel search gives each thread its own tree with a decorrelated seed and sums the root statistics. Shared-tree parallelism with virtual loss was tried, scaled no better, and broke determinism.
How long these take on the benchmark server (not a player's PC): the advisor's 8,000 simulations on an Act-1 boss take 275 ms on one thread and 39.6 ms on eight; the browser hint's 2,000 on an early fight take 39.9 ms as WASM in Chromium and 27.9 ms native. Those were measured with the old, narrower end-of-turn widening; the current one makes the hint's search about 8% slower (native, 2,000 simulations: 28.6 ms before, 30.9 ms after), and the advisor's has not been re-timed. More on the benchmarks page.
What a fight is worth
The search does not score a fight as a bare win or loss. Values are arranged in bands, so that a certain loss never outranks an uncertain line and a win is still a win however it looks:
| Outcome | Value | Ranked by |
|---|---|---|
| Won the fight | +1.0 to +1.5 | HP left, potions kept (one potion ≈ 8 HP), a small bonus for finishing early; minus a penalty if a thief escaped with gold or cards |
| Search stopped mid-fight | −1 to +1 | tanh of a linear evaluation over 216 features of the position, fitted to outcomes |
| Lost the fight | −1.45 to −1.05 | enemy HP removed (60%), own HP kept (25%), turns survived (15%) |
In short, the search maximises the chance of winning the fight first, then the resources carried out of it. Potions are ordinary actions inside the search, priced by the same potion term.
The run policy
Everything outside combat with two or more options goes to a transformer, one per character. It decides:
- the path on the map;
- card rewards (including skipping), relic rewards, boss relics and treasure;
- shop purchases and card removal;
- rest sites (rest or smith, and which card to upgrade);
- event options;
- deck changes the game asks for: remove, upgrade, transform, enchant;
- which potion to drop when the belt is full.
How it learned
Nobody wrote down which choices are good. The network was trained with reinforcement learning (PPO): it played hundreds of thousands of simulated runs, with the combat search fighting every fight, and after each batch it made the choices that had led to better-than-expected runs a little more likely and the others a little less likely. A second output, the value head, learned alongside it to predict how well a run will go from a given situation; that prediction is what decides whether an outcome counts as better or worse than expected. Each update is kept small, which is the "proximal" in Proximal Policy Optimization. The training page walks through this step by step.
What it sees
The state is a set of tokens. Each token is the sum of a section embedding, up to twelve categorical embeddings and a projection of continuous features. There is no positional encoding: apart from its section, a token's place in the sequence carries no meaning.
| Token | Carries |
|---|---|
| State (one) | decision type, act boss, HP, gold, act, floor, ascension |
| Deck card (one per card) | card identity, upgrade, enchantment |
| Relic, potion | identity |
| Map node | room type, position relative to the player, reachability, elites ahead (min/max), distance to the next rest site |
| Candidate action (one per option) | what the option is: a path step with its forward-cone features, a card or relic with its shop price, an event option with its gold, HP, relic, potion and card effects |
Vocabularies cover 580 cards, 297 relics, 63 potions, 69 events, 23 enchantments and 12 act bosses.
The network
| Architecture | pre-norm transformer: RMSNorm, multi-head attention, SwiGLU MLP (4× width) |
| Size | dmodel 192, 3 layers, 4 heads; about 2.05 M parameters |
| Policy head | one shared linear layer scores each candidate-action token; illegal slots are masked; softmax over the candidates |
| Value head | mean-pool over all tokens; predicts the training return, not a win probability |
| Fixed shapes | 320 tokens, 160 action slots. Overflow is a hard error, never a silent truncation; each promotion gate checks peak use stays well inside (the Regent gate peaked at 164 tokens and 48 slots) |
Scoring candidates with a shared head over their own tokens is what lets one network handle a three-way event, a twelve-item shop and a forty-card removal screen with the same weights.
Choosing
At play time the policy is greedy: it takes the highest-probability option. (During training it samples from the probabilities instead, so it keeps trying options it is unsure about.) Options that differ only in their target (remove which card, smith which card) first pool their probability by kind, so a well-liked action is not out-voted because its mass was split across many targets. One rule sits on top: an event choice that would kill the player outright is vetoed.
One run, in order
- Map. The policy picks the next node.
- Fight. Every action with more than one legal option is searched: 1,000 simulations, or 4,000 in a boss room.
- Rewards, shops, rest sites, events, treasure, deck changes. One forward pass each.
- Repeat to the final boss, or until the character dies.
Fallbacks are counted, never silent. If a search returns nothing usable, a fixed heuristic decides and the fallback counter records it; the release smoke test saw 1 such case in 2,527 combat decisions. In training there is no fallback class at all: every decision is the net, an automatic single option, or the combat agent.
The numbers on screen
- "est. win" in the mod, and percentages on cards in the workbench, come from the search: of the simulations that started with this move, the fraction that reached a won fight within the rollout. It is a fight-level number, measured on belief samples, and not calibrated.
- The mod's evaluation bar shows the search's mean value for the recommended move.
- The policy's
Vis the value head's estimate of the training return. It is shown as a score, never as a percentage, because it is not a win probability.
Where it runs
| Host | Search | Policy |
|---|---|---|
| Simulator and gates | native Rust | ONNX via tract, pure Rust, CPU, batch 1 |
| Browser workbench | the same Rust crate compiled to WebAssembly, in a Web Worker | the same tract runtime inside the WebAssembly module; the ~8.5 MB model is fetched once and cached for offline use |
| Spire Mentor mod | The mod itself only reads the game and draws the HUD. It starts a separate advisor process that bundles the Rust engine and the champion models, mirrors the live state into the engine and answers over a local socket. | |
The models are pinned by SHA-256 in
sts2-pycli/data/ppo/champions.toml. A missing or mismatched
champion is a load error, not a silent swap to something weaker. The Rust
encoder matches the Python one exactly, and tract matches PyTorch
to within 1e-4.