# STS2 Bot — Research Notes Living notes on reverse-engineering Slay the Spire 2 and driving it with a TypeSafe System One (Jev) decision model. These documents record **what was measured**, not what was assumed. Where a claim comes from a live test, the evidence is quoted. Where something is a guess, it says so. ## Contents | # | Document | Covers | |---|---|---| | 01 | [Game engine and mod surface](01-game-engine-and-mod-surface.md) | Engine, assemblies, the official mod loader, manifest schema | | 02 | [System One / Jev](02-system-one-jev.md) | What Jev is, measured latency and cost, the arithmetic failure | | 03 | [STS2MCP interface](03-sts2mcp-interface.md) | The community mod, version drift, rebuilding from source | | 04 | [State shapes](04-state-shapes.md) | Every verified JSON shape, per `state_type` | | 05 | [Failure modes](05-failure-modes.md) | Every infinite loop found live, with its fix | | 06 | [Decision architecture](06-decision-architecture.md) | The three-layer design and why the split is where it is | | 07 | [Run log](07-run-log.md) | Results per run, with seeds and outcomes | | 08 | [What actually wins](08-what-actually-wins.md) | Why the web meta is unusable, and the measured cause of 9 run losses | | 09 | [TypeSafe best practice](09-typesafe-best-practice.md) | Doc-by-doc gap analysis: what we match, what measurement killed, what is missing | | 10 | [Synthesis and next iteration](10-synthesis-and-next-iteration.md) | Game + state + primitives joined; the three rules; the planned iteration and refactor | Architecture summary lives in [`../DESIGN.md`](../DESIGN.md). ## How to add a finding 1. Prefer a measurement over an inference. Quote the command and the output. 2. Put game facts in 01/03/04, model facts in 02, bugs in 05. 3. Record the **date** and the **game build** (`v0.107.1` today). Both move. 4. When a finding is later disproved, do not delete it. Mark it superseded and say what replaced it. The wrong turn is often the useful part. 5. **Re-measure numbers before you copy them.** Test counts, latencies and run totals in these docs have gone stale more than once. The suites print their own totals; use those, not a figure quoted from an earlier session. > Worked example: a hand-off summary recorded `29` and `118` assertions. > Re-running the suites showed `50` and `131`, and the difference was two > regression sections that already existed in the files. The stale numbers > would have gone into this index unchallenged. Run the suite; do not trust > the summary. ## Environment these notes were taken on | Item | Value | |---|---| | Game build | `v0.107.1`, commit `59260271` | | Platform | macOS (arm64), Steam | | Engine | Godot 4.5.1 (.NET), runtime .NET 9.0.7 | | Mod | STS2MCP, rebuilt from upstream `main` @ `55e0648` | | Model | `jev-latest` resolving to `jev-1.13.0` | ## Rebuilding the mod `STS2MCP` release `0.4.0` is **broken on this game build**. See [03](03-sts2mcp-interface.md). To rebuild: ```bash cd ~/sts2-bot/vendor/STS2MCP nix shell nixpkgs#dotnet-sdk_9 --command bash -c ' dotnet build STS2_MCP.csproj -c Release -o out/STS2_MCP \ -p:STS2GameDir="$HOME/Library/Application Support/Steam/steamapps/common/Slay the Spire 2"' cp out/STS2_MCP/STS2_MCP.dll \ "$HOME/Library/Application Support/Steam/steamapps/common/Slay the Spire 2/SlayTheSpire2.app/Contents/MacOS/mods/" ``` Then restart the game. Mods load only at process start. ## Testing Three suites, all offline. Run them before every session. ```bash python3 test_brain.py # 131 assertions — structural / programmatic python3 test_facts.py # 50 assertions — arithmetic and parsing python3 test_run.py # 21 assertions — the decision log and its joins ``` `test_brain.py` is the important one for catching usage bugs. It asserts that every `state_type` produces an action **legal for that state**, that every action the decision layer can emit is declared somewhere, and that every fallback respects its own inputs. It needs no model and no running game. `test_run.py` drives `main()` end to end with a fake `sts2` and a stub client, so it pins the decision log without a network: a model decision's row carries that decision's answers, and a decision that asked nothing logs `jev: null` rather than inheriting the previous step's. Removing the reset before each `decide()` makes it fail. It was added after a session in which four separate infinite loops and three hardcoded fallbacks were found by hand. Most of them would have been caught here.