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.
174 lines
6.7 KiB
Markdown
174 lines
6.7 KiB
Markdown
# 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.
|