Spire Pilotcombat model deep-dive
← How it works Live runs Encounters Training For engineers
spire pilot · combat system · as built · 2026-08-03

The combat system, as a design

This is the design document for everything the agent decides inside a fight, written for an ML engineer meeting the system cold. The short version: the network never plays a card — a fully-C++ search over a perfect reimplementation of the game engine plays the turn, with the network pricing leaf boards and shaping which branches get visits. One model per encounter, one risk-aware selection rule, and a release discipline whose core invariant is that no fight the system ever learned to win may be silently unlearned.


1 · Problem statement

What makes this hard, in ML terms

A combat turn is a sequential decision under imperfect information: your own draw order is hidden, enemy moves roll from a known finite-state machine, and the within-turn action space (card × target × potion × end-turn, over orderings) branches combinatorially. The live budget is 1,200 simulations per decision, a few seconds of wall clock. Three properties shape the design:


2 · Architecture at a glance

A serving loop and a learning loop, meeting at the model file

flowchart TB
  subgraph SERVE["Serving loop — one decision, 1200 sims, ~3–10s"]
    GS["Live game state
(CommunicationMod)"] --> SH["Shadow engine state
(C++ replica, divergence-checked)"] SH --> TOK["C++ tokenizer
entity tokens + engine playmath"] TOK --> SRCH["Determinized ISMCTS over the real engine
libtorch forward at each leaf — net value IS the leaf"] SRCH --> SEL["CVaR root selection
death gate → safe set → robust max-Q"] SEL --> ACT["One action executed;
full re-plan at the next decision"] end subgraph LEARN["Learning loop — per encounter, on the fleet"] BAT["Entry-state battery
(R2-versioned, reused, never regenerated)"] --> TCH["Teacher self-play search
sims=6000 × det=8 (5× live budget)"] TCH --> SHD["Labelled decision shards"] SHD --> TRN["Anchored per-encounter train
(full retrain or surgical pin)"] TRN --> GATE["Ratchet gate — full case suite
@ sims=1200, exact live config"] GATE --> STG["Staged candidate
never auto-deployed"] end STG -.->|"reviewed deploy + ratchet bless"| SPC["63 per-encounter specialists
one model file per encounter"] SPC -.->|"resolved at fight start"| SRCH

3 · Component design

3.1 Per-encounter specialists, and why the champion died

Live play resolves one model file per (encounter, character) — 63 deployed Ironclad specialists today — at combat start (also the hot-reload point), and a missing specialist raises, naming the encounter: no silent fallback, no character borrowing another's nets. The champion net that preceded them was retired because its worst defects — over-blocking with no damage incoming, mispricing decisive plays — had no global fix: two compensating search-side gates were built and rejected on measurement, because the death signal saturates to 1.0 on every threatened boss board, leaving a gate nothing to discriminate with; only per-encounter retraining has moved these failures.

3.2 The state is a set of entities, and the engine does the arithmetic

The board is tokenized as entities, not a sequence: player, each monster with its intent and rolled next-move distribution, each card instance per zone (Strike ≠ Strike+; copies are distinct), each relic with its live counter, potions, orbs, statuses as weighted embedding sums. Most load-bearing: playmath — modifier-applied damage, block, lethal availability — is engine-computed and attached to card tokens; a fleet once shipped for weeks with playmath gated off and the result was systematic strength-blindness. Transformers are bad at arithmetic, so they are never asked to do it. One C++ tokenizer serves live search and training alike, parity-tested at max abs diff 0.0 — the net trains on states byte-identical to the ones it sees live.

3.3 The network: a small transformer with a distributional value head

Each specialist is a 4-layer, 4-head transformer at d=128 — deliberately small, because it runs inside a 1,200-forward search budget. The heads:

3.4 The search: determinized ISMCTS with CVaR selection

The searcher explores the within-turn action graph over the real engine. Hidden information is handled by honest determinization: the unknown draw-pile remainder is reshuffled — the search never peeks at the true RNG stream — and end-turn edges open explicit chance nodes, each child a fresh re-determinization whose random-number salt depends only on (world, depth), never the path, so no line can shop for a lucky world. The net value is the leaf: rollout grounding is 5–15× slower and effectively hangs on multi-enemy boards, so it cannot run live — and everything downstream (training, gating, validation) evaluates on that configuration. Root selection is lexicographic CVaR: a death gate — P(death) read from the net's own death mass, because rollout death rates saturate flat at 1.0 exactly where a gate matters — builds a safe set at a 0.20 risk threshold, then a robust max-Q pick among well-visited children. A symbolic turn planner adds macro edges at the root — whole proven lines folded to their first action — and dominance folding prunes actions no deterministic line can justify.


4 · The data engine

A teacher five times stronger than the student's budget

Training labels come from self-play in which the same searcher runs at 6,000 simulations with 8 determinizations per decision — 5× the live budget, marginalized over worlds so no label ever encodes one seed's future. Fights start from a battery: a frozen set of entry states spanning deck-strength and entry-HP variation, versioned in object storage and pulled onto whichever training box needs it — never regenerated, so every retrain sees the same distribution and results stay comparable across the fleet. One honest caveat: the collected policy labels are one-hot in 100% of rows — no soft visit distribution — which matters for the pathology in §5.


5 · Training, and the surgical-pin method

Anchored fine-tunes; pins as decisive-state reweighting

The canonical per-encounter trainer runs collect → train → gate in rounds. The important lesson is when not to run it: a full retrain re-exposes the value function to the whole self-play distribution, and on guard-heavy encounters this measurably drifts fragile, hard-won behaviors off their optimum — proven five times on one boss. The non-perturbing alternative is the surgical pin: one fine-tune from the deployed anchor with the trunk frozen and the value and policy losses zeroed, trained only on synthesized rehearsal pins — ~60 state variants around one decision, contrasting the correct play against the observed misplay. Fragile encounters are flagged in a committed config and always re-encode pin-only.

Why pins work is now measured, and it is not the original theory. Auditing the corpus for a targeting defect: the alternative branch is present, thousands of times — "self-play never explores it" is refuted — but at the decisive board configuration the one-hot labels split ~8:1 against the correct target (675 vs 81 teacher moves), plausibly holding the policy prior wrong and starving the value head of correct-branch outcomes. A pin is therefore reweighting the decisive-state contrast, not injecting a missing branch. Pins push a region, not a point: each fix is checked against neighboring guards.


6 · Release engineering

The ratchet: won fights may never be unlearned

The unit of evaluation is a case: a frozen reproduction of a real board — relics and potions included, they are encoder inputs — with a machine-checkable statement of the correct play. The suite holds ~450 cases: guards (behaviors the deployed lineage has passed and must keep) and xfails (known-open misplays a candidate is invited to fix). Promotion is decided only by the full suite at the live budget of 1,200 simulations, net-value leaf, the exact live selection config — a cheaper 200-sim screen measurably disagrees and once would have shipped a guard regression. The accept rule is the ratchet: pass every committed ratchet case (373 today, 43 quarantined with written reasons) and fix at least one case not yet in it; breaking any ratchet case rejects regardless of net total. A passing candidate is staged, never auto-deployed. On deploy the ratchet is blessed: the fixed xfail becomes a guard, so any future retrain that drifts it is rejected at the gate — which is what makes fixes durable. Subtle search-config changes get their own gate: a repeated per-encounter OFF/ON A/B counting only consistent flips — the suite is measured deterministic, so "that flip was noise" is inadmissible.


7 · Runtime integrity

The shadow engine, and reading the tree before blaming it

Live play maintains a shadow state: the C++ engine stepped in lockstep with the real game, a divergence checker comparing predicted against observed after every action, with expected divergence classes (a relic that hides enemy intents) auto-filed so the actionable stream stays clean. The same discipline applies to post-mortems: before a live death becomes a case or a pin, the decision log is checked for the search-divergence tell — if the tree already ranked the correct play highly and the selection layer ejected it, the defect is valuation, not the policy prior, and a pin would treat the wrong organ. Observability gaps count as defects too: a missing potion field once silently invalidated two investigations.


8 · Open problems, honestly

What is measured to be wrong, and the boss-fight interlock

The interlock: the trunk-decoupled encounter heads price fight danger for the path planner, campfires, and card-reward valuation, so the models that play the fights also inform which fights get taken and with what deck. The joint exam is the act boss: deck quality is the out-of-combat system's job, play quality is this one's, and the run is decided where they compound.