fix(policy): reconcile session-owned action memory with game observations
This commit is contained in:
parent
3f243eaeee
commit
bf41945ef9
9 changed files with 405 additions and 362 deletions
103
test_run.py
103
test_run.py
|
|
@ -108,13 +108,14 @@ class FakeSts2:
|
|||
self.actions.append((a, k))
|
||||
if self.action_error:
|
||||
raise self.action_error
|
||||
return types.SimpleNamespace(ok=self.action_ok, message="rejected" if not self.action_ok else "")
|
||||
ok = (self.action_ok[min(len(self.actions) - 1, len(self.action_ok) - 1)]
|
||||
if isinstance(self.action_ok, list) else self.action_ok)
|
||||
return types.SimpleNamespace(ok=ok, message="rejected" if not ok else "")
|
||||
|
||||
|
||||
def invoke(fake, *flags, client=None, clock=None, decide=None):
|
||||
"""Run the real loop with isolated files, no delays, and no live services."""
|
||||
callbacks = []
|
||||
brain._reset_screen_guards("test-run-reset")
|
||||
with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack:
|
||||
capdir = pathlib.Path(directory) / "capture"
|
||||
stack.enter_context(patch.object(run, "sts2", fake))
|
||||
|
|
@ -291,6 +292,104 @@ result = invoke(FakeSts2([menu, combat]), "--max-jev-errors", "2", client=client
|
|||
check("successful model call resets the failure budget", client.calls, 4)
|
||||
check("two failures after recovery abort", result.rc, 3)
|
||||
|
||||
print()
|
||||
print("=== 8. policy memory follows action results and fresh observations ===")
|
||||
# Exercise the real policy and runner together. No global reset between sessions.
|
||||
grid = {"state_type": "card_select", "card_select": {
|
||||
"screen_type": "select", "prompt": "Choose 2 cards to Remove.",
|
||||
"cards": [{"index": 0, "name": "Strike"}, {"index": 1, "name": "Defend"}],
|
||||
"can_confirm": False, "preview_showing": False}}
|
||||
confirmable = dict(grid, card_select=dict(grid["card_select"], can_confirm=True))
|
||||
# One rejected toggle, two accepted toggles, a delayed confirm, then transition.
|
||||
fake = FakeSts2([menu, grid, grid, grid, confirmable, confirmable,
|
||||
{"state_type": "overlay"}, confirmable, menu],
|
||||
action_ok=[False, True])
|
||||
result = invoke(fake, "--no-jev", "--steps", "8", "--max-duplicate-waits", "0")
|
||||
check("selection flow completes", result.rc, 0)
|
||||
check("rejected selection is retried; accepted indices are not toggled twice",
|
||||
fake.actions, [(("select_card",), {"index": 0}),
|
||||
(("select_card",), {"index": 0}),
|
||||
(("select_card",), {"index": 1}),
|
||||
(("confirm_selection",), {}),
|
||||
(("menu_select",), {"option": "singleplayer"})])
|
||||
changed_grid = dict(grid, card_select=dict(grid["card_select"],
|
||||
cards=[{"index": 0, "name": "Defend"}, {"index": 1, "name": "Strike"}]))
|
||||
fake = FakeSts2([menu, grid, changed_grid])
|
||||
result = invoke(fake, "--no-jev", "--steps", "2", "--max-duplicate-waits", "0")
|
||||
check("changed grid does not reuse the old selection indices", len(fake.actions), 1)
|
||||
for session in range(2):
|
||||
fake = FakeSts2([menu, grid])
|
||||
result = invoke(fake, "--no-jev", "--steps", "1")
|
||||
check(f"session {session + 1} starts with fresh selection memory",
|
||||
fake.actions, [(("select_card",), {"index": 0})])
|
||||
fake = FakeSts2([menu, grid])
|
||||
result = invoke(fake, "--no-jev", "--steps", "3", "--dry-run", "--max-duplicate-waits", "0")
|
||||
check("dry-run sends no selection actions", fake.actions, [])
|
||||
check("dry-run proposals never advance selection memory",
|
||||
[r["params"] for r in result.rows if r["event"] == "decide"], [{"index": 0}] * 3)
|
||||
|
||||
shop = {"state_type": "shop", "shop": {"items": [
|
||||
{"index": 0, "category": "card", "card_name": "Inflame",
|
||||
"card_description": "Gain 2 Strength.", "price": 50,
|
||||
"is_stocked": True, "can_afford": True}]}}
|
||||
sold = {"state_type": "shop", "shop": {"items": []}}
|
||||
affordability_only = dict(shop, shop={"items": [dict(shop["shop"]["items"][0], can_afford=False)]})
|
||||
fake = FakeSts2([menu, shop, shop, affordability_only, sold])
|
||||
result = invoke(fake, "--steps", "4", "--max-duplicate-waits", "0")
|
||||
check("purchase waits through unchanged inventory and unrelated state changes",
|
||||
fake.actions, [(("shop_purchase",), {"index": 0}), (("proceed",), {})])
|
||||
|
||||
bundle = {"state_type": "bundle_select", "bundle_select": {
|
||||
"bundles": [{"index": 2, "cards": []}], "preview_showing": False}}
|
||||
preview = dict(bundle, bundle_select=dict(bundle["bundle_select"],
|
||||
preview_showing=True, can_confirm=True))
|
||||
fake = FakeSts2([menu, bundle, bundle, preview, preview, menu])
|
||||
result = invoke(fake, "--no-jev", "--steps", "5", "--max-duplicate-waits", "0")
|
||||
check("bundle waits for preview and never cancels a delayed confirmation",
|
||||
[a[0][0] for a in fake.actions], ["select_bundle", "confirm_bundle_selection", "menu_select"])
|
||||
|
||||
crystal = {"state_type": "crystal_sphere", "crystal_sphere": {
|
||||
"grid_width": 3, "grid_height": 3, "clickable_cells": [{"x": 1, "y": 1}],
|
||||
"can_proceed": False}}
|
||||
revealed = dict(crystal, crystal_sphere=dict(crystal["crystal_sphere"], can_proceed=True))
|
||||
fake = FakeSts2([menu, crystal, crystal, crystal, revealed], action_ok=[False, True])
|
||||
result = invoke(fake, "--no-jev", "--steps", "4", "--max-duplicate-waits", "0")
|
||||
check("rejected crystal click stays available; accepted click waits for evidence",
|
||||
[a[0][0] for a in fake.actions],
|
||||
["crystal_sphere_click_cell", "crystal_sphere_click_cell", "crystal_sphere_proceed"])
|
||||
|
||||
characters = {"state_type": "menu", "menu_screen": "character_select", "options": ["IRONCLAD", "embark"]}
|
||||
fake = FakeSts2([menu, characters, characters, characters, characters, characters,
|
||||
{"state_type": "unknown"}, characters, menu],
|
||||
action_ok=[True, False, True])
|
||||
result = invoke(fake, "--no-jev", "--steps", "8", "--max-duplicate-waits", "0")
|
||||
check("rejected embark re-selects; accepted embark waits through transitions",
|
||||
[a[1].get("option") for a in fake.actions],
|
||||
["IRONCLAD", "embark", "IRONCLAD", "embark", "singleplayer"])
|
||||
|
||||
rewards = {"state_type": "rewards", "rewards": {"items": [{"index": 0, "type": "card"}]}}
|
||||
fake = FakeSts2([menu, card_reward, card_reward, rewards, menu, rewards], action_ok=[False, True])
|
||||
result = invoke(fake, "--steps", "5", "--max-duplicate-waits", "0")
|
||||
check("skip memory follows acceptance and clears outside the reward flow",
|
||||
[a[0][0] for a in fake.actions],
|
||||
["skip_card_reward", "skip_card_reward", "proceed", "menu_select", "claim_reward"])
|
||||
|
||||
hand = {"state_type": "hand_select", "hand_select": {
|
||||
"mode": "simple_select", "prompt": "Choose a card to Exhaust.",
|
||||
"cards": [{"index": 2, "name": "Strike"}], "can_confirm": False}}
|
||||
selected_hand = dict(hand, hand_select=dict(hand["hand_select"], can_confirm=True,
|
||||
selected_cards=[{"index": 0, "name": "Strike"}]))
|
||||
fake = FakeSts2([menu, hand, hand, selected_hand, selected_hand, menu])
|
||||
result = invoke(fake, "--no-jev", "--steps", "5", "--max-duplicate-waits", "0")
|
||||
check("hand selection waits for selected count, not the separate selected-card index",
|
||||
[a[0][0] for a in fake.actions], ["combat_select_card", "combat_confirm_selection", "menu_select"])
|
||||
|
||||
fake = FakeSts2([menu] + [dict(shop, animation_tick=i) for i in range(100)])
|
||||
result = invoke(fake, "--steps", "100", "--stuck-seconds", "10", clock=itertools.count())
|
||||
check("pending purchase times out despite unrelated observation changes",
|
||||
(result.rc, result.sessions[0]["stop_reason"], len(fake.actions)),
|
||||
(1, "pending_action_timeout", 1))
|
||||
|
||||
print()
|
||||
print(f"=== {PASS} passed, {FAIL} failed ===")
|
||||
sys.exit(1 if FAIL else 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue