"""Legacy: what survives between runs. The Warden keeps files on you. One JSON store (SCRYPT_HOME/legacy.json) holds everything that crosses run boundaries: crashes post-mortems of dead runs — fuel for /var/crash dumps, the Warden's taunts, and conscription (your dead fight for it) estate cycles + one card sealed behind a password when you die; recoverable next run via forensics on your own crash dump contraband the one card you smuggled out of a won run daemon your most-played card, installed in future sandboxes as a service you can find and arm intel the password you read in the Warden's diary (root epilogue); next run's archive puzzle reuses it wins/runs the uptime ledger; audit_level() scales director cruelty shards the Warden's distilled memory of you, persisted Everything mechanical that comes OUT of this store is bounded at load time — corrupt or hand-edited files can never mint a broken card. """ from __future__ import annotations import json import os import re from pathlib import Path from .cards import KNOWN_SIGILS, Card, Cost, CostType MAX_CRASHES = 5 MAX_STATEMENT = 120 _PRINTABLE = re.compile(r"[^\x20-\x7e]") EMPTY = { "initiated": False, # has this installation ever seen orientation? "runs": 0, "wins": 0, "crashes": [], "estate": None, "contraband": None, "daemon": None, "intel": None, "shards": [], "diary": [], # the Warden's own entries about won runs, newest last } def _store_path() -> Path: home = Path(os.environ.get("SCRYPT_HOME", "~/.scrypt")).expanduser() return home / "legacy.json" def load() -> dict: try: data = json.loads(_store_path().read_text(encoding="utf-8")) if not isinstance(data, dict): return dict(EMPTY) except (OSError, ValueError): return dict(EMPTY) out = dict(EMPTY) out.update({k: data[k] for k in EMPTY if k in data}) for key in ("crashes", "shards", "diary"): if not isinstance(out[key], list): out[key] = [] return out def save(data: dict) -> None: path = _store_path() path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2), encoding="utf-8") def clean_statement(raw: str) -> str: """The exit-interview statement: printable, trimmed, never empty.""" text = _PRINTABLE.sub("", raw).strip()[:MAX_STATEMENT].strip() return text or "(they said nothing)" def record_crash( data: dict, *, encounter: str, turn: int, deck_ids: list[str], strongest: dict | None, statement: str, cycles: int, estate_card: str | None, password: str, ) -> None: """File a dead run, and seal its estate behind the password.""" data["runs"] += 1 data["crashes"].append( { "n": data["runs"], "encounter": encounter, "turn": turn, "deck": deck_ids[:16], "strongest": strongest, "statement": clean_statement(statement), } ) data["crashes"] = data["crashes"][-MAX_CRASHES:] if cycles > 0 or estate_card: data["estate"] = { "cycles": min(cycles, 50), "card": estate_card, "password": password, } def record_win(data: dict, *, most_played: str | None, diary_password: str) -> None: data["runs"] += 1 data["wins"] += 1 if most_played: data["daemon"] = most_played data["intel"] = diary_password def audit_level(data: dict) -> int: """0 until your second win — the first victory is a pure payoff. After that the machine starts taking you seriously. Capped: the director's cruelty budget must stay bounded.""" return max(0, min(2, int(data.get("wins", 0)) - 1)) def conscript(crash: dict) -> Card | None: """The strongest process you died holding, now on the Warden's payroll. Stats are clamped no matter what the JSON says.""" raw = crash.get("strongest") if not raw: return None sigils = tuple(s for s in raw.get("sigils", ()) if s in KNOWN_SIGILS)[:1] try: power = max(1, min(4, int(raw["power"]))) health = max(1, min(6, int(raw["health"]))) except (KeyError, TypeError, ValueError): return None name = clean_statement(str(raw.get("name", "defector")))[:16] return Card( id="conscript", name=name, power=power, health=health, cost=Cost(CostType.FREE), sigils=sigils, flavor="It remembers being yours.", art=" ▄▄▄\n▐ ∅ ▌\n ▀▀▀", )