j160 (default-off): the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger
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.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user