23bce2dad5
TR_RAM_FLOOR_ENERGY (0.0 = off): at/below this self energy we start no NEW shot, holding back the reserve for a final ram exchange. Justified by the only energy gain in the game being +3*power per bullet hit LANDED, so not firing denies the enemy its only refill. Blocks only NEW shots (gunHeat already gates committed ones) and is bypassed while ramming. TR_RAM_ENEMY_ENERGY (0.0 = off): last-scanned enemy energy <= this -> ram mode. Enemy energy IS observable (ScannedBotEvent.energy, schemas.nim:306), 1-8 ticks stale. This is the shipped finisher with its energy tolerance promoted to a knob, keeping the self>enemy surplus guard because RAM_DAMAGE 0.6 applies to BOTH bots on every contact tick. Open-loop measurement (measure_ramfloor_energy, 8149 recordings / 29871 rounds / 33.8M ticks): 'both low' is COMMON (10.4% of ticks below 20, 15.1% below 25) but neither side goes low first (enemy 52.7% / us 47.3%), and the owner's literal trigger - enemy so low it cannot fire (energy <= 1.95) - is only 2.5% of ticks, 1.1% while we are healthy. Guards 136 -> 147 in test_tfil_commit_env.nim, all green. A/B PRE-REGISTERED in docs/ram_floor_exhaustion_ab.md and NOT RUN.
142 lines
5.8 KiB
Python
142 lines
5.8 KiB
Python
#!/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()
|