File size: 4,682 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""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 β–€β–€β–€",
    )