feat(state): scope card evidence and policy memory by reported run identity
This commit is contained in:
parent
484551047c
commit
ba519a850e
9 changed files with 343 additions and 82 deletions
80
run.py
80
run.py
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue