refactor(policy): extract combat selection and context modules
This commit is contained in:
parent
bf41945ef9
commit
484551047c
9 changed files with 1247 additions and 1167 deletions
|
|
@ -17,6 +17,7 @@ from __future__ import annotations
|
|||
import sys
|
||||
|
||||
import brain
|
||||
from policy import combat as combat_policy, selection
|
||||
import facts
|
||||
import sts2
|
||||
|
||||
|
|
@ -306,26 +307,26 @@ print()
|
|||
print("=== 3. fallbacks must respect their inputs ===")
|
||||
|
||||
# Removal must not target an upgraded card while a plain Strike exists.
|
||||
from_brain = brain.card_select_fallback(
|
||||
from_brain = selection.card_select_fallback(
|
||||
[card("Strike+", 0, upgraded=True), card("Strike", 1), card("Bash", 2)],
|
||||
"remove",
|
||||
)
|
||||
check("remove prefers an un-upgraded Strike", from_brain["name"], "Strike")
|
||||
|
||||
# Upgrade must not target a basic card while a real card exists.
|
||||
from_brain = brain.card_select_fallback(
|
||||
from_brain = selection.card_select_fallback(
|
||||
[card("Strike", 0), card("Demon Form", 1, rarity="Rare")], "upgrade"
|
||||
)
|
||||
check("upgrade prefers a non-basic card", from_brain["name"], "Demon Form")
|
||||
|
||||
# Upgrade must never target an already-upgraded card.
|
||||
from_brain = brain.card_select_fallback(
|
||||
from_brain = selection.card_select_fallback(
|
||||
[card("Strike", 0, upgraded=True), card("Defend", 1)], "upgrade"
|
||||
)
|
||||
check("upgrade skips an already-upgraded card", from_brain["name"], "Defend")
|
||||
|
||||
# Card reward fallback must prefer rarity, not index 0.
|
||||
from_brain = brain.best_by_rarity(
|
||||
from_brain = selection.best_by_rarity(
|
||||
[card("Common A", 0), card("Rare B", 1, rarity="Rare"), card("Uncommon C", 2, rarity="Uncommon")]
|
||||
)
|
||||
check("card reward fallback takes the rarest", from_brain["name"], "Rare B")
|
||||
|
|
@ -658,27 +659,27 @@ check("even with jev, no enemies means wait", d.action, "__wait__")
|
|||
# with NO preview, and can_confirm is true from the start. Measured live: the
|
||||
# handler re-selected index 10 forever because preview_showing stayed false.
|
||||
check("raw class name normalises to upgrade",
|
||||
brain.screen_kind("NDeckEnchantSelectScreen"), "upgrade")
|
||||
selection.screen_kind("NDeckEnchantSelectScreen"), "upgrade")
|
||||
check("friendly names still work",
|
||||
(brain.screen_kind("remove"), brain.screen_kind("transform")), ("remove", "transform"))
|
||||
(selection.screen_kind("remove"), selection.screen_kind("transform")), ("remove", "transform"))
|
||||
|
||||
# MULTI-select: "Choose 5 cards to Remove" keeps can_confirm FALSE until all
|
||||
# five are picked, and card_select has no `selected_cards` field. Measured live:
|
||||
# the handler re-selected the same index forever and stalled.
|
||||
check("the required count is parsed from the prompt",
|
||||
brain.card_select_need("Choose 5 cards to Remove."), 5)
|
||||
selection.card_select_need("Choose 5 cards to Remove."), 5)
|
||||
check("single-select defaults to 1",
|
||||
brain.card_select_need("Choose a card to Upgrade."), 1)
|
||||
selection.card_select_need("Choose a card to Upgrade."), 1)
|
||||
# The number is not always immediately followed by "cards":
|
||||
# "Choose 2 Common Cards to Add to Your Deck." has "Common" in between. A
|
||||
# stricter pattern returned 1 and stalled the screen waiting for a confirm.
|
||||
check("the count survives an adjective",
|
||||
brain.card_select_need("Choose 2 Common Cards to Add to Your Deck."), 2)
|
||||
selection.card_select_need("Choose 2 Common Cards to Add to Your Deck."), 2)
|
||||
check("an ADD screen is not an upgrade screen",
|
||||
brain.screen_kind("simple_select", "Choose 2 Common Cards to Add to Your Deck."),
|
||||
selection.screen_kind("simple_select", "Choose 2 Common Cards to Add to Your Deck."),
|
||||
"add")
|
||||
check("a REMOVE prompt beats a generic screen_type",
|
||||
brain.screen_kind("select", "Choose 5 cards to Remove."), "remove")
|
||||
selection.screen_kind("select", "Choose 5 cards to Remove."), "remove")
|
||||
|
||||
# Crystal Sphere: can_proceed is FALSE until tiles are revealed. The old
|
||||
# unconditional crystal_sphere_proceed was rejected and stalled the run.
|
||||
|
|
@ -703,9 +704,9 @@ check("crystal sphere proceeds when unlocked", d3.action, "crystal_sphere_procee
|
|||
# "Choose a card to Exhaust." is mode simple_select with can_confirm TRUE and
|
||||
# one card already chosen. Measured live: the handler only confirmed for
|
||||
# upgrade_select, so it toggled between two cards forever.
|
||||
check("a singular prompt needs 1", brain.hand_select_need("Choose a card to Exhaust."), 1)
|
||||
check("a counted prompt needs N", brain.hand_select_need("Choose 2 cards to discard."), 2)
|
||||
check("\"any number\" is unbounded", brain.hand_select_need("Choose any number of cards to replace."), None)
|
||||
check("a singular prompt needs 1", selection.hand_select_need("Choose a card to Exhaust."), 1)
|
||||
check("a counted prompt needs N", selection.hand_select_need("Choose 2 cards to discard."), 2)
|
||||
check("\"any number\" is unbounded", selection.hand_select_need("Choose any number of cards to replace."), None)
|
||||
|
||||
d = brain.decide(
|
||||
{"state_type": "hand_select",
|
||||
|
|
@ -805,7 +806,7 @@ kin_hand = [
|
|||
|
||||
def blk(obs):
|
||||
"""The block value of whatever combat_decision picks, or None."""
|
||||
d = brain.combat_decision(facts.combat_facts(obs), None)
|
||||
d = combat_policy.combat_decision(facts.combat_facts(obs), None)
|
||||
if d.action != "play_card":
|
||||
return d.action
|
||||
return facts.block_value(facts.combat_facts(obs).hand[d.params["card_index"]])
|
||||
|
|
@ -826,16 +827,16 @@ check("nothing incoming does not force a block", blk(quiet), 0)
|
|||
# Defense must never pre-empt lethal.
|
||||
lethal = combat(hand=kin_hand + [card("Bludgeon", index=4, cost="3", desc="Deal 32 damage.")],
|
||||
enemies=[enemy("KIN_0", hp=20, intent=12)], hp=74)
|
||||
d = brain.combat_decision(facts.combat_facts(lethal), None)
|
||||
d = combat_policy.combat_decision(facts.combat_facts(lethal), None)
|
||||
check("lethal still beats blocking",
|
||||
facts.combat_facts(lethal).hand[d.params["card_index"]]["name"], "Bludgeon")
|
||||
|
||||
# The dead question must stay dead. Assert on the QUESTION being built, not
|
||||
# on the string: the replacement comment legitimately names it.
|
||||
check("the should_defend question is no longer asked",
|
||||
'questions["should_defend"]' in open("brain.py").read(), False)
|
||||
'questions["should_defend"]' in open("policy/combat.py").read(), False)
|
||||
check("...and nothing reads it either",
|
||||
"response.get(\"should_defend\")" in open("brain.py").read(), False)
|
||||
"response.get(\"should_defend\")" in open("policy/combat.py").read(), False)
|
||||
|
||||
print()
|
||||
print("=== relic_select reads the ids it asked for (regression) ===")
|
||||
|
|
@ -859,7 +860,7 @@ relic_obs = {
|
|||
}
|
||||
relic_stub = StubClient(noul=0.30,
|
||||
noul_override={"good_relic0": 0.80, "good_relic1": 0.40})
|
||||
d = brain.relic_select_decision(relic_obs, relic_stub)
|
||||
d = selection.relic_select_decision(relic_obs, relic_stub)
|
||||
check("relic_select acts on Jev's answer", d.source, "jev")
|
||||
check("...and takes the relic Jev rated highest, not the rarest",
|
||||
d.params.get("index"), 0)
|
||||
|
|
@ -875,7 +876,7 @@ blocked = combat(hand=[card(index=n) for n in range(3)],
|
|||
bf = facts.combat_facts(blocked)
|
||||
check("18 raw vs 10 hp + 5 block is lethal", bf.lethal_available, True)
|
||||
check("...and the executor finds the line",
|
||||
bool(brain._lethal_line(bf.playable, bf.energy, bf.player_status, bf.enemies[0])),
|
||||
bool(combat_policy._lethal_line(bf.playable, bf.energy, bf.player_status, bf.enemies[0])),
|
||||
True)
|
||||
|
||||
strong = combat(hand=[card(index=n, desc="Deal 12 damage.") for n in range(2)],
|
||||
|
|
@ -883,11 +884,11 @@ strong = combat(hand=[card(index=n, desc="Deal 12 damage.") for n in range(2)],
|
|||
status=[{"name": "Strength", "amount": 6, "type": "Buff"}])
|
||||
sf = facts.combat_facts(strong)
|
||||
check("the executor uses Strength-adjusted hand text",
|
||||
[c["name"] for c in (brain._lethal_line(sf.playable, sf.energy,
|
||||
[c["name"] for c in (combat_policy._lethal_line(sf.playable, sf.energy,
|
||||
sf.player_status, sf.enemies[0]) or [])],
|
||||
["Strike", "Strike"])
|
||||
check("...and does not need to add Strength again",
|
||||
bool(brain._lethal_line(sf.playable, sf.energy, [], sf.enemies[0])), True)
|
||||
bool(combat_policy._lethal_line(sf.playable, sf.energy, [], sf.enemies[0])), True)
|
||||
|
||||
print()
|
||||
print("=== correctness: shared damage and block plans ===")
|
||||
|
|
@ -897,7 +898,7 @@ adjusted = combat(hand=[card(index=5, desc="Deal 8 damage.")],
|
|||
status=[{"name": "Strength", "amount": 2}])
|
||||
af = facts.combat_facts(adjusted)
|
||||
check("executor does not invent a Strength-based lethal line",
|
||||
brain._lethal_line(af.playable, af.energy, af.player_status, af.enemies[0]), None)
|
||||
combat_policy._lethal_line(af.playable, af.energy, af.player_status, af.enemies[0]), None)
|
||||
|
||||
defenders = [card("Big Block", index=5, cost="2", desc="Gain 9 Block.", ctype="Skill", target="Self"),
|
||||
card("Small Block", index=8, desc="Gain 6 Block.", ctype="Skill", target="Self"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue