# 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: ```json { "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.