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.
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
- Never trust
status: ok. Verify against the next state read. - Wait on transitions; do not stop.
- Bound every wait, and bound consecutive rejections.
- When the state repeats after a successful action, wait. When it repeats after a rejected action, try something else.
- 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
Scorequestions per axis, combined with weights in code, may beat a singleChoice. - 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.