Spire Pilotout-of-combat deep-dive
← How it works Live runs Training Combat model For engineers
spire pilot · out-of-combat architecture · design 2026-07-31

How the model plays the rest of the run

Combat is only part of a Slay the Spire run. Everything else — which path to walk, which card to draft, what to remove, whether to rest or smith, which boss relic to carry into act 2 — decide runs just as surely, and today's system plays them worst. The short version of this page: the run is one game, the current stack plays it as several disconnected ones, and the redesign routes every out-of-combat decision through one of four primitives sharing one value scale — with exact dynamic programming doing the planning where the game's structure permits exactness, and full-run self-play manufacturing the labels.

This describes the target design (approved 2026-07-31), which assumes the combat rewrite described in how the combat model plans a turn. The Phase-0 probes in Part 5 are implemented and run against the current stack today; the rest lands phase by phase. Grounded in the design spec §0–§10 and the live-code survey it opens with.


Part 1 · The run is one game

Three findings, one disease

A survey of the live code (2026-07-31) found three things, each structural, none a per-decision fluke.

First: live does not play the system the fleet trains. The live picker, SimPicker, holds a forward-simulating evaluator — the component training and the promotion gate actually exercise — but its __getattr__ (sim_picker.py:89) silently delegates pick_card, pick_campfire, pick_boss_relic, pick_neow, and removal_rank to a base imitation picker. The gated evaluator is reached in live play only inside map-path rollouts (ooc_loop.py:378). Two stacks: one gated, the other played. This is the out-of-combat instance of a trap this project has already been bitten by once on the combat side — validating one decision path while live runs a sibling.

Second: whole surfaces are unmodeled. Boss relics — the highest-leverage out-of-combat pick — are a pure imitation argmax with no counterfactual (ooc_infer.py:593). Girya LIFT, Shovel DIG, and campfire RECALL are hardcoded to 0.0 (ooc_infer.py:1145). All out-of-combat potion economy is dead behind MCTS_POTION_RETAIN_W=0.0 (live/engine.py:182-188). Chest opening is hardcoded — and live and the fleet disagree on whether it is a decision at all (live/ooc.py:1992 vs node_classifier.py:37).

Third: the value signal is saturated. Every surface that is modeled computes V(s′) − V(s) on a win-probability head reading 0.9999. At that reading a full heal is worth a measured +0.00003 (ooc_infer.py:1032-1037); draft deltas are ±0.05 noise; and removal ranked keeping Bash over purging it in 8 of 8 trials — a mis-rank patched not by fixing the value but by a structural guard (ooc.py:1663-1688). The ceiling is the signal, not the architecture.

The guardrails don't catch any of this: the out-of-combat case suite covers 6 cases over 2 of roughly 15 decision surfaces (combat's suite has hundreds), and seven ranked silent-fallback paths mean regressions on most surfaces ship invisibly.

