A Slay the Spire 2 mod that exposes live game state and accepts normal-play gameplay
actions over a local WebSocket (ws://localhost:15527), plus a TypeScript/Node CLI that
drives the game from a terminal.
- Scope: read full state + take legal in-game actions (play card, use potion, end turn, navigate, claim rewards, events, shop, rest sites, selection, run/menu management). No god-mode/debug mutation of HP/gold/energy/deck/relics.
- Build target: Slay the Spire 2 v0.107.1 (Windows-native, .NET 9.0.7, Godot 4).
I built this to test out the Oh-My-Pi coding harness and experiment with the latest GLM 5.2 model from z.ai. This took about 800k worth of tokens to get functional, but I'm pretty impressed with how the agent learned during the troubleshooting process. I learned a lot about session context management and setting clear concise goals when talking with the agent.
I originally thought about wiring this up to a twitch stream to provide a TwitchPlays Slay The Spire 2. However, in the course of researching planning material I came across a fully featured MCP and twitch channel already doing this. If anything, I may vibe code a webUI front-end so that I can play from my phone browser.
Another idea was to take the data from SpireBird and see if that information can be used to train a model to be 'quantifiably good' at Slay The Spire. However, I don't have the tokens to burn currently and model training/tuning is not something I understand quite yet.
| Scope | State |
|---|---|
WS server + get_state (menu/combat/all screens) |
✅ verified |
Combat: play_card, use_potion, end_turn + energy check |
✅ verified |
| Full map grid, enemy intents, card descriptions/cost, power stacks | ✅ verified |
| Nav/reward/event/shop/rest/treasure/relic + card_select/confirm (incl. potion overlays during combat) | ✅ verified |
cancel_card_select — go back from a card-select/confirm screen (smith upgrade, transform, removal, …) |
🔧 built, needs runtime verify |
Menu/run: start_new_game, abandon, return_to_main_menu, continue_game |
✅ verified |
Pause detection + save_and_quit (soft rollback) |
✅ verified |
| Deck/draw/discard/exhaust pile inspection | ✅ verified |
discard_potion (engine DiscardPotionGameAction) — CLI discardpotion, TUI d |
🔧 built, needs runtime verify |
| Incoming-damage turn summary (Σ intent damage − block) — CLI + TUI | ✅ client-side, unit-tested |
Mid-combat curse surfacing (run.player.curses diff) — mod + CLI + TUI |
🔧 built, needs runtime verify |
TUI pile-view (v) modal viewport clipping |
✅ regression-tested |
| TypeScript CLI (REPL + ASCII map + combat/pile renderers) | ✅ typechecks clean |
Selection-mode combat actions (combat_select_card, etc.) |
❌ not implemented |
Everything builds in rootless Podman. The only host install is Podman itself; the .NET 9 SDK and Node come from the images. The game DLLs are mounted read-only; the only host write is the repo (build outputs).
# From the sts2-ws-control\ repo root:
pwsh .\podman-build.ps1 # build mod + CLI in the container, stage build/mods/
pwsh .\install.ps1 # copy staged {dll,json} -> <game>\mods\STS2_WSControl\ (only host write outside repo)Or build the mod directly (fastest iteration, no Node layer):
podman run --rm `
-v "${PWD}:/work" `
-v "C:\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\data_sts2_windows_x86_64:/game/dlls:ro" `
mcr.microsoft.com/dotnet/sdk:9.0 `
dotnet build /work/mod/STS2_WSControl.csproj -c Release -p:Sts2DataDir=/game/dlls -p:GameDir=/gameLaunch the game, enable STS2 WebSocket Control in the mod manager, agree to load mods.
On success the log shows [STS2 WebSocket Control] server started on ws://localhost:15527/.
scripts\ws-probe.ps1 is a pure-.NET WebSocket client (pwsh 7):
pwsh .\scripts\ws-probe.ps1 # get_state
pwsh .\scripts\ws-probe.ps1 -Action play_card -ArgsJson '{"card_index":0,"target":"1"}'
pwsh .\scripts\ws-probe.ps1 -Action end_turnpodman run --rm -v "${PWD}:/work" -w /work/cli node:22 sh -c "npm install && npm run dev"If mise is installed navigate to /cli and run mise run dev
The mod binds loopback (localhost/127.0.0.1). A CLI running in a container cannot
reach the host's loopback by default (rootless Podman + the podman-machine WSL2 boundary).
For now, drive the game from the host (pwsh ws-probe.ps1, or run the CLI on a host with
Node). To run the CLI in-container later, rebind the mod to http://+:15527/ and connect via
host.containers.internal — a documented plan delta.
A full-screen terminal client in tui/, built with Bubble Tea
and lipgloss. It auto-reconnects, polls get_state,
renders every screen (combat with intents + HP bars, the map grid, rewards, shop, events, rest
sites, relic/treasure selects, pause), and drives the game from the keyboard. Like the TS CLI it
runs on the host to reach the mod's loopback socket.
cd tui
# build a Windows binary via the container toolchain (no host Go install):
podman run --rm -v "${PWD}:/work" -w /work docker.io/library/golang:1.24 `
sh -c "GOOS=windows GOARCH=amd64 go build -o sts2-tui.exe ."
.\sts2-tui.exe # connects to ws://localhost:15527/ (override with -url)With Go 1.24+ on the host: go build / go test ./... from tui/. Press ? in-app for the
full key reference; see tui/README.md.
JSON, one message per WS text frame:
client → server : { "type":"request", "id":int, "action":string, "args":{…}? }
server → client : { "type":"response", "id":int, "ok":bool, "result":{…} | "error":string }
get_state returns { state_type, run?, combat?, … }. state_type ∈ menu | monster | elite
| boss | hand_select | combat_rewards | card_reward | map | rest_site | shop | event |
card_select | relic_select | treasure | game_over | overlay. Poll-based: call get_state,
act, re-get_state.
The mod is implemented against the game's own public API, verified by reflecting over the
shipped sts2.dll (scripts\probe-api.ps1 / probe-types.ps1) and the XML doc. Key confirmed
entry points: RunManager.Instance / .DebugOnlyGetState(), CombatManager.Instance.DebugOnlyGetState(),
LocalContext.GetMe(...), and combat dispatch via PlayCardAction/UsePotionAction/
EndPlayerTurnAction enqueued to RunManager.Instance.ActionQueueSet.
mod/ the .NET 9 mod (STS2_WSControl.{csproj,json} + WsControlMod*.cs + Protocol.cs)
cli/ TypeScript CLI (src/{protocol,client,format,repl,index}.ts)
tui/ Go + Bubble Tea full-screen TUI client (main.go, client.go, model.go, view.go)
scripts/ probe-api.ps1, probe-types.ps1, ws-probe.ps1 (reflection + smoke-test tools)
Containerfile, compose.yaml, podman-build.ps1, install.ps1 container build/install
archive/ superseded planning docs (PLAN, TODOS, etc.)
