fix(bot): correct combat estimates and enforce client and runner failures

This commit is contained in:
0xrsydn 2026-09-22 12:45:46 +07:00
commit 3f243eaeee
10 changed files with 859 additions and 273 deletions

View file

@ -11,6 +11,7 @@ Run: python3 test_facts.py
from __future__ import annotations
import gzip
import json
import pathlib
import sys
@ -133,12 +134,18 @@ plain = card("Defend", 1, "Gain 5 Block.", ctype="Skill", target="Self")
check("non-damage card", facts.parse_card_damage(plain).raw_total, 0)
print()
print("=== 3. modifiers ===")
print("=== 3. hand descriptions include attacker modifiers ===")
strike = card("Strike", 1, "Deal 6 damage.")
check("bare strike", facts.damage_to_target(strike, [], [], 0), 6)
check("+2 Strength", facts.damage_to_target(strike, [{"name": "Strength", "amount": 2}], [], 0), 8)
strong_strike = card("Strike", 1, "Deal 8 damage.")
check("Strength is already in hand text", facts.damage_to_target(
strong_strike, [{"name": "Strength", "amount": 2}], [], 0), 8)
check("target Vulnerable (x1.5)", facts.damage_to_target(strike, [], [{"name": "Vulnerable", "amount": 2}], 0), 9)
check("attacker Weak (x0.75)", facts.damage_to_target(strike, [{"name": "Weak", "amount": 1}], [], 0), 4)
weak_strike = card("Strike", 1, "Deal 4 damage.")
check("Weak is already in hand text", facts.damage_to_target(
weak_strike, [{"name": "Weak", "amount": 1}], [], 0), 4)
check("Vulnerable rounds each hit, not the combined total", facts.damage_to_target(
multi, [], [{"name": "Vulnerable", "amount": 1}], 0), 8)
check("enemy block absorbed", facts.damage_to_target(strike, [], [], 4), 2)
check("block exceeds damage", facts.damage_to_target(strike, [], [], 10), 0)
@ -230,21 +237,73 @@ check("18 raw vs 20 hp + 5 block is not lethal", s.lethal_available, False)
check("...and nothing is listed killable", s.killable, [])
print()
print("=== 9. player statuses reach the lethal search (regression) ===")
# The executor used to call the search with `[]`, so facts could report
# `lethal_available: true` while the code meant to execute the kill found
# nothing -- Strength and Weak were invisible to it.
strong_hand = [card("Strike", 1, "Deal 6 damage.", index=n) for n in range(2)]
print("=== 9. lethal calculations use displayed damage once ===")
# Hand text already includes Strength; statuses remain available as context.
strong_hand = [card("Strike", 1, "Deal 12 damage.", index=n) for n in range(2)]
strong = observation(strong_hand, [enemy("E_0", 18)],
status=[{"name": "Strength", "amount": 6, "type": "Buff"}])
st = facts.combat_facts(strong)
check("Strength is carried on the facts", st.player_status[0]["name"], "Strength")
check("12+12 raw damage vs 18 hp is lethal", st.lethal_available, True)
check("ignoring the statuses misses it",
facts._subset_damage(st.playable, st.energy, [], st.enemies[0]), 12)
check("using them finds it",
check("displayed damage works without reapplying statuses",
facts._subset_damage(st.playable, st.energy, [], st.enemies[0]), 24)
check("passing statuses must not apply Strength twice",
facts._subset_damage(st.playable, st.energy, st.player_status, st.enemies[0]), 24)
print()
print("=== 10. captured hand text is not base damage ===")
fixture = pathlib.Path("dataset/states/06/0618279b1a19a34c32469d654d00386fa904e76fc1cf8ee0943d47469d257a49.json.gz")
captured = json.loads(gzip.decompress(fixture.read_bytes()))
cp = captured["player"]
cs = next(c for c in cp["hand"] if c["name"] == "Strike")
check("captured Strength", facts.power_amount(cp["status"], "Strength"), 2)
check("captured Strike text", cs["description"], "Deal 8 damage.")
check("captured damage without target modifiers",
facts.damage_to_target(cs, cp["status"], [], 0), 8)
cf = facts.combat_facts(observation([cs], [enemy("E_0", 9)], energy=1, status=cp["status"]))
check("8 displayed damage cannot kill 9 HP", cf.lethal_available, False)
print()
print("=== 11. individual killability does not prove joint lethal ===")
two = facts.combat_facts(observation([strike], [enemy("E_0", 6), enemy("E_1", 6)], energy=1))
check("each target is individually killable", two.killable, ["E_0", "E_1"])
check("joint lethal is unknown until a shared-resource search exists", two.lethal_available, None)
check("the model receives unknown, not a false proof", two.to_state()["combat"]["lethal_available"], None)
print()
print("=== 12. fixed-cost block optimization and covered damage ===")
block_hand = [card("Big Block", 2, "Gain 9 Block.", ctype="Skill", target="Self", index=0),
card("Small Block", 1, "Gain 6 Block.", ctype="Skill", target="Self", index=1),
card("Small Block", 1, "Gain 6 Block.", ctype="Skill", target="Self", index=2)]
bf = facts.combat_facts(observation(block_hand, [enemy("E_0", 100, intent_damage=12)], energy=2))
check("two small blocks beat the largest card", bf.max_block_available, 12)
free = card("Free Block", 0, "Gain 3 Block.", ctype="Skill", target="Self", index=3)
ff = facts.combat_facts(observation([free] + block_hand, [enemy("E_0", 100)], energy=2))
check("zero-cost block is counted once", ff.max_block_available, 15)
covered = facts.combat_facts(observation(block_hand, [enemy("E_0", 100, intent_damage=12)],
hp=30, block=12))
check("covered damage does not force block", covered.block_urgent, False)
check("covered damage does not demand more current block", covered.must_block, False)
partial = facts.combat_facts(observation([plain], [enemy("E_0", 100, intent_damage=12)], hp=10))
check("survival allows nonlethal HP loss", partial.survives_with_cards, True)
exact = facts.combat_facts(observation([plain], [enemy("E_0", 100, intent_damage=15)], hp=10))
check("zero HP is not survival", exact.survives_with_cards, False)
print()
print("=== 13. unsupported mechanics do not yield deterministic lethal lines ===")
x_card = card("Variable Attack", "X", "Deal 20 damage.")
xf = facts.combat_facts(observation([x_card], [enemy("E_0", 10)], energy=1))
check("X cost is not a free card in damage search",
facts._subset_damage(xf.playable, xf.energy, [], xf.enemies[0]), 0)
check("X cost remains visible to the model", xf.to_state()["combat"]["hand"][0]["cost"], "X")
conditional = card("Conditional", 1, "If the enemy is Vulnerable, deal 20 damage.")
cc = facts.combat_facts(observation([conditional], [enemy("E_0", 10)]))
check("conditional text is not unconditional damage",
facts._subset_damage(cc.playable, cc.energy, [], cc.enemies[0]), 0)
armored = facts.combat_facts(observation([strike], [enemy("E_0", 6,
status=[{"name": "Intangible", "amount": 1}])]))
check("unsupported target powers do not produce a killable target", armored.killable, [])
print()
print(f"=== {PASS} passed, {FAIL} failed ===")
sys.exit(1 if FAIL else 0)