Commit graph

35 commits

Author SHA1 Message Date
0xrsydn
5719faa05b refactor(policy): extract pure scoring and ranking functions 2026-09-23 14:56:41 +07:00
0xrsydn
de4cae1591 docs(research): propose evidence-based composite card scoring 2026-09-23 13:40:03 +07:00
0xrsydn
108fd8bf80 docs(ui): document overlay setup and verification
Describe the compact panel, module boundaries, build and installation steps, read-only limits, and manual acceptance checks. Record user-confirmed in-game readability.
2026-09-23 13:40:02 +07:00
0xrsydn
82f2c98e7e test(ui): cover decision log ingestion and attribution
Add 34 offline integration checks for partial writes, malformed records, session joins, bounded history, file rotation, and recovery without game or model connections.
2026-09-23 13:40:02 +07:00
0xrsydn
130273570b feat(ui): integrate read-only Jev decision overlay
Initialize the Godot panel, poll the decision log in the background, and preserve inspection state during updates. Use a .conf file to keep local settings separate from mod manifests.
2026-09-23 13:40:02 +07:00
0xrsydn
1e14680c39 fix(policy): unblock upgrade confirmation after card selection 2026-09-23 13:40:02 +07:00
0xrsydn
2dc711a55c feat(ui): add decision-log reader and overlay components 2026-09-23 13:40:02 +07:00
0xrsydn
c1259ecba4 docs(research): analyze character mechanics and policy coverage 2026-09-23 13:40:02 +07:00
0xrsydn
9696282110 feat(recording): link session observations proposals and action results
Capture all successful state reads with hashes and session-local IDs. Record action intent before POST, retain accepted/rejected/unknown results, and link subsequent observations. Preserve legacy feeds and finalize each invocation synchronously.
2026-09-22 15:53:14 +07:00
0xrsydn
ba519a850e feat(state): scope card evidence and policy memory by reported run identity 2026-09-22 15:18:00 +07:00
0xrsydn
484551047c refactor(policy): extract combat selection and context modules 2026-09-22 15:18:00 +07:00
0xrsydn
bf41945ef9 fix(policy): reconcile session-owned action memory with game observations 2026-09-22 15:18:00 +07:00
0xrsydn
3f243eaeee fix(bot): correct combat estimates and enforce client and runner failures 2026-09-22 15:18:00 +07:00
0xrsydn
62693618db docs(dev): add lean agent guidance and prototype hardening research 2026-09-22 15:16:33 +07:00
0xrsydn
8fae007e50 feat(dataset): migrate game history and captures into a trainable corpus
Turn the data we already have into an open, educational dataset, so the trace we
are about to start collecting has somewhere to go.

`migrate.py` produces:
  * runs.jsonl          37 runs with outcome, killer, seed and final deck
  * decisions.jsonl     1052 decisions, each with its own outcome attached
  * states_index.jsonl  346 unique observations
  * states/             content-addressed gzipped blobs

Content addressing matters: measured, only 56% of captures are unique, so 44% of
storage is duplicates. 3.28 MB raw -> 0.42 MB stored.

The card-reward rows keep the REJECTED options, so this is a ranking dataset
rather than a classification one, and the per-fight `damage_taken` /
`turns_taken` pair is the dense reward signal a combat policy is judged on.

What it deliberately does NOT do: reconstruct per-step combat state/action pairs.
The session logs record the action but not the observation, and captures exist
only for combat, so a step has a state with no action or an action with no state
-- never both. Inventing them would poison the corpus. The gap is declared in
manifest.json instead, and collect.py will close it going forward.

Integrity checking is a separate entry point (`--check-only`) because `--verify`
alone rebuilds first and so can only ever see data that is correct by
construction -- a smoke test pretending to be a check. All six invariants were
verified by deliberately breaking the dataset and confirming the checker fails.
2026-09-22 06:09:16 +07:00
0xrsydn
239a42c317 docs(research): record the defence failure mode and refresh the run log
Failure #35 is the largest defect found so far and it was invisible from the
code: 9 runs lost to one Act 1 boss, all with the same cause, all recorded in
the run files as damage taken, turns elapsed and potions spent. Recorded with
the reproduction, the fix, and the replay that verifies it.

Also adds a rule to the docs index: re-measure numbers before copying them.
Test counts, latencies and run totals in these notes have gone stale more than
once -- a hand-off summary recorded 29 and 118 assertions where the suites
actually printed 50 and 131.
2026-09-22 06:09:16 +07:00
0xrsydn
65fe1171cd docs(research): synthesis of game, state and primitives; next iteration
Join the three evidence sources that were kept apart, and extract the useful
result: each source independently decides what belongs in code and what belongs
to the model, and every serious bug we have found broke one of those rules.

  * Game mechanism  -> arithmetic and rules belong in CODE
  * Internal state  -> anything directly readable or writable belongs in CODE
  * TypeSafe        -> judgements go to the MODEL, in the primitive matching the
                       answer's shape

  * never blocked in boss fights        -> game mechanism (arithmetic left to the model)
  * card quality asked as a Noul        -> primitives (a spectrum forced into yes/no)
  * indices read from the wrong array   -> internal state

The counter-example is kept too: the Score we tried for fight-level planning was
unusable, so the rule is "match the primitive to the shape, and verify" rather
than "use Score more".

Also records the ordered plan (measurement first, then re-run, then primitives,
then deck composition), the continual-learning design, and why it is blocked:
with 0 wins in 37 runs the reward has no gradient, so training would fit the bug
rather than the game.
2026-09-22 06:09:16 +07:00
0xrsydn
f249349dd8 docs(research): TypeSafe best-practice gap analysis
Read the vendor documentation against what the bot actually does, and separate
what is measured from what is merely by construction.

Adopt structured criteria where disambiguation costs us -- measured on card
play, the structured shape picked the same card 6/6 with margin 0.425 -> 0.473,
so it is a small effect worth having at irreversible decisions, not a blanket
rewrite.

What the measurements KILLED, recorded so it is not retried:

  * A fight-level plan asked as a Score was unusable on 4 of 6 combat states,
    confidence as low as 0.01. Fight-level planning stays in code.
  * "A bigger margin means a better play" is not supported: the same question
    repeated on the same state returned 0.04 -> 0.24 and 0.36 -> 0.02. We have
    no optimal-action label, so a higher margin is evidence of noise, not skill.

Also records three correctness fixes that are independent of any model question:
enemy block counted twice in the lethal search, the executor ignoring player
statuses, and `relic_select` asking `good_relicN` while reading `relicN`.
2026-09-22 06:09:16 +07:00
0xrsydn
c00cfe78c3 docs(research): what actually wins in Slay the Spire 2
Two findings, both from measurement rather than the public guides.

1. The public STS2 meta is not usable as evidence. Deck-size targets disagree by
3x across five sources (12-18 to 25-35), and one tier list names Demon Form,
Barricade, Limit Break and Body Slam as STS2 Ironclad cards -- those are Slay the
Spire 1 cards, so that page is an STS1 list retitled. Only Offering and Break
appear on more than one list.

2. Our own run files are far better evidence, and they contain 1052 labelled
decisions. They show the bot is NOT bloating (249 picks from 1087 offers = 23%
per card, ~69% per reward), that 81% of runs die in Act 1, and that 43% die to an
Act 1 boss with `THE_KIN_BOSS` alone killing 9.

The binding constraint is combat defence, not deck composition: those Kin fights
ran 5-10 turns and cost 44-80 HP with block cards in hand the whole time.
2026-09-22 06:09:16 +07:00
0xrsydn
d676bf067e fix(eval): end each A/B session with its run, and attribute runs to arms
Two defects made the earlier A/B numbers meaningless:

  * Without `--stop-on-run-end` a session contained several runs, so runs could
    not be attributed to an arm.
  * The script then read "the last 2N runs", which silently compared runs from
    earlier experiments rather than the two arms.

Pass `--stop-on-run-end`, and attribute each run record to its arm by diffing
the history directory around each session.

Also raise the step budget and warn on a thin sample: measured, a run still
going at step 600 produces NO run record, so a short budget biases the sample
toward runs that died early -- exactly the wrong bias for this question.
2026-09-22 06:09:16 +07:00
0xrsydn
471c77b353 fix(combat): block on projected fight damage, and three lethal-search defects
Mining the 37 run files showed 81% of runs (30/37) die in Act 1, and 43% to an
Act 1 boss. THE_KIN_BOSS alone killed 9. Every one of those fights ran 5-10
turns and cost 44-80 HP -- about 10-13 a turn, with block cards in hand the
whole time. Four defects, fixed here together because they were found and
verified as one combat-correctness pass.

1. NO DEFENCE POLICY (the big one, found by mining the data)

   facts classes a hit of <=15% of max HP as THREAT_CHIP. At 80 max HP that is
   12, exactly what the boss deals, and _fallback_combat only blocked for HEAVY
   or worse while HP was HEALTHY (>60%). So at 74/80 HP the bot attacked through
   the boss's main attack and only started blocking below 48 HP.

   _jev_combat also asked a should_defend Noul on every combat turn and never
   read it -- grep -rn should_defend returned one line, the one creating it.
   Defence therefore fell to choice("Which single play best advances winning
   this fight?"), which is damage-biased: on the real Kin state Jev answered
   Bash at 0.42 confidence, below the 0.45 gate, so it fell through to the
   fallback, which also chose damage. Both paths agreed on the wrong answer.

   Blocking is arithmetic, so it is now decided in code before Jev is asked,
   using turns_to_kill, projected_incoming, affordable_loss and must_block /
   block_urgent. Measured against all 600 real combat captures, the rule changes
   8 of 68 in-play turns (11.8%) and stays silent on short fights and when
   nothing is incoming.

2. ENEMY BLOCK COUNTED TWICE IN THE LETHAL SEARCH

   `total - max(0, enemy.block) >= enemy.effective_hp` subtracts block a second
   time, because effective_hp is already hp + block. A 10 hp / 5 block enemy
   against 18 raw damage read as "not lethal" and real kills were discarded.

3. THE LETHAL EXECUTOR IGNORED PLAYER STATUSES

   `_lethal_line(f.playable, f.energy, [], enemy)` passed an empty status list,
   so facts reported lethal_available: true while the code meant to execute the
   kill found nothing. CombatFacts.player_status is now passed through.

4. RELIC_SELECT ASKED good_relicN AND READ relicN

   Every answer missed, so best_by_noul returned (None, 0.0) for every state and
   the path could only ever take the rarest relic.

Tests: 50 in test_facts.py and 131 in test_brain.py, with a regression case for
each -- a blocked enemy, a Strength-carrying player, a relic offer whose
highest-rated relic is deliberately the common one so the rarity fallback cannot
pass by accident, and the Kin turn itself.
2026-09-22 06:09:16 +07:00
0xrsydn
f59aa87fa3 fix(run): end the session with the run, and clear a parked game-over
A session was `--steps 600`, and dying did not stop it: the bot returned to the
menu and started a fresh run inside the same session. One session therefore
produced several run records, and nothing could be attributed to an experimental
arm. The A/B harness then compared "the last 2N runs", which silently included
runs from earlier experiments.

Add `--stop-on-run-end`, so one session is one run.

Two deadlocks found while making it work, both measured:

  * Breaking on `game_over` BEFORE dismissing the screen left the game parked
    there. Every later session then saw `game_over` at step 1 and stopped
    instantly -- `takes=0 skips=0` across a whole batch.
  * `preflight` could not recover from that parked screen, so it now dismisses
    it. The dismissal is not instant, hence the wait: without it the loop
    re-reads the state, still sees `game_over`, and still stops at step 1.

`preflight` also fails fast on a pending Timeline epoch, which the mod refuses
to automate. A whole A/B arm once ran with 0 decisions because of it.
2026-09-22 06:06:06 +07:00
0xrsydn
17cb02a147 feat(run): log every decision with the answers and the run outcome
`JEV_TRACE` alone cannot be joined to a decision: it records a time to the
second and the answers, with no run, step or action, so nothing in it can be
traced back to a run.

Wrap the client in `RecordingClient` so the last call's questions and answers
travel with the decision row they produced, and give each process a
`SESSION_ID` so rows from two sessions in the same second stay apart. Diff the
history directory before and after a session to attribute the run records it
produced.

What this supports is arm-level analysis: this decision belongs to this session,
and the session's outcome is the run file. It does NOT say whether an individual
answer was correct -- one run result attached to one step cannot label that
step. Per-decision accuracy needs replay, expert judgement, or ground truth the
code can verify on its own (lethal, legality, affordability).

A decision that asked nothing must carry no answers, so `client.last` is
cleared before each decide(); otherwise rows silently inherit the previous
step's answers.
2026-09-22 06:06:06 +07:00
0xrsydn
b5b5485f8a feat(jev): structured question shapes, Score primitive, per-call answer record
The documented API accepts an object or array wherever a string is accepted:
`instructions`, every Choice option description, every Score level, and the
Noul `criteria.true` / `criteria.false` entries. The client sent bare strings
everywhere and never used `criteria` on a Noul at all.

Add the builders for those shapes:

  * `ask()`        structured instructions: question, focus, inspect, compare
  * `entry()`      a description with what it covers, what it is not for, examples
  * `noul_criteria()` contrastive true/false criteria
  * `score()`      the ordered-level primitive, documented but not implemented
  * `gate_choice()` an explicit floor per Choice call site

A threshold tuned on a Noul is never reused on a Choice: the two answer different
questions and are not on a comparable scale, so each gate states its own pair
(`CHOICE_TOP_MIN`, `CHOICE_MARGIN_MIN`).

Also add `answer_record()` and the `JEV_TRACE` writer, so a decision can carry
the answers that produced it. Without a trace there are no labels for any model
decision, which blocks every question about decision quality.
2026-09-22 06:06:06 +07:00
0xrsydn
fe889a3f5c Ignore run artifacts and trace logs
deck.json, capture/live_*.json, and JSONL trace files are machine
state from bot runs, not source. Reference captures stay tracked.
2026-09-22 00:02:31 +07:00
0xrsydn
9966580d41 Add reference game-state captures
Hand-captured state snapshots (menu through hand_select) used to
build the facts layer and decision handlers. Live run dumps are
gitignored, these stay for reference.
2026-09-22 00:02:24 +07:00
0xrsydn
efd7d2e423 Add batch eval and card-skip A/B scripts
eval_batch.sh: N back-to-back sessions with per-session summaries.
ab_card_skip.sh: A/B the jev vs combined card-reward skip policy and
compare deck size and progress from the game's run history.
2026-09-22 00:02:18 +07:00
0xrsydn
41e3ea4a2b Add run loop with decision trace logging
observe -> decide -> act loop: Timeline preflight, stuck detection,
duplicate-action suppression, deck snapshot persistence. Appends one
JSON line per decision and per rejected action to
capture/decisions.jsonl for tail -f.
2026-09-22 00:02:11 +07:00
0xrsydn
bbb91e2f33 Add decision brain with tests
Per-state decision logic: Jev-first with deterministic fallbacks for
combat, card rewards, relic select, map, card select, events, rest
sites, shops, treasures, bundles, and hand select. 113 tests.
2026-09-22 00:02:04 +07:00
0xrsydn
1623377769 Add TypeSafe System One (Jev) client with call tracing
Batched question API (noul/choice/score), retrying transport, and a
margin-based confidence gate. Every call appends one JSON line of
questions and parsed answers to $JEV_TRACE when set; tracing never
raises, so it cannot break a run.
2026-09-22 00:01:57 +07:00
0xrsydn
68e946ef3a Add combat facts layer with tests
Pure computation: lethal damage lines, block values, threat model,
deck counts, affordability. Jev never does arithmetic; facts.py
computes the numbers and passes conclusions in.
2026-09-22 00:01:50 +07:00
0xrsydn
5b57fc7af3 Add STS2MCP HTTP client and state capture tool
sts2.py: thin client for the mod's localhost HTTP API, maps
state_type -> legal actions (the action space), strict
observe -> act once -> observe-again loop.
capture.py: manual state snapshot tool for building facts.py.
2026-09-22 00:01:44 +07:00
0xrsydn
0aa77a5a57 Migrate docdump tool from sts2-re
Queries the game's shipped sts2.xml (19,635 documented members) to
answer C# API questions without decompiling.
2026-09-22 00:01:37 +07:00
0xrsydn
059e8c9f3c Vendor STS2MCP mod source and 0.4.0 release DLL
Full C# source of the kunology STS2MCP fork (McpMod HTTP bridge for
game state and actions) plus the prebuilt 194,560-byte release DLL and
its manifest. The rebuilt fork DLL from docs/research/03 is not saved;
rebuild from this source if the pinned 0.4.0 breaks on a game update.
2026-09-22 00:01:30 +07:00
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