#!/usr/bin/env python3 """j160 open-loop energy measurement for the FIRING FLOOR / EXHAUSTION RAM. NO battle, NO server, NO counterfactual replay. This reads the ALREADY RECORDED closed-loop captures under /tmp and reports, per tick: * how often SELF energy sits below a floor candidate, * whether the owner's "both low, nobody firing" situation actually occurs, * who crosses a low-energy line FIRST (self or the enemy), * how often the enemy is low while we are healthy -- the opportunity the exhaustion trigger (TR_RAM_ENEMY_ENERGY) would act on. Deliberately produces NO "damage if we had not fired" number: the offline harness scored 0/6 on closed-loop questions (docs/offline_harness_trust.md), so that class of number is worthless here. Usage: python3 common_libs/tests/measure_ramfloor_energy [glob-dir] """ import json, os, glob, statistics, sys, array ROOTS = sys.argv[1:] or ["/tmp"] def recordings(): out = [] for root in ROOTS: for f in glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True): if f.endswith(".events.jsonl"): continue try: with open(f) as fh: first = fh.readline() except OSError: continue if '"closed_loop":true' not in first.replace(" ", ""): continue out.append(f) return sorted(out) def split_rounds(path): """Yield per-round [(self, enemy)] from a recording, using its round map.""" rf = path.replace(".jsonl", ".jsonl.rounds.json") bounds = [] if os.path.exists(rf): try: for r in json.load(open(rf))["rounds"]: bounds.append((r["startTick"], r["startTick"] + r["count"])) except Exception: bounds = [] rows = [] with open(path) as fh: for line in fh: if '"tick"' not in line: continue try: d = json.loads(line) except ValueError: continue if "se" in d and "ee" in d: rows.append((d["tick"], d["se"], d["ee"])) if not rows: return [] if not bounds: bounds = [(rows[0][0], rows[-1][0] + 1)] rounds = [] for s, e in bounds: r = [(se, ee) for t, se, ee in rows if s <= t < e] if not r: continue # trim the trailing both-disabled tail: a dead bot sits at ~0 forever last = max(i for i, (a, b) in enumerate(r) if a > 0 and b > 0) rounds.append(r[:last + 1]) return rounds def main(): files = recordings() rounds = [] for f in files: rounds += split_rounds(f) if not rounds: print("no closed-loop recordings found"); return N = sum(len(r) for r in rounds) se, ee = array.array("d"), array.array("d") for r in rounds: for a, b in r: se.append(a); ee.append(b) print(f"recordings={len(files)} rounds={len(rounds)} ticks={N}\n") THR = [5, 10, 15, 20, 25] print("=== A) SELF energy below a floor candidate (share of ticks) ===") print(f"{'floor':>5} {'pct':>7} {'rounds hit':>10} {'med run':>8} {'p90 run':>8} {'max run':>8}") for t in THR: tot, hit, lens = 0, 0, [] for r in rounds: cur, got = 0, False for a, _ in r: if a <= t: cur += 1; tot += 1; got = True elif cur: lens.append(cur); cur = 0 if cur: lens.append(cur) hit += 1 if got else 0 lens.sort() print(f"{t:>5} {100*tot/N:>6.2f}% {hit:>10} " f"{statistics.median(lens) if lens else 0:>8.0f} " f"{lens[int(.9*len(lens))] if lens else 0:>8} " f"{lens[-1] if lens else 0:>8}") print("\n=== B) the owner's \"both low, nobody firing\" situation ===") for t in THR: both = sum(1 for a, b in zip(se, ee) if a <= t and b <= t) sonly = sum(1 for a, b in zip(se, ee) if a <= t < b) eonly = sum(1 for a, b in zip(se, ee) if b <= t < a) print(f" both<={t:>2}: {100*both/N:6.3f}% self-only {100*sonly/N:6.2f}%" f" enemy-only {100*eonly/N:6.2f}%") print("\n=== C) who crosses a low-energy line FIRST (per round) ===") for t in [10, 15, 20, 25]: s = e = n = 0 for r in rounds: fs = next((i for i, x in enumerate(r) if x[0] <= t), None) fe = next((i for i, x in enumerate(r) if x[1] <= t), None) if fs is None and fe is None: n += 1 elif fs is None or (fe is not None and fs < fe): s += 1 else: e += 1 m = len(rounds) print(f" t={t:>2}: self-first {s:>5} ({100*s/m:5.1f}%) " f"enemy-first {e:>5} ({100*e/m:5.1f}%) neither {n:>4} ({100*n/m:4.1f}%)") print("\n=== D) \"the enemy can no longer fire\" (server rejects energy <= power) ===") for p in (0.4, 1.0, 1.95, 3.0): c = sum(1 for b in ee if b <= p) c2 = sum(1 for a, b in zip(se, ee) if b <= p and a > 20) print(f" enemy <= {p:>4}: {100*c/N:6.3f}% and self>20: {100*c2/N:6.3f}%") print("\n=== E) exhaustion-trigger OPPORTUNITY: enemy low while we are healthy ===") for t in [10, 20, 30]: row = " ".join(f"self>{fl}: {100*sum(1 for a,b in zip(se,ee) if b<=t and a>fl)/N:6.2f}%" for fl in (0, 20, 25)) print(f" enemy<={t:>2} {row}") print("\n=== F) the ALREADY-SHIPPED finisher (enemy<20 & self>enemy & dist<300) ===") c = 0 # dist needs the raw file; recompute over the whole trimmed corpus for f in files: with open(f) as fh: for line in fh: if '"se"' not in line: continue try: d = json.loads(line) except ValueError: continue if "se" not in d: continue if d["ee"] < 20 and d["se"] > d["ee"] and \ ((d["ex"]-d["sx"])**2 + (d["ey"]-d["sy"])**2) ** .5 < 300: c += 1 print(f" {c} ticks ({100*c/N:.4f}% of the trimmed corpus)") if __name__ == "__main__": main()