Commit graph

2 commits

Author SHA1 Message Date
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
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