fix(policy): reconcile session-owned action memory with game observations

This commit is contained in:
0xrsydn 2026-09-22 12:45:46 +07:00
commit bf41945ef9
9 changed files with 405 additions and 362 deletions

View file

@ -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)