flowchart LR
  subgraph LIVE["live run — what actually plays"]
    direction TB
    H["OOC handlers
live/ooc.py"] --> SP["SimPicker"] SP -.->|"__getattr__ fall-through
sim_picker.py:89"| IM["imitation OOCPicker
pick_card · pick_campfire · pick_boss_relic
pick_neow · removal_rank"] HC["hardcodes
Girya/Shovel/RECALL = 0.0 · chest open
potion economy off (retain_w = 0.0)"] end subgraph FLEET["fleet — what training and gate_ooc exercise"] direction TB EV["forward-sim OOCEvaluator"] end SP -->|"map-path rollouts ONLY
ooc_loop.py:378"| EV
The current fragmented stack (spec §0). The dashed edge is the delegation hole: the evaluator that gets gated is bypassed for every listed surface, so live plays the imitation heads the gate never sees. The redesign's answer is structural, not disciplinary — one daemon-owned decision path where this class of divergence is impossible (Part 5).

Why "one game" is the diagnosis and not a slogan

Watching the bot play produces complaints that sound unrelated: it skipped Offering on a boss reward and drafted Twin Strike the next floor; it "doesn't understand Pyramid," skipping energy acceleration its deck is built for; strong relics visibly carry runs whose card evaluations are wrong. The earlier surveys filed these as three findings (F8, F9, F10). They are one bug seen from three rooms: there is no single value function — the audit counted six-plus value functions and about eight hard-rule overrides — so a card can be priced one way in a draft, another way at a removal screen, and not at all next to the relic that makes it good. Every mechanism in this design exists to make that class of inconsistency structurally impossible: one value scale, one set of primitives, one decision path.


Part 2 · Four primitives, every decision

Complete coverage, by contract

The redesign's coverage contract: every decision routes through exactly one primitive or is an exact enumeration / engine-fact rule. No imitation heads, no hardcoded values, no "always take." The primitives: P1 is combat search itself (the combat rewrite); P2 is candidate scoring — a contrastive head ranking concrete alternatives against the run state; P3 is the resource-horizon planner — pricing use-now against use-later over the forecast remainder of the route; P4 is exact dynamic programming over the act map. The inventory, in full:

DecisionMechanism
Card play, target, end turn, in-combat selectsP1 (combat rewrite)
Potion use in combat (+ target, timing)P1 with P3 opportunity-cost feature (the potion seam, below)
Neow (all options incl. trades, boss swap)P2, grounded (clone-apply-resolve)
Card reward pick / skip / Singing BowlP2 (bowl = static-fact candidate)
Boss relic pick AND skipP2 (replaces imitation head, F17)
Shop incl. leaveP2 subset enumeration under gold budget, scored as possession sets (G3)
Grids: remove/transform/duplicate/upgrade/bottle/multi-picks/OrreryP2 grid, contrastive keep/gain value (leave-one-out banned)
Map path, act-4 keys, Wing Boots flightsP4 exact DP (boots = edge-set expansion)
Campfire rest/smith/lift/dig/toke/recallP3 timing × P2 target (Girya/Shovel become valued candidates)
Potion take/swap at rewards & shopsP3 (belt-slot cost vs drop-counter distribution)
Potion OOC drink (Fruit Juice, Blood Potion…)P3, required-on; MCTS_POTION_RETAIN_W gate deleted
Event options (~50 events; fights via forecast API)P2 over grounded event forcing
Knowing Skull & repeatable eventsP3-style stopping over the enumerable chain
Match & Keepbelief-state enumeration, card values from P2 marginals
Chest open/skip; sapphire-key-vs-relicP2 / P3 (live/fleet disagreement resolved)
Gold/relic take; reward-screen ordering (Whetstone/War Paint/Bottle)dominance facts + exact enumeration of orderings, scored
Prismatic / cross-class offersP2, class-conditioned embeddings

The worked example: the potion seam

The one genuinely new mechanism in the design is where combat and the run meet. Combat's P1 owns the decision to use a potion, but the cost of using it is a run-level quantity — "will I want this Fire Potion more against the elite two floors from now?" — that combat's fight-scoped value cannot see. Neither prior design wired that hold-value into combat search; the live system's answer today is the MCTS_POTION_RETAIN_W=0.0 gate, which prices holding at exactly nothing.

The fix: at fight start, the daemon computes Ulater per held potion from the horizon planner — P3 run over the remaining route, using the forecast elites and boss (the same fight-forecast API the combat rewrite exposes), the potion drop-counter distribution, and belt capacity — and injects it into combat search as an opportunity-cost feature on that potion's USE action. Combat then trades "8 HP now" against a grounded number instead of against zero. Teacher labels are generated with the same feature present, so the net trains on the seam it plays.

sequenceDiagram
  participant P3 as horizon planner (P3)
  participant D as sts_brain daemon
  participant P1 as combat search (P1)
  Note over D: fight start
  D->>P3: remaining route: forecast elites/boss,
drop-counter distribution, belt capacity P3-->>D: U_later per held potion D->>P1: inject U_later as opportunity-cost
feature on that potion's USE action Note over P1: search trades "8 HP now"
against a grounded number P1-->>D: decision record: U_later consumed,
use vs hold priced on one scale
The potion seam (spec §3.1). One mechanism prices the potion at rewards (take), on the belt (drink to free a slot), and mid-fight (use) — all from the same Ulater. It closes the two oldest potion findings end-to-end: mis-timing because holding was free (F12), and Fruit Juice needing a hardcoded auto-drink because +5 max HP sat below the value head's noise floor (F14).

Part 3 · The signal, not the architecture

What the value head is allowed to say

The autopsy in Part 1 has one root cause: asking a saturated probability for derivatives. When P(win) reads 0.9999 on every reasonable board, the difference between two futures — a heal, a draft, a removal — lives in the fourth decimal place, below the head's own noise. The measured numbers are the whole argument: +0.00003 for a full heal, ±0.05 of noise on draft deltas, Bash kept over purged 8/8. No architecture on top of that signal can rank decisions, which is why the fix starts with the signal.

Four label layers, priced by cost

Labels come in four layers, the expensive spent only where the cheap is uncertain: (1) DP-integrated labels — routing values and Unow/Ulater targets, cheap, millions per generation; (2) contrastive matched pairs — both arms forward-simmed across paired seeds, moderate cost; (3) full-run counterfactual branches — at budgeted contested decisions the run is branched, both arms played to completion with teacher combat, and the milestone outcome vectors differenced: the gold standard, and the teacher's signal (Part 4); (4) human-corpus advantage-weighted regression over 3.5M runs — the generation-0 cold start, decayed as self-play labels accumulate.

One number line for everything

Ranking is within-surface, but the run's real dilemmas trade across surfaces: buy-vs-save, remove-vs-buy, key-vs-heal. So the contrastive head emits advantage in milestone-value units, and matched pairs are mined across surfaces — draft-vs-gold, remove-vs-buy, key-vs-heal — to anchor one scale. Cross-surface trades then read from one number line, which is exactly what the fragmented six-value-function stack could never do.

The per-generation gate holds the signal honest: self-calibration on the candidate bundle's own held-out runs is baked into the export and hard-fails on drift — the DP plans against these predictions, and a confident wrong forecast is worse than a refused one.


Part 4 · Planning

Integrate, don't sample

Three candidate architectures were assessed for the runtime: (A) a hardened factored design — net heads plus exact DP inside the daemon; (B) run-level Monte-Carlo graph search at decision time with forecast leaves; (C) one end-to-end unified game with no combat/OOC seam. The adjudication that settled it: they are roles, not rivals.

CandidateRoleWhy
C — unified full-run game The teacher. At decision time it is infeasible — an OOC node expanding through real combat search, rejected twice already. Offline on the fleet it is exactly the generation loop's collect stage: full-run self-play with deep combat search, manufacturing the labels. C's ceiling is the system's ceiling; everything else distills it.
A — factored heads + exact DP The live spine. Exact DP where structure permits exactness — the act map is a small known DAG, so integration beats sampling — and distilled contrastive heads where the signal problem lives, all consumed inside sts_brain. As labeling budget grows, A converges toward C: the same distillation contract the combat rewrite makes.
B — decision-time run search Filed, with a trigger. Bounded run-level search only at contested route forks (two routes inside the DP noise band, or high drift-term variance), with the DP as prior. Adopted only if the deck-drift case family fails on the DP alone. Not a default: B pays determinization machinery and train/play divergence to buy emergent deck drift — its single structural advantage — and ranking surfaces gain nothing from search.

The one thing DP can't see natively: deck drift

The P4 DP prices map node k with today's deck — but the deck at floor k depends on the drafts along floors 1..k−1. Route comparisons at act scale are really deck-evolution comparisons: the elite-heavy path pays out in relics and card rewards that change what the deck is by the time it reaches the boss. The mechanism: teacher full-run trajectories label drift distributions — deck-strength delta by act, floor-delta, and route-class — which the DP integrates as an additional term. That captures the first-order route-comparison error (elite relics, draft accumulation); the residual is second-order because drift largely cancels between sibling routes. A named deck-drift case family gates the term, and failure of that family is precisely B's adoption trigger.

The honest ceiling statement: the binding constraints are teacher labeling throughput, forecast fidelity at the combat seam, and coverage of rare surfaces and out-of-distribution candidates. None of them are runtime architecture — which is why A wins: it spends zero runtime machinery on non-problems.


Part 5 · Explainability as contract

Every decision explains itself, or fails loudly

Live blunders are archaeology today: reconstructing why the bot walked into an elite means grepping logs after the fact. The design makes explanation a contract, not a courtesy. Every out-of-combat decision emits a decision record: all candidates with their advantages in value units — skip, leave, and hold always listed as candidates; the exact forecast distributions consumed; the DP slice (the node × HP-bucket table) for P3/P4 decisions; and replay-sufficient state for one-command case scaffolding. A decision that cannot emit its record fails loudly. Seam attribution falls out for free: a wrong route decomposes into "forecast wrong" (combat's fault) versus "integration wrong" (OOC's fault).

Phase 0: measure the current system first

Before any of the redesign ships, five probes run on the current stack with zero behavior change, turning Part 1's findings into per-surface numbers:

  1. Delegation tracer — count every SimPicker.__getattr__ fall-through, per surface.
  2. Saturation meter — log winner-minus-runner-up margins at draft, removal, and campfire picks.
  3. Encounter-head consumption audit — log the survival term's actual contribution to each campfire and path pick.
  4. Boss-relic counterfactual shadow — run grounded forward-sim valuation alongside the imitation head; log disagreement.
  5. Fleet/live decision diff — replay live runs through the fleet's chooser offline; publish per-surface disagreement rates.

These are implemented and running now: the delegation tracer, the shadow-sim on boss relics, card rewards, and campfires, the campfire survival-term audit, and the consolidated Phase-0 report land as of this week, alongside the case suite's expansion beyond its 6 cases to the removal and boss-relic surfaces — each new case a wiring-faithful replay pushed through the live decision stack, shape-verified against real recorded game pushes.

That last property already paid for itself. The expanded suite's first run against live's real encounter head surfaced a genuine finding: the DP-fallback pathing tier routes into an elite at 4 HP — the sim-planner tier, given the same board, does not. No model-level gate could have caught it, because nothing is wrong with any model's score; the defect lives in which tier the live wiring hands the decision to. Replaying the true stack, fall-throughs and all, is the only kind of test that sees it — which is the whole Phase-0 thesis, demonstrated on its first outing.


Part 6 · Verdict

What stays, what gets fixed, what was refused

KeepLoad-bearing and staying

  • Exact DP over the act map. The map is a small known DAG; integrating it exactly beats sampling it, and the route value comes with a full node × HP-bucket table attached.
  • Grounded event forcing. Event options are resolved by clone-apply-resolve against the real engine, never by a learned guess at what the event does.
  • Match & Keep as belief-state enumeration, with card values read from P2 marginals — exact where exactness is available.
  • Ratchet discipline, imported from combat. Per-surface case families, a deployed-passing set, quarantine-with-reason, never-regress — plus combat's budget-honesty lesson: never gate at a cheaper forecast budget than live runs.

ImproveKnown-deficient, with a named fix

  • Case coverage: 6 cases → all ~15 surfaces. Seeded from documented blunders and probe findings; removal and boss relics are already landed (Part 5).
  • Commensurable value units. One milestone-value scale across surfaces, anchored by cross-surface matched pairs, so remove-vs-buy and key-vs-heal stop being category errors.
  • Potion economy, required-on. The MCTS_POTION_RETAIN_W gate is deleted, not tuned; the horizon planner's Ulater prices take, drink, and use from one number.

RejectEvaluated and refused, with reasons

  • Decision-time full-run search — twice rejected: an OOC node expanding through real combat search is infeasible at decision time, and ranking surfaces gain nothing from search. It survives only as B, a filed contingency behind the deck-drift trigger, and as C, the offline teacher.
  • Imitation heads — the boss-relic argmax is the cautionary instance: no counterfactual, no candidate cross-attention, no way to say why. Every ranking surface moves to P2.
  • V(s′) − V(s) deltas — differencing a saturated probability produced the +0.00003 heal and the 8/8 Bash mis-rank; banned at code-review and assertion level.
  • Values-as-rules hardcodes — Girya/Shovel/RECALL at 0.0, chest-open-always, Fruit-Juice auto-drink: each is a value judgment frozen into code where no gate can see it drift. Everything becomes a valued candidate.
  • Silent fallbacks — the seven ranked fall-through paths become raises or loudly-counted degrades; the delegation-hole class is made structurally impossible, not discouraged.

The through-line matches the combat page's: every job sits with the component that can be held accountable for it. Exactness lives in the DP and the enumerations, judgment in one contrastive head on one value scale, truth in the teacher's full-run branches, and accountability in the decision record. The run is one game; from Phase 0's first measurement to the last surface handed off, the system is being rebuilt to play it as one.

Sources: OOC ideal-solution design 2026-07-31 §0–§10 (user-approved), absorbing the 2026-07-26 signal-first spec and §5–§6 of the 2026-07-25 combat rewrite spec · survey code refs: sim_picker.py:89, ooc_loop.py:378, ooc_infer.py:593, ooc_infer.py:1145, live/engine.py:182-188, ooc.py:1663-1688 · Phase-0 probes + expanded case suite (landed 2026-07-31) · companion page: how the combat model plans a turn.