spire-mentor

Benchmarks

How fast the Rust engine is on one core and on a whole 24-core machine, how it compares with the shipped game run headless, how long its searches and a PPO training iteration take, and how it compares with two other Slay the Spire 2 simulators. All measured on one server on 14 October 2026.

engine actions per second on one core (59,400 random-play fights per second)
1.30 M
actions per second on the whole machine, 48 processes
31.0 M
the shipped game's best measured throughput on one core
1,500×
the previous engine (12 Oct 2026), same fights
2.25×
a typical PPO training iteration, down from 609 s
208 s
Headline results
WhatResultCompared with what
One core59,400 fights/s · 1.30 M actions/s · 766 ns per actionRandom play: Ironclad's starter deck against the 22 Act-1 encounters; one process on one core, the core's second hardware thread idle
Whole machine1.41 M fights/s · 31.0 M actions/s48 processes on 24 cores / 48 threads: 23.5× the one-process run of the same series
Previous engine2.25× fasterThe engine of 12 Oct 2026 on the same fights, identical results; 7 interleaved pairs, 2.17–2.37×
Shipped game, one core1,500× (1,630× with both hardware threads busy on each side)The game's best measured on one core: 16 game processes sharing it (the most tried; still rising from 8 to 16, CPUs 89% busy), 871 actions/s
Shipped game, whole machine2,430×The game's best on all 24 cores: 192 processes, 12,700 actions/s
Shipped game, CPU time2,970× (2,250× against its game thread alone)One warm game process: 2.28 ms of CPU per action (1.73 ms on the game thread) against 766 ns
Browser hint, 1,000 simulations18.4 ms as WASM in Chromium · 13.7 ms native1.93× faster than the previous engine in Chromium
PPO training208 s per typical iteration instead of 609 s (2.93×); 150 iterations in about 9 h instead of 25 hThe deployed Ironclad recipe (960 games, 46 workers, one RTX 3090) on the engine training ran before
sansb/sts2-simOurs is 3.13× faster per core on random play with no relic on either side, each engine stepping its own way (3.39× with our relic live; 1.63× on sansb's own fixture with a state copy per step, as sansb steps); 3.52× on the whole machine with our Burning Blood live (about 3.25× without it, scaled by the one-core ratio)Combat only, game build v0.111.0, certified against real-game checksums; its review solver searches the true state by design (hindsight reviews)
lain-wood/sts2corests2core is 1.56× faster per core (1.64–2.99× on sansb's fixture), 1.44× on the whole machineCombat only, effectively Ironclad only, partial content, its own RNG by design, validated on recordings of real play; its live solver is given the true draw-pile order (partly clairvoyant)

Rounding: most measured values have three significant figures; actions per fight have two decimals and percentages whole points; counts, dates and build ids are exact. Medians over repeated runs. The unrounded values, the commands and the identity checks are in the benchmark write-up.

The machine

SystemASUS ESC4000A-E10 server, one socket, one NUMA node
CPUAMD EPYC 7443P (Zen 3 "Milan"): 24 cores, 48 hardware threads (SMT on)
Clockboost up to 4.04 GHz (top non-boost state 2.85 GHz), schedutil governor. Measured: 3.91 GHz with one busy process, 3.65 GHz with 48
Caches32 KiB L1d, 32 KiB L1i and 512 KiB L2 per core; 128 MiB L3 in four 32 MiB domains of six cores (CPUs 0–5, 6–11, 12–17, 18–23, each with its SMT siblings 24 higher)
Memory256 GiB DDR4-3200 registered ECC, 4 × 64 GB; 4 of the CPU's 8 memory channels populated (102.4 GB/s peak)
GPUs4 × NVIDIA GeForce RTX 3090, 24 GiB each; training uses one
OSUbuntu 24.04.4, Linux 6.8, no isolated CPUs, transparent huge pages on request (madvise)
Rust1.96.0; release profile with fat LTO, one codegen unit, opt-level 3, no target-cpu, no profile-guided optimisation (except the training build marked PGO)
.NET9.0.15 runtime, default settings (tiered JIT, dynamic PGO)
Python, PyTorchCPython 3.13.12; torch 2.12.1 with CUDA 13.0
Browserheadless Chromium 149 (Playwright 1.61); Node 25.9

The host is shared with other users, so every timed run waited for it to go quiet first (Method). Three definitions hold on the whole page:

  • One core: one single-threaded process pinned to one physical core (CPU 20) with that core's second hardware thread (CPU 44) idle. The shipped game runs several threads per process, so for it one core means one game process and its Python driver confined to both hardware threads of that core.
  • Both threads of one core: two of our processes on CPUs 20 and 44. For the game: 2 to 16 game processes sharing CPUs 20 and 44.
  • Whole machine: all 24 cores and 48 threads. For a Rust engine, the better of 24 processes (one per core) and 48 (one per hardware thread); for the game, the best of 24 to 384 processes. Each table says which one won.

An action is one state transition: a card play or an end of turn. Whole-machine rates are the sum of the processes' own rates, all released at the same instant.

Engine throughput

The workload is random play: Ironclad at Ascension 0, the 10-card starter deck at 80/80 HP and Burning Blood, against the 22 Act-1 Overgrowth encounters in turn (4 weak, 12 normal, 3 elites, 3 bosses), with uniformly random legal moves until the fight ends. Each fight starts from a copy of a start state built the way a real run builds it. A run is 110,000 fights: 2,200 seeds × 50. The random player wins 32.4% of them, with 22.0 actions per fight.

One core, previous and new engine
Previous (12 Oct)New
Fights per second26,40059,400
Actions per second581,0001.30 M
Time per action1,720 ns766 ns
Instructions per action13,5006,520
Instructions per cycle1.972.07
Clock while running3.90 GHz3.88 GHz

The new engine is 2.25× faster (median of 7 interleaved pairs; 2.17–2.37×). It executes half the instructions per action (the previous one needs 2.08×) and the fights are identical: the same 2,417,920 actions, the same 35,646 wins and the same digest over every fight's length and outcome in all 14 runs.

From one core to the whole machine (new engine, 3 repeats each)
PresetCPUsProcessesFights/sActions/sPer processEfficiencyGHz
1 core20 (44 idle)160,1001.32 M1.32 M1.003.91
1 core, both threads20, 44264,5001.42 M709,0000.5373.90
1 L3 domain, 1 thread per core18⁠–⁠236357,0007.84 M1.31 M0.9893.87
1 L3 domain, both threads18⁠–⁠23, 42⁠–⁠4712381,0008.37 M697,0000.5283.86
12 cores, 3 per L3 domain0⁠–⁠2, 6⁠–⁠8, 12⁠–⁠14, 18⁠–⁠2012710,00015.6 M1.30 M0.9853.87
24 cores, 1 thread each0⁠–⁠23241.37 M30.2 M1.26 M0.9533.79
48 threads0⁠–⁠47481.41 M31.0 M645,0000.4883.65

Each process plays 440,000 fights. Efficiency = the aggregate rate ÷ (processes × the same repeat's one-process rate), so a value near 0.5 on the two-thread presets means each core still does about the work of one process. Every process of every preset played identical fights (one digest).

1 core (1 process on CPU 20): 60,077 fights/s1 core60.1 k 1 core, both threads (2 processes on CPUs 20 and 44): 64,531 fights/s1 core, 2 threads64.5 k One L3 domain, 6 cores (6 processes): 356,735 fights/s1 L3, 6 cores357 k One L3 domain, 12 threads (12 processes): 380,857 fights/s1 L3, 12 threads381 k 12 cores, 3 per L3 domain (12 processes): 710,015 fights/s12 cores710 k 24 cores, one thread each (24 processes): 1,374,049 fights/s24 cores1.37 M 48 threads (48 processes): 1,409,466 fights/s48 threads1.41 M 00.5 M1.0 M1.5 M random-play fights per second, all processes
Fights per second of the whole preset, new engine, median of 3 repeats (the table above has the numbers). Throughput grows almost linearly with cores; the second hardware thread of each core adds 7% on one core and 3% across the machine.

Measured the same way, the previous engine reaches 13.8 M actions/s on the whole machine (48 processes), so the new one is 2.24× faster there as well (2.23× on one core of the same series). With every core busy the clock falls from 3.91 to 3.79 GHz (24 processes) and 3.65 GHz (48), which accounts for most of the missing 5% at 24 processes.

Against the shipped game

What "headless" means. The game's own assembly, sts2.dll of build v0.107.1 from a copy of the game, IL-patched and run by the project's harness on .NET 9 with Godot stubbed out: no window, no rendering, no animations. A Python driver talks to it as JSON over a pipe and plays the same fights as the Rust benchmark, choosing uniformly at random from the game's own list of legal actions. It is the same oracle the parity suite checks the engine against. Random play gives the same statistics on both: over 8,448 fights the game wins 32.2% with 21.98 actions per fight, our engine 32.4% with 21.98 over 110,000, and 21 of the 22 encounters agree within two standard errors (largest |z| 2.05).

Why one game process is mostly asleep. After every card play the harness waits for the game's action queue to drain by polling it: Thread.Sleep(1) until five idle checks in a row (WaitForActionExecutor in sts2-pycli/src/Sts2Headless/RunSimulator.cs). The game thread goes to sleep 8.6 times per action, and a warm game process is on the CPU for only 22% of its play time: 2.28 ms of CPU in 10.4 ms per action. The wall time of one process therefore measures the harness's sleeping as much as the game. The fair comparisons are CPU time and throughput with many game processes sharing the CPUs, as many as raised the game's throughput in the layouts tried.

Five ways to compare (our engine: new, Burning Blood live)
ComparisonThe shipped gameOur engineRatio
One process, cold: a fresh process per fight (the old site figure)17.9 ms wall per action766 ns23,400×
One process, warm: many fights per process10.4 ms wall per action766 ns13,500×
CPU time per action, warm process2.28 ms (whole process) · 1.73 ms (game thread)766 ns2,970× · 2,250×
One core, best measured871 actions/s (16 processes on CPUs 20 and 44)1.30 M (one thread) · 1.42 M (both threads)1,500× · 1,630×
Whole machine, best measured12,700 actions/s (192 processes)31.0 M (48 processes)2,430×

The fair headline: against the game's best measured process count (16 on one core, 192 on the machine), our engine does about 1,500 times the shipped game's work on one core and 2,430 times on the whole machine. Per unit of CPU time it is 2,250 to 2,970 times in one warm game process (game thread or whole process), and 954× at the lowest across every process layout measured. On one core the game had not levelled off at 16 processes, so the per-core ratio may be somewhat lower with more.

Our process is CPU-bound, so its 766 ns per action is also its CPU time. The game's figures lean in the game's disfavour in two small ways: its CPU time includes the harness's own work (building the state dictionary on the game thread, JSON on the main thread), and its throughput cells include a reset of about 30 ms per fight (reloading the save and entering the room), which our engine replaces with copying a start state. In one warm process the game does 96.5 actions/s without the reset and 85.8 with it. Across all the process layouts below, the game thread's CPU per action ranges from 0.73 to 1.73 ms, so the game-thread CPU-time ratio ranges from 954× to 2,250× (whole process: 1,200× to 2,970×).

Why "23,000×" is retired

Earlier versions of this site said the engine was about 23,000× faster than the shipped game. That compared the wall time of one game process per fight, started cold, with one Rust thread: today 17.9 ms per action against 766 ns, 23,400×. Of those 17.9 ms, about 2.3 ms is the game's warm CPU work, about 7.8 ms is extra CPU a fresh process spends on its first fight (JIT compilation and cold start), and about 7.8 ms is time off the CPU in the harness's 1 ms polling sleeps. It measured that setup correctly, but it is neither a throughput nor a language comparison.

The shipped game in every process layout measured
LayoutProcessesActions/sPer coreCPUs busy
One core, 1 warm process185.885.8–
One core, shared217417423%
One core, shared435235231%
One core, shared862762760%
One core, shared1687187189%
Whole machine, 1 per core242,14089.113%
Whole machine, 1 per thread484,24017715%
Whole machine, unpinned484,25017719%
Whole machine, 4 per core967,90032938%
Whole machine, 8 per core19212,70053067%
Whole machine, 16 per core38412,50052186%

Every process carries its own Python driver on the same CPUs. Warm processes play 220 untimed fights first. The game never fills the CPUs: with 384 processes they are 86% busy and throughput is already lower than with 192. The whole-machine best is 61% of 24 × the one-core best. All layouts of a group played the same fights with the same digest, and 66 reference fights replayed step for step as in the earlier measurement.

Training speed

Training plays whole runs: the PPO run policy for every decision outside fights, MCTS for every fight (250 simulations per decision, 1,000 at bosses). Three builds: the previous engine as training ran it before (plain build), the new engine plain, and the new engine with the profile-guided build (PGO) that training uses now. Only this project's engine appears here: the shipped game cannot copy a state for search, and the other two simulators have no run-level training loop.

One worker on one core: six fixed Ironclad runs
BuildCPU s per runRuns per hourvs previous
Previous, plain12.32921.00
New, plain5.027182.46×
New, PGO3.919213.16×

Seeds 700–705 at Ascensions 0/3/6/10, the Ironclad champion, CPU 20; 3 runs per build, all nine with byte-identical game records. Part of the speed-up is not the engine: the new code skips the search when a decision has exactly one legal move.

A full iteration is the deployed Ironclad recipe: 960 games across Ascensions 0–10, 46 worker processes on this 48-thread machine, then a PPO update on one RTX 3090, starting from the Ironclad champion. The new builds play identical games. Measured as the trainer ran it, iteration 0 is distorted by one game: game 703 (Ascension 4) has an Act-1 boss fight that takes far longer than any other game, so for more than half of iteration 0 it runs alone while the other 45 workers wait. On the previous engine it ran past the trainer's 600 s stall watchdog, which dropped it, so that iteration finished only 959 of 960 games and its time is a lower bound.

Iteration 0, as the trainer ran it (2 runs each)
Previous, plainNew, plainNew, PGO
Collection≥ 1,180 s537 s381 s
PPO update on the GPU34.1 s34.1 s34.2 s
Iteration≥ 1,210 s571 s416 s
Games per hour≤ 2,9306,4409,070
CPU seconds per game27.411.78.43
Machine busy during collection50%47%47%
A typical iteration: the same pool without its one outlier game
Previous, plainNew, plainNew, PGO
Collection (the other 959 games)575 s242 s174 s
Iteration (with the update)609 s276 s208 s
Games per hour6,01014,30019,900
150 iterations25.4 h11.5 h8.66 h
Speed-up per iteration1.002.21×2.93×
Total CPU for all 960 games26,700 s11,000 s7,900 s

The throughput measures agree: total CPU falls 2.42× (plain) and 3.37× (PGO), one core 2.46× and 3.16×, a typical iteration 2.21× and 2.93×. The GPU's share is small: the update takes 34 s of the 208 s, with the GPU 87% busy during it and idle during collection. With the 46 workers busy, the whole machine plays 21.6× the games per hour of one worker on one core (PGO build; different games, so indicative).

How "typical" was measured: the same 960-game pool replayed outside the trainer without the watchdog, once per build. The replays reproduce the trainer's new-engine collection times within 0.5% and predict the previous engine's watchdog cut at 1,175 s (trainer: 1,177–1,180 s). They ran right after the quiet-host window, one at a time with the machine about 2% busy before each, but not under its gate, so treat them as a validated estimate. A real campaign's utilisation is about 0.95, not the 0.5 of this iteration 0, but it also has rare outlier iterations: 21 of 762 in the four past Ironclad campaigns on this host hit the same watchdog. As a cross-check, the current Ironclad champion's last 150-iteration leg took 23.9 h on an older engine, close to the 25.4 h projected for the previous one.

Other simulators

This section measures two other open-source Rust simulators of Slay the Spire 2 combat, and each does something better than this project: sansb/sts2-sim certifies its fights against checksums the real game writes while it is played, which this project does not have yet, and it tracks a newer game build than the other two; lain-wood/sts2core is the fastest of the three per core; it steps a compact 3,880-byte state by value. Both were measured at the commits linked, on this machine, with our own harnesses built against their public APIs.

What each one is
spire-mentor (this project)sansb/sts2-simlain-wood/sts2core
Measured at6d1918237, 14 Oct 2026 (previous engine d3ea66a51)c61336cf, 12 Oct 20263b2d969d, 25 Sep 2026
Game buildv0.107.1v0.111.0 (the newest of the three; v0.109.0–v0.110.1 lie between)v0.107.1
Scopewhole single-player runs: map, events, shops, rest sites, rewards, three acts, Ascension 0–10combat only, from a fully specified start state; Python tools rebuild run context from capturescombat only: a rules kernel, an in-combat solver and a deck advisor
Charactersall 5all 5 (Ironclad is the largest share of its validation, 43–48% per set)effectively Ironclad only (our reading of its 79 recordings and its tables; its docs do not say)
Contentan id for every current content class of the game (580 card ids for its 577 cards plus 3 legacy/internal; 297 relics, 63 potions, 121 monsters, 69 events, 23 enchantments; an id does not prove the behaviour); 3 of the 6 event-started fights are skipped: two give a stand-in outcome instead (one grants a placeholder relic), the third is not ported596 cards, 299 relics, 64 potions, 105 monster kinds, 88 encounters; content it has not ported is refused, never approximated117 cards incl. a placeholder (112 of the 128 in its reference catalog), 105 enemies incl. 3 synthetic and an unknown placeholder, 118 relic entries of which 103 modelled, 5 of the game's 22 enchantments; the Queen boss missing; values from Ascension 8 up taken from the game's code but not yet checked against its recordings (A0–A3)
Reproduces the game's RNGyes: .NET System.Random ported, all 15 run and player streamsyes: all 9 combat streamsno, by design: its own streams for shuffles, enemy rolls, HP and rosters
Validated howlockstep against the game's own assembly run headless, compared after every action (enemy intents and card afflictions are not in the compared state): full fuzz tier of random moves, Ironclad 6,104/6,104 clean (30 Jun 2026); Silent 5,732/5,762, Defect 5,740/5,752, Regent 5,740/5,762, Necrobinder 5,716/5,752 (7 Jul 2026, a deferred tail still open); run level 1,232/1,250 clean (7 Jul 2026, 5 characters × A0/2/5/8/10, random-policy runs that mostly end in Act 1; deeper Act-2 checks in June still had open divergences); no full sweep since; no checksummed real-game captures yet; five suspected bugs from 13–14 Oct openlockstep against the checksums the real game writes after each action: 1,196 of the author's 1,206 captured fights replay exactly and none diverges (the other 10 are truncated, refused or unusable captures); 594 certified published fixtures; 4,205 certified A9/A10 fights from site uploads (a corpus of certified fights only; the sets overlap) (as of c61336cf)one-step predictions on 79 recordings of real play: 1,565 match, 1 mismatch (a trace recorded before an enchantment fix), 9 content missing, 177 skipped; enemy next move 85% (353/415). Its README calls its deck advisor's absolute death rates untrustworthy, only paired differences (a caveat about the advisor, not the rules kernel)
Search sees hidden information?no, for every deployed search (run AI, browser hint, real-game advisor: since July 2026 the draw order is hidden and every combat random stream reseeded); the search library's default setting is still clairvoyantyes, by design: its exact solver and its site's reviews search the true state, draw order and RNG included, to show what was achievable in hindsight; a draw-blind lookahead (reshuffled draw pile, reseeded streams) exists for its coach's offer reviewspartly: a patched game-state exporter supplies the true draw-pile order, used as known until the next reshuffle
LicenceMITMITMIT
Rust code lines217,781 (engine: core + content; roughly 40% of raw lines in test modules; search lives in other crates); 365,739 (all 12 crates)419,659 (engine/src, incl. its solver; roughly 55–60% of raw lines in test modules)32,366 (src/, incl. its solver, advisor binaries and tests)
BindingsPython (PyO3); WASM (the browser workbench)WASM (its browser engine); no Python bindingsnone; zero dependencies
Steppingone state changed in place; a fight copies its start state oncea new state allocated for every stepa 3,880-byte plain-old-data state passed by value
Legal actionsone per card in hand per target (our search merges identical cards itself)identical cards and identical targets mergedidentical cards merged
Buildrustc 1.96.0, fat LTO, unwinding panicsrustc 1.97.1, thin LTO, aborting panicsrustc 1.96.0 (it pins none; its authors use 1.97.1), fat LTO, aborting panics

Clairvoyance is a property of a search, not of an engine: every engine here steps a concrete state, ours included. No benchmark in this section runs a search, so it affects none of these numbers.

Lines of code: cloc, Rust code lines without comments or blank lines, in-file test modules included, for each project's engine source directories. Test shares are a heuristic count (lines from a #[cfg(test)] module to the end of its file, plus test files). sansb's and sts2core's directories include their solvers; ours keeps search in separate crates, counted in the 365,739.

Random play, one core (W1)

The workload of the sections above. The other engines merge identical actions, so their own "uniformly random" player picks among fewer moves (it ends its turn more often) and plays a different, shorter game. Our harnesses therefore also give them the instance player: one choice per card in hand per target, the same random player as ours and the game's. Its weighting is our harness code, running inside their timed loop. With their own players, which play different games, their action rates are 0.7% (sansb) and 1.7% (sts2core) higher (medians in the table), and the ratios are 3.12× (sansb, ours with no relic) and 1.59× (sts2core).

W1 on CPU 20, 7 rounds, order rotated every round
Engine, random playerFights/sActions/sns per actionWinsActions per fight
Ours, Burning Blood live59,0001.30 M77032.4%21.98
Ours, no relic54,7001.20 M83132.4%21.98
sansb, instance player17,500383,0002,61032.5%21.95
sts2core, instance player90,2002.03 M49335.0%22.47
sansb, its own merged-action player20,300386,0002,59020.3%19.01
sts2core, its own merged-action player106,0002.06 M48521.0%19.41

Paired by round: ours ÷ sansb is 3.13× with no live relic on either side (Burning Blood is held but inert in sansb, whose combat-only scope leaves out its after-fight heal) and 3.39× with ours live. Ours ÷ sts2core is 0.640: sts2core is 1.56× faster, with Burning Blood modelled on both. Our engine is 7.6% slower with no relic than with Burning Blood on identical fights (it runs 3.1% more instructions per action); the cause is not investigated, so both figures are given.

W1, whole machine: actions/s of all processes, instance player (3 repeats; ours with Burning Blood live)
PresetProcessesOurssansbsts2core
1 core11.32 M388,0002.01 M
1 core, both threads21.42 M341,0002.20 M
1 L3 domain, 1 thread per core67.84 M2.29 M11.6 M
24 cores, 1 thread each2430.2 M8.79 M44.7 M
48 threads4831.0 M7.52 M44.7 M

Best against best (in bold): ours ÷ sansb 3.52× (ours with Burning Blood live, as in the per-core 3.39×; about 3.25× like for like, scaled by the one-core ratio; there is no relic-free whole-machine run), ours ÷ sts2core 0.692 (sts2core 1.44×, Burning Blood live on both). The second hardware thread helps ours (+3% across the machine), makes no difference to sts2core and costs sansb 14.5% (48 threads ÷ 24 cores = 0.855).

sansb's own benchmark fixture (W2)

sansb's sts-sim bench plays 240,000 random playouts from one Ironclad start state against two Toadpoles (68/80 HP, no relic), allocating a new state for every step. The fixture leaves the ascension field out, which sansb reads as Ascension 10, so all three engines play it at A10. Our "copy per step" mode does what sansb does; "in place" is how our own searches step.

W2 on CPU 20, 5 rounds
Engine, modeActions/sPlayouts/sWinsActions per playout
Ours, a copy per step658,00035,30088.7%18.7
Ours, in place1.19 M63,80088.7%18.7
sansb, instance player404,00021,00088.2%19.3
sansb, sts-sim bench as published414,00024,300–17.0
sts2core, instance player1.95 M106,00089.3%18.5
sts2core, its own merged-action player1.95 M114,00061.1%17.2

Ours ÷ sansb with the same player: 1.63× copying per step, 2.95× in place. Ours ÷ sts2core: 0.335 copying, 0.610 in place. On this fixture our lead over sansb is smaller than on W1, and sts2core's lead over ours larger.

Their own benchmarks
BenchmarkAs published by its authors (their hardware)Re-run on this machine
sansb sts-sim bench (W2 above)1,049,000 transitions/s, one thread on an Apple M5 Max, best of 5414,000 actions/s, CPU 20, median of 5
sansb eval_throughput: random playouts from its 594 certified real-game fixturesmedian 302,000 transitions/smedian 120,000 (10th–90th percentile 66,700–338,000)
sts2core bench: a 28-card deck against a 300-HP synthetic target, a player that never ends its turn by choice3.56–3.61 M steps/s on 1 thread, 54.9–56.5 M on 32 threads; its authors call their machine noisy, "not a baseline"2.68 M on 1 thread, 44.4 M on 24 threads, 55.3 M on 48

Published figures come from the authors' own machines (sansb: an Apple M5 Max; sts2core: a Windows PC), so only the re-run column is comparable with the rest of this page.

What makes the comparison fair, and where it is not

  • The same work. With the instance player, random play agrees per encounter: sansb with ours on 19 of the 22 encounters within two fight-level standard errors (which understate seed noise on randomized rosters), sts2core on 14. sts2core's largest gaps are Cubex Construct (+18 points; its monster differs from the game's code) and Flyconid, Mawler and Snapping Jaxfruit (+9 to +13; not traced). On Slithering Strangler ours is the outlier: 12.8% against 10.3% and 10.2% (12.0% over 1,600 seeds, whose 200-seed blocks vary by 0.7 points), unexplained. On Slimes (weak), sansb plays 0.86 more actions per fight than ours, unexplained.
  • Merged action lists make each engine's own random player a different player; compare actions per second, or use the instance player, never fights per second across players. Merging also shrinks a search's branching; a per-action benchmark does not credit that.
  • Burning Blood is inert in sansb; hence the no-relic comparison.
  • Stepping. sansb allocates a state per step and sts2core copies 3,880 bytes per step; ours mutates one state. W2's copy mode puts ours on sansb's terms.
  • Game build. sansb models v0.111.0, ours and sts2core v0.107.1. The W1 monsters' HP bands are identical between the two builds at A0, and none of the monsters on sansb's v0.110.1→v0.111.0 change list appears in W1; the v0.107.1→v0.110.1 changes are not documented there, and movesets were not audited.
  • RNG. sts2core draws its own random numbers, so its fights match ours in distribution, not seed for seed; sansb and ours reproduce the game's streams but enumerate actions in different orders, so the same random player still picks different moves.
  • Builds. Each engine uses its own release profile (table above). None uses target-cpu or PGO, and all use the system allocator.
  • W2 is an A10 fight. Read at A0 by mistake, and with our harness opening the back Toadpole with the wrong move (since fixed, see Method), it once looked like a 10-point rules disagreement between sansb and the other two; at A10 all three agree within about one point of win rate, and turns 1 and 2 match exactly. From turn 3 on, sansb's playouts run about 3–4% more actions (19.3 per playout against 18.7 and 18.5), unexplained.

Method

  • When and where. 14 October 2026, 10:59–16:33 UTC, on the machine above. Before every timed stage the host had to be quiet: 1-minute load below 1.5, the whole machine below 3% busy over 5 s, and the stage's own CPUs and their SMT siblings below 5% busy. At the 58 gate passes the load was 1.38 and the machine 2% busy (medians). Whole-machine runs, ours and the game's, used the whole-machine part of the gate only, because background noise from other users on single CPUs never let the per-CPU check pass for the game's cells. The training stages checked only the whole-host busy share (≤ 3%); the 1-minute load at those 15 gate passes was 1.09–7.3.
  • Pinning. Single-threaded runs on CPU 20 with CPU 44 idle; WASM on CPUs 18–23; the advisor as in its table; the game's one-core cells on CPUs 20 and 44. Whole-machine runs fork every process, pin it, and release all of them at the same instant.
  • Interleaving. Previous and new engine alternate (previous, new, new, previous, …): 7 pairs for random play, 5 for each search runtime, 4 for the advisor; the three training builds run in reversed order every round (3 rounds on one core, 2 full iterations). Cross-engine runs rotate their order every round. Values are medians; speed-ups are medians of the paired ratios.
  • Identity checks. A timing ratio is only a speed-up if both sides did the same work, so every harness prints a digest of it: every fight's length and outcome, a search's root statistics, a position fingerprint, or a hash of every training record. All 111 checks passed, and no published run was forced on a busy host.
  • A harness fix. Before this benchmark, our random-fight harness built monsters through a debug scenario builder whose monster AI and HP rolls differed from a real run's. It now builds them as a run does. The random-play statistics barely moved, but the numbers on this site before 14 October 2026 used the old builder; site_random_bench --legacy-spawn reproduces them.
  • Not covered. Other hardware: no desktop or laptop CPU, no ARM, no Windows, no phone or tablet browser. The real game with its renderer. The advisor on a player's PC with the game running.

The full write-up, with every value unrounded, the reproduce commands, the identity checks and what was left out and why: sts2-rs/docs/perf/2026-10-14-benchmarks.md. Harnesses: site_random_bench.rs and xsim_toadpoles.rs (random play, W2), hint_bench.rs (the hint's search), and scripts/site-bench/ (whole-machine runner, the shipped game, WASM, advisor, training, and the harnesses for the other two simulators).