sts2-bot/docs/research/06-decision-architecture.md
0xrsydn fb32822468 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.
2026-09-22 00:01:22 +07:00

6.7 KiB

06 — Decision architecture

The core constraint that shapes everything

Jev is a classifier. It picks one option from a set we define. It cannot plan, cannot invent actions, and cannot do arithmetic — and it reported 0.79 confidence on a wrong lethal check.

So the architecture is not "send state, get actions". It is:

game state ──▶ CODE computes facts and enumerates legal actions
           ──▶ JEV judges between the enumerated options
           ──▶ CODE gates on margin, executes exactly one action
           ──▶ re-observe

The arrow that matters is the first one. The action space is generated by code, never by the model.

Three layers

Layer Engine Responsibility Examples
Facts pure Python arithmetic, legality, thresholds lethal, threat bucket, block deficit, deck counts
Tactics Jev preference between legal options which card, which target, which node
Gate code when to trust Jev margin thresholds, fallback heuristics

Why the split is exactly here

Jev is good at semantic judgement over described options. It is bad at arithmetic, counting, multi-hop indirection, and large noisy states.

So the split puts every number in code, and every preference in Jev. The model sees conclusions:

{
  "combat": {
    "your_turn": true,
    "energy": 3,
    "your_health": "healthy",
    "incoming_threat": "chip",
    "lethal_available": false,
    "enemies_you_can_kill_now": "none",
    "enemies": [{"id": "NIBBIT_0", "name": "Nibbit", "hp_state": "healthy",
                 "incoming": 12, "intends": "This enemy intends to Attack for 12 damage."}],
    "hand": [{"index": 2, "name": "Bash", "cost": 2, "type": "Attack",
              "targets": "AnyEnemy", "text": "Deal 8 damage. Apply 2 Vulnerable."}]
  }
}

Note your_health: "healthy" and incoming_threat: "chip" — buckets, not numbers to compare. Note lethal_available: false — a conclusion computed by facts.py.

Decision precedence in combat

1. CODE: lethal proven by facts.py?  -> execute the lethal line
2. CODE: no play phase?              -> wait
3. JEV : which play is best?         -> act if margin is sufficient
4. CODE: fallback heuristic          -> act

Step 1 never consults the model. That is the direct consequence of the arithmetic failure: facts.py proves lethal and a deterministic search executes it. Across 759 loop steps, 33 lethal lines were executed by code and Jev was never asked "can I kill this".

Step 4 exists because a low-confidence answer must not become a guess. The fallback is a documented heuristic adapted from the STS2MCP strategy notes.

Current coverage

Decision Jev Fallback Status
Combat: card play + target yes heuristic working
Combat: potion use yes, plus a hard-need override spend when lethal working
Map pathing yes HP / gold heuristic working
Card reward yes (with skip) take first falls back often
Card select: upgrade/transform/remove yes select first working
Event option yes, stricter gate keyword safety net working
Relic select yes take first shape unverified
Shop purchase yes (re-ranking) leave working
Treasure relic yes when >1 offered take first working
Bundle select yes first bundle working
In-combat exhaust/discard select no Strikes, then Defends heuristic
Rest site: heal vs upgrade no HP < 60% → heal heuristic
Crystal sphere no skip gap

Combat now issues three kinds of action: play_card, use_potion, and end_turn.

Potion precedence, and why it is split

1. CODE: lethal by cards?                     -> play the lethal line
2. CODE: not play phase?                      -> wait
3. CODE: incoming hit lethal AND no card can prevent it
                                              -> spend a potion (no model)
4. JEV : is a potion worth spending now?      -> use it if confident
5. JEV : which play is best?                  -> play it
6. CODE: fallback heuristic                   -> act

Step 3 exists because Jev answered the soft potion question at 0.61, just under the 0.65 Noul floor, on a state where the next hit was lethal and no card could prevent it. Delegating that decision would have lost the run. Code decides that a potion must be spent; Jev decides which.

The gaps cluster in resource spending — potions, gold, and one-shot card effects. That is the category that decides boss fights. Run 1 died to Vantom holding all three potions, which is a direct consequence of the potion gap.

Batching

All questions for one state go in one call. Questions are evaluated in parallel, and measured latency barely moves with question count:

Request Time
1 short question 0.73 s
3 questions, full combat state 0.90 s

So adding a potion question to the combat call costs no extra latency. A question that is only sometimes relevant is still worth asking (speculative fan-out); the code ignores answers it does not need.

Questions in one call are independent. If Q2 needs Q1's answer, use a second call.

The closed loop

observe -> decide -> execute ONE action -> observe again

Strictly closed, because playing a card removes it from hand and shifts every later index. An action list computed up front would be wrong after the first play. The same applies to reward lists, which re-index on every claim.

Confidence gating

Answer type Gate
Choice top >= 0.45 and (top - runner_up) >= 0.20
Noul abs(noul - 0.5) >= 0.15
Score confidence >= threshold

Never gate a Choice on confidence alone. It measures peakedness and therefore falls as the option count rises.

Robustness rules

  1. Never trust status: ok. Verify against the next state read.
  2. Wait on transitions; do not stop.
  3. Bound every wait, and bound consecutive rejections.
  4. When the state repeats after a successful action, wait. When it repeats after a rejected action, try something else.
  5. Claim and select collections right-to-left when they re-index.

Open questions

  • Can a System One policy clear an act boss at all, or does the macro layer need escalation to a reasoning model? Run 1 reached floor 16, so the plateau is now measurable rather than assumed.
  • Card reward fell back to "take the first" several times on thin margins. Composite Score questions per axis, combined with weights in code, may beat a single Choice.
  • Deck tracking is currently a composition snapshot persisted to deck.json, refreshed from combat states. It will drift if cards are removed outside combat without an intervening fight.