C# · .NET 9 NativeAOT · Python · Gymnasium · reinforcement learning
sts2
A Slay the Spire 2 combat and run simulator in C#, with Python bindings for training. Its organising rule is that fidelity to the real game is demonstrated rather than asserted, and it is the ground for an offline combat agent.
View on GitHub ↗
Original emulator ↗
How a card gets trusted
A wrong simulator trains an agent that exploits the wrong thing. So every card, relic and event goes through these lines, and the last one keeps the failures.
- Sourceexpected values read off the decompiled gameregenerated locally from your own copy
- Testshand-written, with the source cited in each filethe build fails if a card has none
- Capturestage the card in the running game, keep the before and afterSTS2MCP
- Auditwhich cards were actually read, keyed by a digest of the sourcegoes stale when the game patches
- Replayreal captured runs replayed against the emulatorascension 8, v0.107.1
- Catalogueevery divergence found, with how to reproduce itkept after it is fixed
The problem
Slay the Spire 2 is a deep card game, and an agent that learns from a simulator learns whatever the simulator gets wrong. A card that rounds differently, or an effect that resolves in the wrong order, is a rule the agent will quietly build a strategy on.
So the simulator comes first, and it has to be checkable. It is not a full game emulator yet: exact Neow, shop and reward odds and wider trace parity are still open, and the README says so.
What I built
- A C# combat and run engine compiled with NativeAOT to a shared library, loaded from Python through
ctypes and exposed as Gymnasium environments with action masks.
- All five character pools card by card, each character's own resource modelled, plus 250 of 296 relics, 111 monsters, 57 events, potions, shops, rest sites, map routing and Neow.
- About 3,650 C# tests, plus Python tests that cross the native boundary, where an observation field can go stale without any C# test noticing.
- An interactive terminal client that plays the same environment the agent uses, with the act map drawn in text. A wrong card or intent reads as something a player notices.
- A combat agent: determinized tree search over a cloneable combat, and a transformer with value and policy heads trained on self-play.
Decisions worth explaining
- Generated, not hand-kept
- Card, enemy, potion and relic definitions are generated from the decompiled source by a script. A hand-kept list of ids is a claim about the game that nothing rechecks.
- C# owns the state
- Deterministic game behaviour lives in the compiled core. Python is bindings and training only, and it fails fast if the loaded library is older than the C# source.
- Reading and capturing are not substitutes
- The Defect pool arrived fully read, and live capture still found three defects. The Necrobinder and Regent arrived fully captured, and reading them found 21 of 45 and 13 of 27. Both are kept.
- Unreachable is not untested
- Five of the six events an Act 1 run cannot reach were placeholders wrong in every particular. They are tested against the source anyway.
- Say what is unfinished
- The 46 relics with no implementation all belong to Act 2 and later ancients. That claim is computed by a script, not asserted, after an earlier version of it was too narrow.
- Measure the agent, then believe it
- Everything that helped was about the search or its target: lowering the exploration constant from 1.4 to 0.1 took the chance of dying in a fight from 0.183 to 0.092, and using the policy head as a search prior gained 2.0 HP. Four attempts to tell the network more about the position moved no metric at all, and the findings file records them.
Stack
Data and credit
Started from Zamiell's MIT-licensed Slay the Spire 2 emulator. Live capture uses the STS2MCP mod. Slay the Spire 2 belongs to Mega Crit, and the decompiled game source is not redistributed in the repository.