Add design doc and research notes

DESIGN.md covers the three-layer architecture (facts in code, Jev for
tactics, gated escalation for macro). research/ documents the engine and
mod surface, the Jev classifier's measured behavior, the STS2MCP HTTP
interface, state shapes, failure modes, decision architecture, and a
run log of the first four sessions.
This commit is contained in:
0xrsydn 2026-09-21 17:09:43 +07:00
commit fb32822468
9 changed files with 2341 additions and 0 deletions

View file

@ -0,0 +1,168 @@
# 03 — STS2MCP interface
A community mod by `kunology` that exposes the running game over a local HTTP
API. Repository: `github.com/Gennadiyev/STS2MCP`, MIT.
It is the actuator for this project: it reads state and performs one action at
a time. It does **not** alter gameplay (`affects_gameplay: false`).
## Endpoints
Base URL `http://localhost:15526`, no authentication.
| Method | Path | Purpose |
|---|---|---|
| GET | `/api/v1/singleplayer?format=json\|markdown` | Read state |
| POST | `/api/v1/singleplayer` | Perform one action |
| GET | `/api/v1/multiplayer` | Read multiplayer state |
| POST | `/api/v1/multiplayer` | Multiplayer action |
| GET | `/api/v1/profile` | Persistent profile progress |
| GET | `/api/v1/compendium` | Compendium-shaped progress |
| GET | `/api/v1/wiki?query=&item_type=&limit=` | Fuzzy card/relic lookup |
| GET/POST | `/api/v1/profiles` | List / switch / delete profile slots |
All POST bodies carry an `"action"` field. Responses carry
`{"status": "ok"|"error", "message": ...}`.
## `state_type` is the action enumerator
Every response carries `state_type`, and each type has a small fixed action
set. **This is the action space.** There is no need to write one.
| `state_type` | Legal actions |
|---|---|
| `menu`, `game_over` | `menu_select` |
| `monster` / `elite` / `boss` | `play_card`, `use_potion`, `end_turn` |
| `hand_select` | `combat_select_card`, `combat_confirm_selection` |
| `rewards` | `claim_reward`, `proceed` |
| `card_reward` | `select_card_reward`, `skip_card_reward` |
| `map` | `choose_map_node` |
| `event` | `choose_event_option`, `advance_dialogue` |
| `rest_site` | `choose_rest_option`, `proceed` |
| `shop`, `fake_merchant` | `shop_purchase`, `proceed` |
| `treasure` | `claim_treasure_relic`, `proceed` |
| `card_select` | `select_card`, `confirm_selection`, `cancel_selection` |
| `bundle_select` | `select_bundle`, `confirm_bundle_selection`, `cancel_bundle_selection` |
| `relic_select` | `select_relic`, `skip_relic_selection` |
| `crystal_sphere` | `crystal_sphere_set_tool`, `_click_cell`, `_proceed` |
| `overlay`, `unknown` | none (transitions, or manual) |
`use_potion` and `discard_potion` work in any state where potions are
reachable, including outside combat.
## VERSION DRIFT — release 0.4.0 is broken on this build
### Symptom
`GET /api/v1/singleplayer` returns **HTTP 500** as soon as combat starts:
```json
{
"error": "Failed to read game state: Method not found:
'Boolean MegaCrit.Sts2.Core.Combat.CombatManager.get_IsPlayPhase()'.",
"exception_type": "System.MissingMethodException",
"stack_trace": " at STS2_MCP.McpMod.BuildBattleState(RunState, CombatRoom)"
}
```
Menu, character select, map, rewards, and events all work. **Only combat
fails** — the one state that matters most.
### Root cause
`CombatManager.IsPlayPhase` was removed in game `v0.107.1`. Verified:
```bash
grep -c IsPlayPhase sts2.xml # -> 0
```
Replacements present in `v0.107.1`:
- `MegaCrit.Sts2.Core.Combat.PlayerTurnPhase.Play` (per-player)
- `PlayerCombatState.Phase`
- `ActionSynchronizerCombatState.PlayPhase` / `.NotPlayPhase`
- `CombatManager.DebugOnlyGetState().CurrentSide == CombatSide.Player`
### Timeline
| Event | Date |
|---|---|
| Release `0.4.0` (broken) | 2026-05-05 |
| Fix commit `55e06485`, PR #123 | 2026-07-29 |
| Newer tag | none |
| CI build artifact | none |
| Fork shipping a prebuilt DLL | none (checked 12 forks) |
The fix exists in source but was never released.
### Resolution
Build from upstream `main`. The fix is present and documented in the code:
```csharp
// STS2 v0.107 removed CombatManager.IsPlayPhase. The turn phase now lives per-player on
// PlayerCombatState.Phase
internal static bool IsPlayPhase(Player? player)
{
if (!IsPlayerSideTurn()) return false;
return player?.PlayerCombatState?.Phase == PlayerTurnPhase.Play;
}
```
```bash
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"'
```
`Build succeeded. 0 Warning(s) 0 Error(s).` Result: 236,544 B DLL, replacing
the 194,560 B release. The release DLL is kept at
`vendor/STS2_MCP.dll.release-0.4.0`.
**Lesson:** the game moves faster than the mod's releases. Upstream open issues
#131 and #132 already track `v0.111` compatibility. Expect this class of break
to recur after any game update. Pin the mod build to the game build.
## Known doc drift inside the mod
`AGENTS.md` in the repo uses MCP **tool** names; `docs/raw-simplified.md` uses
the HTTP **action** names. They do not match:
| AGENTS.md | HTTP action |
|---|---|
| `combat_end_turn` | `end_turn` |
| `event_choose_option` | `choose_event_option` |
| `rewards_pick_card` | `select_card_reward` |
| `rest_choose_option` | `choose_rest_option` |
| `proceed_to_map` | `proceed` |
Trust `raw-simplified.md` and the live API for direct HTTP use.
## Hard blocker: Timeline cannot be automated
After a run ends with a pending epoch, the mod refuses:
```json
{"status":"error",
"error":"Timeline has obtained epochs that still need to be revealed manually;
not opening Timeline because this game state logs invalid unlock-state
errors when entered through automation",
"pending_epoch_ids":["NEOW_EPOCH"],
"manual_action_required":true}
```
This is a **deliberate guard**, not a bug. Entering Timeline through automation
would corrupt unlock state. A human must open the Timeline and reveal the
epoch. Until then the main menu offers only `settings` and `quit` and no run
can be started.
## Mod config
The mod writes `mods/STS2_MCP.conf`:
```json
{ "port": 15526 }
```
It also injects an **"Instant Mode"** checkbox into the game settings, which
shortens animations. Worth enabling for batch runs.