feat(state): scope card evidence and policy memory by reported run identity

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

80
run.py
View file

@ -28,8 +28,7 @@ import brain
import facts as F
import sts2
from jev import JevClient, JevError, answer_record
DECK_FILE = pathlib.Path("deck.json")
from run_state import RunContext
# Where the game writes its own run records. This is the OUTCOME source: it
# carries win/loss, the killer, the seed and the final deck. Override with
@ -163,28 +162,6 @@ def decision_record(step: int, state_type: str, run_state, decision,
}
# Known starting decks, used only until the first combat exposes the real one.
# Source: the character_select state, which lists starting_deck per character.
STARTING_DECKS: dict[str, dict[str, int]] = {
"The Ironclad": {"Strike": 5, "Defend": 4, "Bash": 1},
"The Silent": {"Strike": 5, "Defend": 5, "Neutralize": 1, "Survivor": 1},
"The Regent": {"Strike": 4, "Defend": 4, "Falling Star": 1, "Venerate": 1},
}
def load_deck() -> dict | None:
if DECK_FILE.exists():
try:
return json.loads(DECK_FILE.read_text())
except (json.JSONDecodeError, OSError):
return None
return None
def save_deck(deck: dict) -> None:
DECK_FILE.write_text(json.dumps(deck, indent=2, sort_keys=True))
def observe() -> dict:
return sts2.state()
@ -243,8 +220,8 @@ def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--steps", type=int, default=20)
ap.add_argument("--dry-run", action="store_true",
help="send no action POSTs and do not update deck.json; "
"state reads, model calls, and capture logs still run")
help="send no action POSTs; "
"state/identity reads, model calls, and capture logs still run")
ap.add_argument("--no-jev", action="store_true")
ap.add_argument("--pause", type=float, default=0.6,
help="seconds to wait after each action")
@ -349,12 +326,9 @@ def main() -> int:
atexit.register(write_session_row)
# The card_reward state does not expose the deck, but combat states expose
# all four piles. Snapshot composition during combat and persist it so it
# survives a restart.
deck_snapshot: dict | None = load_deck()
if deck_snapshot:
print(f"deck snapshot loaded: {deck_snapshot}")
# Never load the old unscoped deck.json. Visible combat piles are limited
# evidence, not a persistent deck, and remain local to this session/run.
run_context = RunContext()
waits = 0
rejected = 0
last_sig: str | None = None
@ -365,7 +339,6 @@ def main() -> int:
duplicate_waits = 0
jev_errors = 0
saw_a_run = False
policy_context = brain.PolicyContext()
pending_action = None
pending_since = time.monotonic()
@ -377,6 +350,28 @@ def main() -> int:
return finish(1, "state_error")
st = obs.get("state_type")
previous_id = run_context.run_id
previous_known_id = run_context.last_known_id
identity = None
if st not in ("menu", "game_over", "unknown", "overlay"):
try:
identity = sts2.compendium().get("current_run")
except sts2.Sts2Error as exc:
trace({"step": step, "event": "run_identity_error", "error": str(exc)[:160]})
run_context.observe(obs, identity)
if run_context.run_id != previous_id:
trace({"step": step, "event": "run_identity", "run_id": run_context.run_id,
"seed": run_context.seed, "source": "compendium.current_run"})
run_changed = (previous_known_id is not None and run_context.run_id is not None
and previous_known_id != run_context.run_id)
if run_changed:
if args.stop_on_run_end:
return finish(0, "run_changed")
last_sig = last_action_key = None
same_state = rejected = duplicate_waits = 0
same_state_since = time.monotonic()
last_ok = True
policy_context = run_context.policy
# One session = one run, when asked. Otherwise the bot dies, returns to
# the menu and starts a fresh run inside the same session, so a single
@ -446,13 +441,7 @@ def main() -> int:
print(f"[{step:03d}] COMBAT {f.describe().splitlines()[0]}")
for line in f.describe().splitlines()[1:]:
print(f" {line}")
# The snapshot carries the name->count map (card identities, which
# are the synergy signal) plus stable all-pile aggregates. Only the
# counts decide whether the deck actually changed.
if f.deck_counts and f.deck_counts != (deck_snapshot or {}).get("counts"):
deck_snapshot = {"counts": f.deck_counts, "summary": f.deck_summary}
if not args.dry_run:
save_deck(deck_snapshot)
run_context.capture_combat(obs, f, step)
decision = None
decide_error = None
@ -461,7 +450,7 @@ def main() -> int:
# must not inherit the previous step's answers in the log.
client.last = None
try:
decision = brain.decide(obs, client, deck_snapshot, context=policy_context)
decision = brain.decide(obs, client, run_context.deck, context=policy_context)
# A procedural action or animation wait does not establish model
# recovery. Only a successful model response resets the budget.
if client is not None and client.last is not None:
@ -481,7 +470,7 @@ def main() -> int:
# the same failing request -- measured, a DNS blip re-raised out of
# the "fallback" and killed the session.
try:
decision = brain.decide(obs, None, deck_snapshot, context=policy_context)
decision = brain.decide(obs, None, run_context.deck, context=policy_context)
except Exception as inner: # noqa: BLE001
trace({"step": step, "event": "fallback_error", "error": str(inner)[:160]})
print(f"[{step:03d}] fallback also failed: {inner}")
@ -539,7 +528,10 @@ def main() -> int:
stats[decision.source] = stats.get(decision.source, 0) + 1
print(f"[{step:03d}] DECIDE {decision}")
trace(decision_record(step, st, obs.get("run"), decision, decide_error, client))
record = decision_record(step, st, obs.get("run"), decision, decide_error, client)
record.update(run_id=run_context.run_id, run_seed=run_context.seed,
deck_provenance=(run_context.deck or {}).get("provenance"))
trace(record)
last_action_key = action_key
if args.dry_run:
@ -553,7 +545,7 @@ def main() -> int:
"action": decision.action, "error": str(exc)[:200]})
return finish(1, "action_error")
policy_context.record_result(obs, decision, accepted=result.ok)
run_context.record_result(obs, decision, accepted=result.ok)
if not result.ok:
print(f"[{step:03d}] action rejected: {result.message}")
trace({"step": step, "event": "action_rejected",