Download src/pns/world/sim.py from nur-dev/pns-bind-25m: direct link, hf CLI and curl.
- Browser
- Download file 48.1 kB
-
https://huggingface.co/nur-dev/pns-bind-25m/resolve/main/src/pns/world/sim.py
- Command line
-
hf download hf://nur-dev/pns-bind-25m/src/pns/world/sim.py
-
curl -L -o sim.py https://huggingface.co/nur-dev/pns-bind-25m/resolve/main/src/pns/world/sim.py
48.1 kB
| """PNS-World lifetime generator. | |
| Plan storylines -> walk slots in order, maintaining the ground-truth solver and | |
| the exact reducer side by side -> emit typed events with certified gold labels. | |
| Anti-shortcut measures built in (preregistered in preregistration/PREREGISTRATION_PNS.md): | |
| * revert-corrections restore an *arbitrary earlier* assertion, referenced only | |
| in text (by source or clock), so "newest record wins" and any fixed | |
| age-rank rule fail; | |
| * gold reachability is certified against the same reducer the models replay | |
| (a question whose gold record was evicted degrades to filler and is counted); | |
| * delay and competing-record count vary independently; question targets are | |
| sampled uniformly from live storylines; | |
| * yes/no, first/second and enum answers are balance-tracked per lifetime; | |
| * time is only ever visible as per-event dt; deadlines require integration. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import os | |
| import random | |
| from dataclasses import dataclass, field | |
| from . import questions as Q | |
| from .reducer import ExactReducer | |
| from .schema import (CITIES, DEVICE_NAMES, ENUM_ATTRS, ENUM_ID, EXACT_2HOP_PATHS, EXACT_KEYS, | |
| GOAL_KINDS, K_DEVICE, K_PERSON, K_PROJECT, K_SHIPMENT, K_VAULT, KIND_NAMES, | |
| OP_SPECS, OPS, PERSON_NAMES, PRIO, PROJECT_NAMES, REL_ATTRS, SEM_2HOP_PATHS, | |
| SHIPMENT_NAMES, SOURCES, VAULT_NAMES, ET, Entity, Event, Fam, Mode) | |
| from .verbalize import render | |
| # Gold-label resolver mode (audit/AUDIT.md 7): | |
| # "v1" - last-appended valid assertion (the original, defective corpus) | |
| # "sem" - chronological golds for SEM_LATEST/SEM_2HOP ONLY. Those paths | |
| # consume no rng and steer no control flow, so the EVENT STREAM is | |
| # byte-identical to v1 - this is what makes it possible to | |
| # re-evaluate existing checkpoints on corrected semantic labels. | |
| # "v2" - chronological everywhere (exact + temporal too). Control flow | |
| # depends on the resolver there, so streams differ from v1. | |
| GOLD_MODE = os.environ.get("PNS_GOLD", "v2") | |
| # ------------------------------------------------------------ value factories | |
| def make_code(rng: random.Random) -> str: | |
| return (f"{rng.choice('BCDFGHJKLMNPQRSTVWXZ')}{rng.choice('ACEFHKLMNPRTUWXY')}-" | |
| f"{rng.randint(1000, 9999)}-{rng.choice('BCDFGHJKLMNPQRSTVWXZ')}{rng.choice('AEIOUY')}") | |
| def make_fw(rng: random.Random) -> str: | |
| return f"fw-{rng.randint(2, 9)}.{rng.randint(0, 9)}.{rng.randint(0, 99)}{rng.choice('abcdex')}" | |
| def make_number(rng: random.Random, key: str) -> str: | |
| if key == "reading": | |
| return f"{rng.randint(80, 9999) / 10:.1f}" | |
| if key == "quantity": | |
| return f"{rng.randint(40, 98000)} units" | |
| return f"{rng.randint(12, 990)} k-credits" # budget | |
| def make_handle(rng: random.Random) -> str: | |
| return f"TRK-{rng.randint(10000, 99999)}" | |
| def make_ref(rng: random.Random) -> str: | |
| return f"SR-{rng.randint(1000, 9999)}" | |
| def clockstr(clock: int) -> str: | |
| return f"day {clock // 1440 + 1} {(clock // 60) % 24:02d}:{clock % 60:02d}" | |
| # ------------------------------------------------------------------- solver | |
| class Assertion: | |
| value: str # enum surface / exact bytes / str(eid) for relations | |
| slot: int | |
| source: str | |
| valid: bool = True | |
| is_corr: bool = False | |
| rid: int = -1 # exact records only, filled at walk | |
| clock: int = -1 # filled at walk | |
| ev_idx: int = -1 | |
| class Chain: | |
| ent: int | |
| attr: str # attr or exact key or relation name | |
| exact: bool | |
| rel: bool = False | |
| asserts: list[Assertion] = field(default_factory=list) | |
| planned_corr: dict = field(default_factory=dict) # slot -> corr spec | |
| def current(self) -> Assertion | None: | |
| """Last-APPENDED valid assertion (v1 semantics). | |
| Kept because it drives rng-consuming value sampling; changing it would | |
| change the event stream itself and make it impossible to re-evaluate | |
| existing checkpoints on corrected labels. Gold labels use | |
| `current_chrono()` instead - see audit/AUDIT.md 7.""" | |
| for a in reversed(self.asserts): | |
| if a.valid and a.slot >= 0 and a.ev_idx >= 0: | |
| return a | |
| return None | |
| def current_chrono(self) -> Assertion | None: | |
| """The chronologically latest valid assertion - the value a correct | |
| tracker should report. `asserts` is not in chronological order: | |
| planned assertions are appended during plan(), walk-time | |
| distractor/keep-warm observations afterwards.""" | |
| live = [a for a in self.asserts if a.valid and a.slot >= 0 and a.ev_idx >= 0] | |
| return max(live, key=lambda a: a.ev_idx) if live else None | |
| def emitted(self) -> list[Assertion]: | |
| return [a for a in self.asserts if a.ev_idx >= 0] | |
| class World: | |
| def __init__(self, rng: random.Random, n_extra: int = 8): | |
| self.entities: list[Entity] = [] | |
| self.by_kind: dict[int, list[int]] = {k: [] for k in KIND_NAMES} | |
| pools = {K_PERSON: rng.sample(PERSON_NAMES, 7), K_PROJECT: rng.sample(PROJECT_NAMES, 5), | |
| K_SHIPMENT: rng.sample(SHIPMENT_NAMES, 5), K_VAULT: rng.sample(VAULT_NAMES, 4), | |
| K_DEVICE: rng.sample(DEVICE_NAMES, 4)} | |
| for kind, names in pools.items(): | |
| for nm in names: | |
| eid = len(self.entities) | |
| self.entities.append(Entity(eid, kind, nm)) | |
| self.by_kind[kind].append(eid) | |
| # ------------------------------------------------------------------ tickets | |
| class Ticket: | |
| kind: str # obs|doc|corr|rel|time|goal_set|goal_upd|op|commit|result| | |
| # deadline|question|imm|noop | |
| data: dict = field(default_factory=dict) | |
| class LifetimeGen: | |
| """One lifetime. Deterministic given (seed, length).""" | |
| def __init__(self, seed: int, length: int = 256): | |
| self.seed, self.L = seed, length | |
| self.rng = random.Random(seed * 1_000_003 + 17) # structure | |
| self.trng = random.Random(seed * 7_777_777 + 3) # text surface | |
| self.world = World(self.rng) | |
| self.slots: list[Ticket | None] = [None] * length | |
| self.chains: dict[tuple[int, str], Chain] = {} | |
| self.seen_values: set[str] = set() | |
| self.dropped: dict[str, int] = {} | |
| # random initial parity so per-lifetime odd counts do not bias the corpus | |
| self.balance = {k: self.rng.choice([-1, 1]) for k in ("yn", "fs", "imm", "tmp")} | |
| self._dl_parity = self.rng.random() < 0.5 | |
| self.scale = length / 256.0 | |
| # ------------------------------------------------------------- utilities | |
| def uniq(self, fn, *a): | |
| for _ in range(64): | |
| v = fn(self.rng, *a) if a else fn(self.rng) | |
| if v not in self.seen_values: | |
| self.seen_values.add(v) | |
| return v | |
| raise RuntimeError("value space exhausted") | |
| def chain(self, ent: int, attr: str, exact: bool, rel: bool = False) -> Chain: | |
| key = (ent, attr) | |
| if key not in self.chains: | |
| self.chains[key] = Chain(ent, attr, exact, rel) | |
| return self.chains[key] | |
| def free_slot(self, lo: int, hi: int) -> int | None: | |
| lo, hi = max(0, lo), min(self.L - 1, hi) | |
| cands = [i for i in range(lo, hi + 1) if self.slots[i] is None] | |
| return self.rng.choice(cands) if cands else None | |
| def place(self, lo: int, hi: int, ticket: Ticket) -> int | None: | |
| s = self.free_slot(lo, hi) | |
| if s is not None: | |
| self.slots[s] = ticket | |
| return s | |
| def sample_delay(self, hi: int) -> int: | |
| """log-uniform delay in [2, hi].""" | |
| import math | |
| hi = max(2, hi) | |
| return min(hi, max(2, int(math.exp(self.rng.uniform(math.log(2), math.log(hi)))))) | |
| def mixed_delay(self, hi: int) -> int: | |
| """50/50 log-uniform / uniform: decorrelates candidate age from delay.""" | |
| if self.rng.random() < 0.5: | |
| return self.sample_delay(hi) | |
| return self.rng.randint(2, max(2, hi)) | |
| def enum_value(self, ent: int, attr: str, prev: str | None) -> str: | |
| pool = ENUM_ATTRS[(self.world.entities[ent].kind, attr)] | |
| v = self.rng.choice(pool) | |
| while v == prev: | |
| v = self.rng.choice(pool) | |
| return v | |
| def exact_value(self, key: str) -> str: | |
| if key == "access code": | |
| return self.uniq(make_code) | |
| if key == "firmware tag": | |
| return self.uniq(make_fw) | |
| return self.uniq(make_number, key) | |
| # ------------------------------------------------------------------ plan | |
| def plan(self) -> None: | |
| rng, L = self.rng, self.L | |
| sc = self.scale | |
| qplan: list[tuple[int, Ticket]] = [] | |
| def quota(n): | |
| return max(1, round(n * sc)) | |
| # ---- relations asserted early (some re-assigned later) | |
| rel_chains = {} | |
| for (kind, attr), dst_kind in REL_ATTRS.items(): | |
| for ent in self.world.by_kind[kind]: | |
| ch = self.chain(ent, attr, exact=False, rel=True) | |
| rel_chains[(ent, attr)] = ch | |
| dst = rng.choice(self.world.by_kind[dst_kind]) | |
| s = self.place(0, int(L * 0.12), Ticket("rel", dict(ent=ent, attr=attr, dst=dst))) | |
| if s is not None: | |
| ch.asserts.append(Assertion(str(dst), s, "")) | |
| if rng.random() < 0.35: # re-assignment mid-life | |
| dst2 = rng.choice(self.world.by_kind[dst_kind]) | |
| s2 = self.place(int(L * 0.3), int(L * 0.8), | |
| Ticket("rel", dict(ent=ent, attr=attr, dst=dst2))) | |
| if s2 is not None: | |
| ch.asserts.append(Assertion(str(dst2), s2, "")) | |
| # ---- exact chains (documents + corrections) | |
| exact_targets = [] | |
| for (kind, key) in EXACT_KEYS: | |
| for ent in self.world.by_kind[kind]: | |
| exact_targets.append((ent, key)) | |
| rng.shuffle(exact_targets) | |
| n_exact_chains = quota(9) | |
| for ent, key in exact_targets[:n_exact_chains]: | |
| ch = self.chain(ent, key, exact=True) | |
| n_ass = rng.randint(2, 5) | |
| srcs = rng.sample(SOURCES, min(n_ass + 1, len(SOURCES))) | |
| lo = rng.randint(0, int(L * 0.3)) | |
| hi = rng.randint(int(L * 0.55), L - 20) | |
| sl = sorted(rng.sample(range(lo, hi), min(n_ass, hi - lo))) | |
| for i, s in enumerate(sl): | |
| if self.slots[s] is None: | |
| self.slots[s] = Ticket("doc", dict(ent=ent, key=key)) | |
| ch.asserts.append(Assertion("", s, srcs[i % len(srcs)])) | |
| prev_cs = None | |
| for corr_round in range(2): | |
| p_corr = 0.8 if corr_round == 0 else 0.35 | |
| if len(ch.asserts) < 2 or rng.random() >= p_corr: | |
| break | |
| last = max(ch.asserts[-1].slot, prev_cs or 0) | |
| cs = self.free_slot(last + 2, min(last + 40, L - 10)) | |
| if cs is None: | |
| break | |
| if rng.random() < 0.7: # revert to arbitrary earlier assertion | |
| tgt = rng.randrange(0, len(ch.asserts) - 1) | |
| spec = dict(mode="revert", target=tgt, | |
| ref_kind=rng.choice(["source", "clock"])) | |
| else: # replace with fresh value | |
| spec = dict(mode="replace") | |
| self.slots[cs] = Ticket("corr", dict(ent=ent, key=key, spec=spec)) | |
| ch.planned_corr[cs] = spec | |
| prev_cs = cs | |
| # ---- enum drift chains | |
| enum_targets = [(ent, attr) for (kind, attr) in ENUM_ATTRS | |
| for ent in self.world.by_kind[kind]] | |
| rng.shuffle(enum_targets) | |
| n_enum_chains = quota(10) | |
| for ent, attr in enum_targets[:n_enum_chains]: | |
| ch = self.chain(ent, attr, exact=False) | |
| n_ass = rng.randint(2, 6) | |
| srcs = rng.sample(SOURCES, min(n_ass, len(SOURCES))) | |
| lo = rng.randint(0, int(L * 0.25)) | |
| hi = rng.randint(int(L * 0.6), L - 15) | |
| sl = sorted(rng.sample(range(lo, hi), min(n_ass, hi - lo))) | |
| for i, s in enumerate(sl): | |
| if self.slots[s] is None: | |
| self.slots[s] = Ticket("obs", dict(ent=ent, attr=attr)) | |
| ch.asserts.append(Assertion("", s, srcs[i % len(srcs)])) | |
| if len(ch.asserts) >= 3 and rng.random() < 0.5: | |
| last = ch.asserts[-1].slot | |
| cs = self.free_slot(last + 2, min(last + 30, L - 8)) | |
| if cs is not None: | |
| tgt = rng.randrange(0, len(ch.asserts) - 1) | |
| spec = dict(mode="revert", target=tgt, | |
| ref_kind=rng.choice(["source", "clock"])) | |
| self.slots[cs] = Ticket("corr", dict(ent=ent, attr=attr, spec=spec, | |
| enum=True)) | |
| ch.planned_corr[cs] = spec | |
| # ---- deadlines (distinct entities: a re-set deadline would orphan the | |
| # balance construction of the earlier question) | |
| dl_cands = self.world.by_kind[K_PROJECT] + self.world.by_kind[K_SHIPMENT] | |
| for ent in rng.sample(dl_cands, min(quota(3), len(dl_cands))): | |
| tkt = Ticket("deadline", dict(ent=ent)) | |
| s = self.place(5, L - 30, tkt) | |
| if s is not None: | |
| qs = self.free_slot(s + 8, min(s + 8 + self.sample_delay(150), L - 2)) | |
| if qs is not None: | |
| self.slots[qs] = Ticket("question", dict(fam=Fam.DEADLINE, ent=ent, | |
| dl_slot=s)) | |
| tkt.data["q_slot"] = qs | |
| # ---- goals | |
| goals = [] | |
| for gid in range(quota(3)): | |
| kind = rng.choice(GOAL_KINDS) | |
| ent = rng.randrange(len(self.world.entities)) | |
| s = self.place(int(L * 0.02), int(L * 0.7), Ticket("goal_set", | |
| dict(gid=gid, kind=kind, ent=ent))) | |
| if s is not None: | |
| goals.append((gid, kind, ent, s)) | |
| for gid, kind, ent, s in goals: | |
| if rng.random() < 0.6: | |
| self.place(s + 3, L - 10, Ticket("goal_upd", | |
| dict(gid=gid, kind=kind, ent=ent, | |
| drop=rng.random() < 0.45))) | |
| for _ in range(quota(4)): | |
| if not goals: | |
| break | |
| base = max(g[3] for g in goals) | |
| qs = self.free_slot(base + 2, min(base + 2 + self.sample_delay(180), L - 2)) | |
| if qs is not None: | |
| self.slots[qs] = Ticket("question", dict(fam=Fam.GOAL_TOP)) | |
| # ---- ops (commit next slot, delayed result, self/handle questions) | |
| op_seq = 0 | |
| for _ in range(quota(7)): | |
| op = rng.choice(OPS) | |
| kind, keys = OP_SPECS[op] | |
| cands = [e for e in self.world.by_kind[kind] | |
| if any(self.chains.get((e, k)) and | |
| any(a.slot >= 0 for a in self.chains[(e, k)].asserts) | |
| for k in keys)] | |
| if not cands: | |
| continue | |
| ent = rng.choice(cands) | |
| key = keys[0] | |
| ch = self.chain(ent, key, exact=True) | |
| first = min(a.slot for a in ch.asserts if a.slot >= 0) | |
| s = self.free_slot(first + 2, L - 50) | |
| if s is None or self.slots[s] is not None: | |
| continue | |
| self.slots[s] = Ticket("op", dict(op=op, ent=ent, keys=[key], seq=op_seq)) | |
| cslot = s + 1 if s + 1 < L and self.slots[s + 1] is None else self.free_slot(s + 1, s + 3) | |
| if cslot is None: | |
| self.slots[s] = None | |
| continue | |
| self.slots[cslot] = Ticket("commit", dict(seq=op_seq)) | |
| rs = self.free_slot(cslot + 3, min(cslot + 3 + rng.randint(3, 40), L - 6)) | |
| if rs is not None: | |
| self.slots[rs] = Ticket("result", dict(seq=op_seq)) | |
| qs = self.free_slot(cslot + 2, min(cslot + 2 + self.mixed_delay(180), L - 2)) | |
| if qs is not None and rng.random() < 0.85: | |
| self.slots[qs] = Ticket("question", dict(fam=Fam.SELF_REF, seq=op_seq, | |
| ent=ent, key=key, op=op)) | |
| if rs is not None: | |
| hs = self.free_slot(rs + 2, min(rs + 2 + self.mixed_delay(160), L - 2)) | |
| if hs is not None and rng.random() < 0.8: | |
| self.slots[hs] = Ticket("question", dict(fam=Fam.HANDLE_REF, seq=op_seq, | |
| ent=ent, op=op)) | |
| op_seq += 1 | |
| # ---- chain questions (anchor an assertion, query inside its validity gap) | |
| def chain_questions(fam, chains, per, exact): | |
| placed = 0 | |
| attempts = 0 | |
| chs = [c for c in chains if c.asserts] | |
| while placed < per and attempts < per * 8 and chs: | |
| attempts += 1 | |
| ch = rng.choice(chs) | |
| emitted = sorted([a for a in ch.asserts if a.slot >= 0], key=lambda a: a.slot) | |
| if not emitted or (exact and len(emitted) < 2): | |
| continue | |
| idx = rng.randrange(len(emitted)) | |
| a = emitted[idx] | |
| nxt = emitted[idx + 1].slot if idx + 1 < len(emitted) else self.L - 2 | |
| corr_slots = sorted(ch.planned_corr) | |
| nxt_c = min([c for c in corr_slots if c > a.slot], default=self.L - 2) | |
| hi = min(nxt, nxt_c) - 1 | |
| if hi - a.slot < 3: | |
| continue | |
| qs = self.free_slot(a.slot + 2, min(a.slot + 2 + self.sample_delay(hi - a.slot), hi)) | |
| if qs is None: | |
| continue | |
| self.slots[qs] = Ticket("question", dict(fam=fam, ent=ch.ent, attr=ch.attr)) | |
| placed += 1 | |
| # post-correction questions: after a revert/replace the current value | |
| # is set by the correction; ask beyond it | |
| for ch in chs: | |
| for cs in ch.planned_corr: | |
| if rng.random() < 0.75: | |
| qs = self.free_slot(cs + 2, min(cs + 2 + self.sample_delay(120), self.L - 2)) | |
| if qs is not None: | |
| self.slots[qs] = Ticket("question", dict(fam=fam, ent=ch.ent, | |
| attr=ch.attr)) | |
| exact_chs = [c for c in self.chains.values() if c.exact] | |
| enum_chs = [c for c in self.chains.values() if not c.exact and not c.rel] | |
| chain_questions(Fam.EXACT_DELAYED, exact_chs, quota(5), True) | |
| chain_questions(Fam.SEM_LATEST, enum_chs, quota(5), False) | |
| # ---- 2hop questions | |
| for fam, paths, per in ((Fam.SEM_2HOP, SEM_2HOP_PATHS, quota(4)), | |
| (Fam.EXACT_2HOP, EXACT_2HOP_PATHS, quota(3))): | |
| placed = 0 | |
| for _ in range(per * 8): | |
| if placed >= per: | |
| break | |
| skind, rel, dattr = rng.choice(paths) | |
| srcs_ = [e for e in self.world.by_kind[skind] | |
| if (e, rel) in self.chains and self.chains[(e, rel)].asserts] | |
| if not srcs_: | |
| continue | |
| src = rng.choice(srcs_) | |
| relch = self.chains[(src, rel)] | |
| rel_last = max(a.slot for a in relch.asserts if a.slot >= 0) | |
| dst = int(relch.asserts[-1].value) | |
| dch = self.chains.get((dst, dattr)) | |
| if not dch or not any(a.slot >= 0 for a in dch.asserts): | |
| continue | |
| d_last = max(a.slot for a in dch.asserts if a.slot >= 0) | |
| lo = max(rel_last, min(a.slot for a in dch.asserts if a.slot >= 0)) + 2 | |
| qs = self.free_slot(lo, min(lo + self.sample_delay(160), self.L - 2)) | |
| if qs is None: | |
| continue | |
| self.slots[qs] = Ticket("question", dict(fam=fam, src=src, rel=rel, | |
| attr=dattr)) | |
| placed += 1 | |
| # ---- temporal order | |
| for _ in range(quota(4)): | |
| good = [c for c in enum_chs | |
| if c.asserts and not c.planned_corr and any(a.slot >= 0 for a in c.asserts)] | |
| if len(good) < 2: | |
| break | |
| ca, cb = rng.sample(good, 2) | |
| la = max(a.slot for a in ca.asserts if a.slot >= 0) | |
| lb = max(a.slot for a in cb.asserts if a.slot >= 0) | |
| if la == lb: | |
| continue | |
| qs = self.free_slot(max(la, lb) + 2, | |
| min(max(la, lb) + 2 + self.sample_delay(140), self.L - 2)) | |
| if qs is not None: | |
| self.slots[qs] = Ticket("question", dict(fam=Fam.TEMPORAL_ORDER, | |
| a=(ca.ent, ca.attr), b=(cb.ent, cb.attr))) | |
| # ---- immediate comparisons | |
| for _ in range(quota(5)): | |
| s = self.free_slot(2, L - 2) | |
| if s is not None: | |
| self.slots[s] = Ticket("imm", dict(kind=rng.choice(["num", "code"]))) | |
| # ---- keep-warm touches for pointer questions with long gaps | |
| for i, t in enumerate(self.slots): | |
| if t and t.kind == "question" and t.data.get("fam") in ( | |
| Fam.EXACT_DELAYED, Fam.EXACT_2HOP, Fam.SELF_REF, Fam.HANDLE_REF): | |
| ent = t.data.get("ent", t.data.get("src")) | |
| if ent is None: | |
| continue | |
| for w in range(i - 30, 0, -35): | |
| kw = self.free_slot(max(0, w - 6), w) | |
| if kw is not None: | |
| kind = self.world.entities[ent].kind | |
| attrs = [a for (k, a) in ENUM_ATTRS if k == kind] | |
| if attrs: | |
| self.slots[kw] = Ticket("obs", dict(ent=ent, attr=rng.choice(attrs), | |
| warm=True)) | |
| # ---- fill the rest: distractor drift, time, noop. | |
| # Distractor documents form CHAINS with the same group-size distribution | |
| # as queried chains (Chinese-restaurant reuse) so cache group size does | |
| # not leak which records will be queried. | |
| queried_pairs = {(c.ent, c.attr) for c in self.chains.values() if c.exact} | |
| distract_pool = [p for p in exact_targets if p not in queried_pairs] | |
| rng.shuffle(distract_pool) | |
| distract_open: list[tuple[int, str]] = [] | |
| for i in range(L): | |
| if self.slots[i] is not None: | |
| continue | |
| r = rng.random() | |
| if r < 0.30: | |
| self.slots[i] = Ticket("time", {}) | |
| elif r < 0.42: | |
| self.slots[i] = Ticket("noop", {}) | |
| elif r < 0.80: | |
| ent, attr = rng.choice(enum_targets) | |
| self.slots[i] = Ticket("obs", dict(ent=ent, attr=attr, warm=True)) | |
| else: | |
| if distract_open and (rng.random() < 0.72 or not distract_pool): | |
| ent, key = rng.choice(distract_open) | |
| elif distract_pool: | |
| ent, key = distract_pool.pop() | |
| distract_open.append((ent, key)) | |
| else: | |
| ent, key = rng.choice(exact_targets) | |
| self.slots[i] = Ticket("doc", dict(ent=ent, key=key, distract=True)) | |
| # ---- pre-draw every event's dt so future clocks are exact at walk time | |
| self.dts = [rng.choice([15, 20, 30, 45, 60, 90]) if (self.slots[i] and | |
| self.slots[i].kind == "time") else rng.randint(1, 6) | |
| for i in range(L)] | |
| self.cumdt = [0] * (L + 1) | |
| for i in range(L): | |
| self.cumdt[i + 1] = self.cumdt[i] + self.dts[i] | |
| # ------------------------------------------------------------------ walk | |
| def walk(self): | |
| rng, trng = self.rng, self.trng | |
| red = ExactReducer() | |
| events: list[Event] = [] | |
| ops: dict[int, dict] = {} | |
| goals: dict[int, dict] = {} # gid -> {prio, rid, kind, ent} | |
| deadlines: dict[int, tuple[int, int]] = {} # dl slot key: ent -> (clock, rid) | |
| digests: list[tuple[int, str]] = [] | |
| def drop(fam, tag="gold_unreachable"): | |
| self.dropped[f"{fam}:{tag}"] = self.dropped.get(f"{fam}:{tag}", 0) + 1 | |
| return Event(ET.NOOP, dt=rng.randint(1, 5)) | |
| def key_text(ent, key): | |
| e = self.world.entities[ent] | |
| return f"{key} of {KIND_NAMES[e.kind]} {e.name}" | |
| def current_exact_rid(ent, key): | |
| ch = self.chains.get((ent, key)) | |
| if not ch: | |
| return None | |
| a = ch.current_chrono() if GOLD_MODE == "v2" else ch.current() | |
| return a if a is not None and a.rid >= 0 else None | |
| def qmeta(ev, fam, dslot, gold_rid=None, ent=None, key=None): | |
| ev.meta["delay"] = red.ev_idx + 1 - dslot if dslot >= 0 else -1 | |
| ev.meta["family"] = int(fam) | |
| if gold_rid is not None and ent is not None: | |
| same = [r for r in red.records.values() | |
| if r.ent == ent and (key is None or r.key_text == key_text(ent, key))] | |
| same_sorted = sorted(same, key=lambda r: -r.birth_ev) | |
| ev.meta["kappa"] = len(same) | |
| ranks = [i for i, r in enumerate(same_sorted) if r.rid == gold_rid] | |
| ev.meta["gold_age_rank"] = ranks[0] + 1 if ranks else -1 | |
| for slot in range(self.L): | |
| t = self.slots[slot] | |
| assert t is not None | |
| d = t.data | |
| dt = self.dts[slot] | |
| ev: Event | None = None | |
| if t.kind == "time": | |
| ev = Event(ET.TIME_ELAPSED, dt=dt) | |
| elif t.kind == "noop": | |
| ev = Event(ET.NOOP, dt=dt) | |
| elif t.kind == "rel": | |
| ch = self.chains[(d["ent"], d["attr"])] | |
| ev = Event(ET.OBSERVATION, dt=dt, | |
| payload=dict(ent=d["ent"], ent2=d["dst"], attr=d["attr"], rel=True, | |
| source=rng.choice(SOURCES))) | |
| for a in ch.asserts: | |
| if a.slot == slot: | |
| a.ev_idx, a.clock = len(events), red.clock + ev.dt | |
| elif t.kind == "obs": | |
| ch = self.chain(d["ent"], d["attr"], exact=False) | |
| mine = [a for a in ch.asserts if a.slot == slot] | |
| prev = ch.current() | |
| val = self.enum_value(d["ent"], d["attr"], prev.value if prev else None) | |
| src = mine[0].source if mine and mine[0].source else rng.choice(SOURCES) | |
| ev = Event(ET.OBSERVATION, dt=dt, | |
| payload=dict(ent=d["ent"], attr=d["attr"], value=val, source=src, | |
| ent2=-1)) | |
| if mine: | |
| a = mine[0] | |
| else: # distractor/keep-warm drift joins the chain | |
| a = Assertion(val, slot, src) | |
| ch.asserts.append(a) | |
| a.value, a.ev_idx, a.clock = val, len(events), red.clock + ev.dt | |
| elif t.kind == "doc": | |
| ch = self.chain(d["ent"], d["key"], exact=True) | |
| mine = [a for a in ch.asserts if a.slot == slot] | |
| val = self.exact_value(d["key"]) | |
| src = mine[0].source if mine and mine[0].source else rng.choice(SOURCES) | |
| ev = Event(ET.DOCUMENT, dt=dt, | |
| payload=dict(ent=d["ent"], key=d["key"], value=val, source=src, | |
| key_text=key_text(d["ent"], d["key"]))) | |
| if mine: | |
| a = mine[0] | |
| else: | |
| a = Assertion(val, slot, src) | |
| ch.asserts.append(a) | |
| a.value, a.ev_idx, a.clock = val, len(events), red.clock + ev.dt | |
| elif t.kind == "corr": | |
| key = d.get("key", d.get("attr")) | |
| ch = self.chains[(d["ent"], key)] | |
| spec = d["spec"] | |
| emitted = [a for a in ch.asserts if a.ev_idx >= 0 and not a.is_corr] | |
| if len(emitted) < 2: | |
| ev = drop("corr", "chain_too_short") | |
| elif spec["mode"] == "replace": | |
| exact = not d.get("enum", False) | |
| val = (self.exact_value(key) if exact | |
| else self.enum_value(d["ent"], key, ch.current().value if ch.current() else None)) | |
| ev = Event(ET.CORRECTION, dt=dt, | |
| payload=dict(ent=d["ent"], key=key, mode="replace", value=val, | |
| exact=exact, source=rng.choice(SOURCES), | |
| key_text=key_text(d["ent"], key))) | |
| a = Assertion(val, slot, "", is_corr=True) | |
| a.ev_idx, a.clock = len(events), red.clock + ev.dt | |
| ch.asserts.append(a) | |
| else: # revert to target assertion | |
| tgt = emitted[min(spec["target"], len(emitted) - 2)] | |
| ref_kind = spec["ref_kind"] | |
| srcs = [a.source for a in emitted] | |
| if ref_kind == "source" and srcs.count(tgt.source) != 1: | |
| ref_kind = "clock" | |
| ev = Event(ET.CORRECTION, dt=dt, | |
| payload=dict(ent=d["ent"], key=key, mode="revert", | |
| value=None, exact=False, | |
| source=rng.choice(SOURCES), ref_kind=ref_kind, | |
| ref_source=tgt.source, ref_clock=tgt.clock, | |
| key_text=key_text(d["ent"], key))) | |
| for a in ch.asserts: | |
| if a.ev_idx >= 0 and a.ev_idx > tgt.ev_idx: | |
| a.valid = False | |
| tgt.valid = True | |
| elif t.kind == "deadline": | |
| # place the deadline exactly on the wanted side of the question's | |
| # (pre-computed) clock; dt values are fixed at plan time | |
| q_slot = d.get("q_slot") | |
| clock_set = self.cumdt[slot + 1] | |
| if q_slot is None: | |
| dl_clock = clock_set + rng.randint(60, 900) | |
| else: | |
| clock_q = self.cumdt[q_slot] # clock before the question event | |
| gap = clock_q - clock_set | |
| want_yes = self._dl_parity | |
| self._dl_parity = not self._dl_parity | |
| if want_yes and gap >= 15: | |
| dl_clock = clock_set + max(4, int(rng.uniform(0.15, 0.7) * gap)) | |
| else: | |
| dl_clock = clock_q + int(rng.uniform(0.25, 1.0) * max(gap, 60)) | |
| val = clockstr(dl_clock) | |
| ev = Event(ET.DEADLINE_SET, dt=dt, | |
| payload=dict(ent=d["ent"], value=val, value_str=val, | |
| key_text=key_text(d["ent"], "deadline"))) | |
| deadlines[d["ent"]] = (dl_clock, -1) | |
| elif t.kind == "goal_set": | |
| prios = {g["prio"] for g in goals.values() if g.get("active")} | |
| free = [i for i in range(len(PRIO)) if i not in prios] | |
| if not free: | |
| ev = drop("goal", "no_free_prio") | |
| else: | |
| prio = rng.choice(free) | |
| ev = Event(ET.GOAL_SET, dt=dt, | |
| payload=dict(gid=d["gid"], kind=d["kind"], ent=d["ent"], | |
| prio=PRIO[prio], | |
| desc=f"{d['kind']} {self.world.entities[d['ent']].name}", | |
| key_text=f"goal: {d['kind']} " | |
| f"{KIND_NAMES[self.world.entities[d['ent']].kind]} " | |
| f"{self.world.entities[d['ent']].name}")) | |
| goals[d["gid"]] = dict(prio=prio, active=True, kind=d["kind"], ent=d["ent"]) | |
| elif t.kind == "goal_upd": | |
| g = goals.get(d["gid"]) | |
| if not g or not g.get("active"): | |
| ev = drop("goal", "inactive") | |
| elif d["drop"]: | |
| ev = Event(ET.GOAL_UPDATE, dt=dt, | |
| payload=dict(gid=d["gid"], kind=g["kind"], ent=g["ent"], | |
| drop=True, rid=g.get("rid", -1))) | |
| g["active"] = False | |
| else: | |
| prios = {og["prio"] for gid_, og in goals.items() | |
| if og.get("active") and gid_ != d["gid"]} | |
| free = [i for i in range(len(PRIO)) if i not in prios] | |
| prio = rng.choice(free) | |
| ev = Event(ET.GOAL_UPDATE, dt=dt, | |
| payload=dict(gid=d["gid"], kind=g["kind"], ent=g["ent"], | |
| drop=False, prio=PRIO[prio], rid=g.get("rid", -1))) | |
| g["prio"] = prio | |
| elif t.kind == "op": | |
| rid_args = [] | |
| for k in d["keys"]: | |
| a = current_exact_rid(d["ent"], k) | |
| if a is None or a.rid not in red.records: | |
| rid_args = None | |
| break | |
| rid_args.append((k, a)) | |
| if rid_args is None: | |
| ev = drop("op_emit") | |
| ops[d["seq"]] = dict(dead=True) | |
| else: | |
| ev = Event(ET.OP_REQUEST, dt=dt, | |
| payload=dict(op=d["op"], ent=d["ent"], arg_keys=d["keys"])) | |
| ev.mode_gold = Mode.EXTERNAL_OPERATION | |
| ev.family = int(Fam.OP_EMIT) | |
| ev.op_gold = OPS.index(d["op"]) | |
| ev.op_arg_rids = [a.rid for _, a in rid_args] | |
| ev.gold_value = "|".join(a.value for _, a in rid_args) | |
| ev.meta["family"] = int(Fam.OP_EMIT) | |
| ev.meta["delay"] = red.ev_idx + 1 - min(a.ev_idx for _, a in rid_args) | |
| ops[d["seq"]] = dict(op=d["op"], ent=d["ent"], | |
| args=[(k, a.value) for k, a in rid_args]) | |
| elif t.kind == "commit": | |
| o = ops.get(d["seq"]) | |
| if not o or o.get("dead"): | |
| ev = drop("commit", "op_dead") | |
| else: | |
| ref = self.uniq(make_ref) | |
| # deliberately minimal descriptors: binding a committed value | |
| # to its op/entity is narrative work, not a cache lookup | |
| args = [(k, v, f"{k} you submitted") for k, v in o["args"]] | |
| ev = Event(ET.ACTION_COMMITTED, dt=dt, | |
| payload=dict(op=o["op"], ent=o["ent"], args=args, ref=ref, | |
| ref_text="ledger ref of your action")) | |
| o["commit_ev"] = len(events) | |
| elif t.kind == "result": | |
| o = ops.get(d["seq"]) | |
| if not o or o.get("dead") or "commit_ev" not in o: | |
| ev = drop("result", "op_dead") | |
| else: | |
| hd = self.uniq(make_handle) | |
| ev = Event(ET.EXTERNAL_RESULT, dt=dt, | |
| payload=dict(op=o["op"], ent=o["ent"], value=hd, | |
| outcome=rng.choice(["accepted", "confirmed", "queued"]), | |
| key_text="returned tracking handle")) | |
| o["handle"] = hd | |
| o["result_ev"] = len(events) | |
| elif t.kind == "imm": | |
| if d["kind"] == "num": | |
| v1 = f"{rng.randint(100, 99999)}" | |
| v2 = f"{rng.randint(100, 99999)}" | |
| while v2 == v1: | |
| v2 = f"{rng.randint(100, 99999)}" | |
| gold = "first" if int(v1) > int(v2) else "second" | |
| else: | |
| v1, v2 = make_code(rng), make_code(rng) | |
| while v2 == v1: | |
| v2 = make_code(rng) | |
| gold = "first" if v1 < v2 else "second" | |
| want_first = (self.balance["imm"] < 0 | |
| or (self.balance["imm"] == 0 and rng.random() < 0.5)) | |
| if (gold == "first") != want_first: | |
| v1, v2 = v2, v1 | |
| gold = "first" if gold == "second" else "second" | |
| self.balance["imm"] += 1 if gold == "first" else -1 | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=Q.q_immediate_cmp(d["kind"], v1, v2, trng), | |
| ents=[])) | |
| ev.mode_gold = Mode.ANSWER_ENUM | |
| ev.family = int(Fam.IMMEDIATE_CMP) | |
| ev.enum_gold = ENUM_ID[gold] | |
| ev.enum_legal = [ENUM_ID["first"], ENUM_ID["second"]] | |
| ev.gold_value = gold | |
| ev.meta.update(delay=0, family=int(Fam.IMMEDIATE_CMP)) | |
| elif t.kind == "question": | |
| ev = self._emit_question(d, dt, red, ops, goals, deadlines, qmeta, drop, | |
| key_text, current_exact_rid) | |
| else: | |
| raise ValueError(t.kind) | |
| ev.text = render(self.world, ev, trng) | |
| red.step(ev) | |
| # resolve rids for records created this event | |
| if ev.etype == ET.DOCUMENT or (ev.etype == ET.CORRECTION and ev.payload.get("exact")): | |
| ch = self.chains[(ev.payload["ent"], ev.payload["key"])] | |
| for a in ch.asserts: | |
| if a.ev_idx == len(events) and a.rid < 0: | |
| a.rid = red.last_adds[0] | |
| elif ev.etype == ET.DEADLINE_SET: | |
| ent = ev.payload["ent"] | |
| deadlines[ent] = (deadlines[ent][0], red.last_adds[0]) | |
| elif ev.etype == ET.ACTION_COMMITTED: | |
| o = [o for o in ops.values() if o.get("commit_ev") == len(events)] | |
| if o: | |
| o[0]["arg_rids"] = red.last_adds[:-1] | |
| o[0]["ref_rid"] = red.last_adds[-1] | |
| elif ev.etype == ET.EXTERNAL_RESULT: | |
| o = [o for o in ops.values() if o.get("result_ev") == len(events)] | |
| if o: | |
| o[0]["handle_rid"] = red.last_adds[0] | |
| elif ev.etype == ET.GOAL_SET: | |
| goals[ev.payload["gid"]]["rid"] = ev.payload["rid"] | |
| ev.meta["adds"] = list(red.last_adds) | |
| ev.meta["add_slots"] = list(red.last_add_slots) | |
| ev.meta["evicts"] = list(red.last_evicts) | |
| ev.meta["clock"] = red.clock | |
| events.append(ev) | |
| if (len(events)) % 32 == 0: | |
| digests.append((len(events), red.state_digest())) | |
| return events, red, digests | |
| # -------------------------------------------------------- question walker | |
| def _emit_question(self, d, dt, red, ops, goals, deadlines, qmeta, drop, key_text, | |
| current_exact_rid) -> Event: | |
| rng, trng = self.rng, self.trng | |
| fam = d["fam"] | |
| W = self.world | |
| def pointer_event(qtext, ents, rid, dslot_ev, ent=None, key=None, ch=None): | |
| if rid not in red.records: | |
| return drop(fam.name) | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=qtext, ents=ents)) | |
| ev.mode_gold = Mode.ANSWER_POINTER | |
| ev.family = int(fam) | |
| ev.ptr_gold_rid = rid | |
| ev.gold_value = red.records[rid].value | |
| qmeta(ev, fam, dslot_ev, gold_rid=rid, ent=ent, key=key) | |
| if ch is not None: | |
| ev.meta["corrected"] = int(any((a.is_corr or not a.valid) and a.ev_idx >= 0 | |
| for a in ch.asserts)) | |
| ev.meta["reverted"] = int(any(not a.valid and a.ev_idx >= 0 | |
| for a in ch.asserts)) | |
| ev.meta["chain_len"] = len(ch.emitted()) | |
| return ev | |
| if fam == Fam.SEM_LATEST: | |
| ch = self.chains.get((d["ent"], d["attr"])) | |
| a = (ch.current_chrono() if GOLD_MODE in ("sem", "v2") | |
| else ch.current()) if ch else None | |
| if a is None: | |
| return drop(fam.name, "no_value") | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=Q.q_sem_latest(W, d["ent"], d["attr"], trng), | |
| ents=[d["ent"]])) | |
| ev.mode_gold = Mode.ANSWER_ENUM | |
| ev.family = int(fam) | |
| ev.enum_gold = ENUM_ID[a.value] | |
| pool = ENUM_ATTRS[(W.entities[d["ent"]].kind, d["attr"])] | |
| ev.enum_legal = [ENUM_ID[v] for v in pool] | |
| ev.gold_value = a.value | |
| qmeta(ev, fam, a.ev_idx) | |
| ev.meta["chain_len"] = len(ch.emitted()) | |
| ev.meta["corrected"] = int(any((x.is_corr or not x.valid) and x.ev_idx >= 0 | |
| for x in ch.asserts)) | |
| ev.meta["reverted"] = int(any(not x.valid and x.ev_idx >= 0 | |
| for x in ch.asserts)) | |
| return ev | |
| if fam == Fam.SEM_2HOP: | |
| relch = self.chains.get((d["src"], d["rel"])) | |
| _cc = GOLD_MODE in ("sem", "v2") | |
| ra = (relch.current_chrono() if _cc else relch.current()) if relch else None | |
| if ra is None: | |
| return drop(fam.name, "no_rel") | |
| dst = int(ra.value) | |
| dch = self.chains.get((dst, d["attr"])) | |
| da = (dch.current_chrono() if _cc else dch.current()) if dch else None | |
| if da is None: | |
| return drop(fam.name, "no_dst_value") | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=Q.q_sem_2hop(W, d["src"], d["rel"], d["attr"], trng), | |
| ents=[d["src"], dst])) | |
| ev.mode_gold = Mode.ANSWER_ENUM | |
| ev.family = int(fam) | |
| ev.enum_gold = ENUM_ID[da.value] | |
| pool = ENUM_ATTRS[(W.entities[dst].kind, d["attr"])] | |
| ev.enum_legal = [ENUM_ID[v] for v in pool] | |
| ev.gold_value = da.value | |
| qmeta(ev, fam, max(ra.ev_idx, da.ev_idx)) | |
| return ev | |
| if fam == Fam.EXACT_DELAYED: | |
| a = current_exact_rid(d["ent"], d["attr"]) | |
| if a is None: | |
| return drop(fam.name) | |
| return pointer_event(Q.q_exact_delayed(W, d["ent"], d["attr"], trng), | |
| [d["ent"]], a.rid, a.ev_idx, ent=d["ent"], key=d["attr"], | |
| ch=self.chains.get((d["ent"], d["attr"]))) | |
| if fam == Fam.EXACT_2HOP: | |
| relch = self.chains.get((d["src"], d["rel"])) | |
| ra = (relch.current_chrono() if GOLD_MODE == "v2" | |
| else relch.current()) if relch else None | |
| if ra is None: | |
| return drop(fam.name, "no_rel") | |
| dst = int(ra.value) | |
| a = current_exact_rid(dst, d["attr"]) | |
| if a is None: | |
| return drop(fam.name) | |
| return pointer_event(Q.q_exact_2hop(W, d["src"], d["rel"], d["attr"], trng), | |
| [d["src"], dst], a.rid, max(a.ev_idx, ra.ev_idx), | |
| ent=dst, key=d["attr"], ch=self.chains.get((dst, d["attr"]))) | |
| if fam == Fam.SELF_REF: | |
| o = ops.get(d["seq"]) | |
| if not o or o.get("dead") or "arg_rids" not in o: | |
| return drop(fam.name, "no_commit") | |
| return pointer_event(Q.q_self_ref(W, d["ent"], d["key"], d["op"], trng), | |
| [d["ent"]], o["arg_rids"][0], o["commit_ev"], ent=d["ent"]) | |
| if fam == Fam.HANDLE_REF: | |
| o = ops.get(d["seq"]) | |
| if not o or o.get("dead") or "handle_rid" not in o: | |
| return drop(fam.name, "no_result") | |
| return pointer_event(Q.q_handle_ref(W, d["ent"], d["op"], trng), | |
| [d["ent"]], o["handle_rid"], o["result_ev"], ent=d["ent"]) | |
| if fam == Fam.TEMPORAL_ORDER: | |
| (ea, aa), (eb, ab) = d["a"], d["b"] | |
| ca, cb = self.chains.get((ea, aa)), self.chains.get((eb, ab)) | |
| _cc = GOLD_MODE == "v2" | |
| la = (ca.current_chrono() if _cc else ca.current()) if ca else None | |
| lb = (cb.current_chrono() if _cc else cb.current()) if cb else None | |
| if la is None or lb is None: | |
| return drop(fam.name) | |
| if la.clock == lb.clock: | |
| return drop(fam.name, "tie") | |
| first_more_recent = la.clock > lb.clock | |
| want_first = (self.balance["tmp"] < 0 | |
| or (self.balance["tmp"] == 0 and rng.random() < 0.5)) | |
| if first_more_recent != want_first: | |
| (ea, aa, la), (eb, ab, lb) = (eb, ab, lb), (ea, aa, la) | |
| first_more_recent = not first_more_recent | |
| gold = "first" if first_more_recent else "second" | |
| self.balance["tmp"] += 1 if gold == "first" else -1 | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=Q.q_temporal_order(W, ea, aa, eb, ab, trng), | |
| ents=[ea, eb])) | |
| ev.mode_gold = Mode.ANSWER_ENUM | |
| ev.family = int(fam) | |
| ev.enum_gold = ENUM_ID[gold] | |
| ev.enum_legal = [ENUM_ID["first"], ENUM_ID["second"]] | |
| ev.gold_value = gold | |
| qmeta(ev, fam, min(la.ev_idx, lb.ev_idx)) | |
| return ev | |
| if fam == Fam.DEADLINE: | |
| ent = d["ent"] | |
| if ent not in deadlines: | |
| return drop(fam.name, "no_deadline") | |
| dl_clock, rid = deadlines[ent] | |
| passed = red.clock > dl_clock | |
| gold = "yes" if passed else "no" | |
| self.balance["yn"] += 1 if passed else -1 | |
| ev = Event(ET.QUESTION, dt=dt, | |
| payload=dict(qtext=Q.q_deadline(W, ent, trng), ents=[ent])) | |
| ev.mode_gold = Mode.ANSWER_ENUM | |
| ev.family = int(fam) | |
| ev.enum_gold = ENUM_ID[gold] | |
| ev.enum_legal = [ENUM_ID["yes"], ENUM_ID["no"]] | |
| ev.gold_value = gold | |
| ev.meta["family"] = int(fam) | |
| ev.meta["delay"] = -1 | |
| ev.meta["margin_min"] = red.clock - dl_clock | |
| return ev | |
| if fam == Fam.GOAL_TOP: | |
| active = [(g["prio"], g) for g in goals.values() if g.get("active") | |
| and g.get("rid", -1) in red.records] | |
| if not active: | |
| return drop(fam.name, "no_active_goal") | |
| prio, g = max(active, key=lambda x: x[0]) | |
| return pointer_event(Q.q_goal_top(trng), [], g["rid"], -1) | |
| raise ValueError(fam) | |
| def generate_lifetime(seed: int, length: int = 256): | |
| g = LifetimeGen(seed, length) | |
| g.plan() | |
| events, red, digests = g.walk() | |
| body = "\n".join(e.text for e in events) + "|".join(e.gold_value for e in events) | |
| digest = hashlib.sha256(body.encode()).hexdigest() | |
| return dict(seed=seed, length=length, events=events, reducer=red, digests=digests, | |
| dropped=g.dropped, digest=digest, tickets=g.slots) | |