64e23e29d1
Decisive measurement for the j160 firing floor, on the recorded closed-loop corpus (8149 recordings / 35163 rounds / 34.46M ticks, state only): * 61.2% of rounds end with self energy crossing 0. Energy at death: median 0.83, p90 8.90, max 24.83 -- the bot dies BROKE, so the floor's premise is real. Time at energy<=0 is a median of 1 tick: the round ends on the crossing tick, there is no recoverable disabled window. * Reserve that would have absorbed the killing blow: median 0.40, p75 2.00, p90 6.90. * Cannot climb back out: at energy<=5 the next tick brings a landed hit 0.232% of the time and death 0.663% (2.9x). At <=20 recovery is 2x more likely, which is why a floor at 20 is the wrong value. * Cost: floor 5 blocks 9.58% of ticks, median run 53, mean run 118, banking ~9.9 energy against a p75 overshoot of 2.0. * Measured caveat: the recorded ledger closes exactly (residual -0.00 over 35065 rounds), so these captures do NOT charge firepower cost; the cost column is derived from the game rules, and the landed-hit power (mode 1.0, mean 1.42) is what sets the bracket. Honest read: worth an A/B, materially different in magnitude from the geometry arm (smaller damage cost, stronger and directly measured safety claim), so NOT the clean 'protects against nothing' negative. docs/ram_floor_exhaustion_ab.md: replaces the draft with the measurement plus a re-sized A/B proposal (4 arms, 42 runs/opponent/arm, 630 runs/arm for MDE 0.10 wins/run, ~2.7 h at the measured 23 battles/min). NOT RUN -- no battle, server or GUI was started. Both knobs remain default 0.0.
279 lines
11 KiB
Python
Executable File
279 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""j162 DECISIVE measurement: does the bot ever actually run out of energy?
|
|
|
|
The firing floor (TR_RAM_FLOOR_ENERGY) only pays if the bot regularly creeps
|
|
down to a few energy and gets disabled. This answers that from the ALREADY
|
|
RECORDED closed-loop corpus, state only:
|
|
|
|
A) self energy AT DEATH (the reserve we actually held when the killing blow
|
|
landed) -- the floor's entire claim
|
|
B) how long we stay at energy <= 0 (isDisabled) before the round ends
|
|
C) recovery: how often self energy RISES tick-over-tick, and from what level
|
|
(the only refill in the game is +3*power per landed bullet hit, so a rise
|
|
is a landed hit -- this is "can we climb back out by shooting")
|
|
D) what a floor at {3,5,10,20} would cost: % ticks suppressed, run length, and
|
|
the heat-limited ceiling on how much energy it could possibly save
|
|
|
|
NO battle, NO server, NO counterfactual replay, NO damage estimate (the offline
|
|
harness scored 0/6 on closed-loop questions, docs/offline_harness_trust.md).
|
|
|
|
Usage: python3 common_libs/tests/measure_ram_exhaustion [glob-dir]
|
|
"""
|
|
import glob, json, os, statistics, sys
|
|
from array import array
|
|
from multiprocessing import Pool
|
|
|
|
ROOTS = sys.argv[1:] or ["/tmp"]
|
|
|
|
# Tank Royale gun heat: heat += 1 + power/5 and the gun cools 0.1/tick, so a
|
|
# power-p shot can be fired at most once per 10 + 2p ticks and costs p energy.
|
|
# The cost bracket is therefore p/(10+2p) energy per tick, from 0.0098 at the
|
|
# cheapest legal shot (0.1) to 0.1875 at the most expensive (3.0).
|
|
def per_tick(power):
|
|
return power / (10.0 + 2.0 * power)
|
|
|
|
|
|
def num(line, key):
|
|
i = line.find('"' + key + '":')
|
|
if i < 0:
|
|
return None
|
|
i += len(key) + 3
|
|
j = line.find(',', i)
|
|
if j < 0:
|
|
j = line.find('}', i)
|
|
try:
|
|
return float(line[i:j])
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def load(path):
|
|
"""[(self, enemy)] per tick, with round boundaries from the round map."""
|
|
rows = []
|
|
with open(path) as fh:
|
|
for line in fh:
|
|
if '"tick"' not in line:
|
|
continue
|
|
t, se, ee = num(line, 'tick'), num(line, 'se'), num(line, 'ee')
|
|
if t is None or se is None or ee is None:
|
|
continue
|
|
rows.append((t, se, ee))
|
|
if not rows:
|
|
return []
|
|
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 = []
|
|
if not bounds:
|
|
# no map: a round is the span between RISES from depleted to full,
|
|
# never the first ticks of a round where both bots sit at 100.
|
|
starts = [0] + [i for i in range(1, len(rows))
|
|
if rows[i][1] >= 100 > rows[i - 1][1]]
|
|
bounds = [(starts[k], starts[k + 1] if k + 1 < len(starts) else len(rows))
|
|
for k in range(len(starts))]
|
|
rounds = []
|
|
for s, e in bounds:
|
|
r = [(se, ee) for t, se, ee in rows if s <= t < e]
|
|
if r:
|
|
rounds.append(r)
|
|
return rounds
|
|
|
|
|
|
def corpus():
|
|
files = []
|
|
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
|
|
files.append(f)
|
|
out = []
|
|
for r in Pool(8).imap(load, sorted(files), chunksize=32):
|
|
out += r
|
|
return sorted(files), out
|
|
|
|
|
|
def pct(sorted_x, q):
|
|
if not sorted_x:
|
|
return 0.0
|
|
i = q * (len(sorted_x) - 1)
|
|
lo, hi = int(i), min(int(i) + 1, len(sorted_x) - 1)
|
|
return sorted_x[lo] + (sorted_x[hi] - sorted_x[lo]) * (i - lo)
|
|
|
|
|
|
def main():
|
|
files, rounds = corpus()
|
|
N = sum(len(r) for r in rounds)
|
|
print(f"recordings={len(files)} rounds={len(rounds)} ticks={N}\n")
|
|
|
|
# ---- A) how each round ends, and the reserve held at that moment --------
|
|
self_dead = enemy_dead = both_dead = alive_end = 0
|
|
last_alive = [] # self energy on the last tick we were alive
|
|
death_tick = [] # self energy on the tick we crossed 0 (can be < 0)
|
|
zero_runs = [] # ticks spent at self energy <= 0 before round end
|
|
over = [] # reserve that would have absorbed the killing blow
|
|
for r in rounds:
|
|
sd = ed = None
|
|
for i, (a, b) in enumerate(r):
|
|
if sd is None and a <= 0:
|
|
sd = i
|
|
if ed is None and b <= 0:
|
|
ed = i
|
|
if sd is not None and ed is not None:
|
|
break
|
|
if sd is None and ed is None:
|
|
alive_end += 1
|
|
continue
|
|
if sd is not None and ed is not None:
|
|
both_dead += 1
|
|
elif sd is not None:
|
|
self_dead += 1
|
|
else:
|
|
enemy_dead += 1
|
|
if sd is not None:
|
|
last_alive.append(r[sd - 1][0] if sd > 0 else r[0][0])
|
|
death_tick.append(r[sd][0])
|
|
over.append(-r[sd][0])
|
|
j = len(r)
|
|
while j > sd and r[j - 1][0] <= 0:
|
|
j -= 1
|
|
zero_runs.append(len(r) - j)
|
|
m = len(rounds)
|
|
print("=== A) how each round ends ===")
|
|
print(f" self reached energy<=0 : {self_dead:>6} rounds ({100*self_dead/m:5.1f}%)")
|
|
print(f" only the enemy did : {enemy_dead:>6} rounds ({100*enemy_dead/m:5.1f}%)")
|
|
print(f" both in the same round : {both_dead:>6} rounds ({100*both_dead/m:5.1f}%)")
|
|
print(f" neither (truncated) : {alive_end:>6} rounds ({100*alive_end/m:5.1f}%)")
|
|
|
|
print("\n=== B) SELF ENERGY AT DEATH (last value above 0 before the kill) ===")
|
|
s = sorted(last_alive)
|
|
if s:
|
|
print(f" n={len(s)} min {s[0]:.2f} p10 {pct(s,.10):.2f} median {pct(s,.5):.2f}"
|
|
f" mean {statistics.fmean(s):.2f} p90 {pct(s,.90):.2f} max {s[-1]:.2f}")
|
|
for t in (0, 1, 3, 5, 10, 20):
|
|
c = sum(1 for x in s if x <= t)
|
|
print(f" <= {t:>2} energy: {c:>6} ({100*c/len(s):5.1f}% of self deaths,"
|
|
f" {100*c/m:5.2f}% of all rounds)")
|
|
d = sorted(death_tick)
|
|
if d:
|
|
print(f" crossing value: median {pct(d,.5):.2f} p10 {pct(d,.10):.2f}"
|
|
f" p90 {pct(d,.90):.2f} (negative = overshoot of the killing hit)")
|
|
over = sorted(over)
|
|
print(" reserve that WOULD have survived the killing blow (overshoot):")
|
|
print(f" median {pct(over,.5):.2f} p75 {pct(over,.75):.2f}"
|
|
f" p90 {pct(over,.90):.2f} p99 {pct(over,.99):.2f} max {over[-1]:.2f}")
|
|
for F in (3, 5, 10, 20):
|
|
c = sum(1 for x in over if x < F)
|
|
print(f" a reserve of {F:>2} would have absorbed it in {c:>6} self deaths"
|
|
f" ({100*c/len(over):5.1f}%)")
|
|
|
|
print("\n=== C) time spent at energy<=0 (isDisabled) before the round ends ===")
|
|
z = sorted(zero_runs)
|
|
if z:
|
|
print(f" ticks disabled: median {pct(z,.5):.0f} p90 {pct(z,.9):.0f}"
|
|
f" max {z[-1]} total {sum(z)} of {N} ticks"
|
|
f" ({100*sum(z)/N:.4f}%)")
|
|
|
|
# ---- D) recovery: energy RISES tick-over-tick = a landed bullet hit -----
|
|
rises, pre = 0, []
|
|
pre_low = {20: 0, 10: 0, 5: 0, 3: 0}
|
|
tot_ticks = 0
|
|
for r in rounds:
|
|
for i in range(1, len(r)):
|
|
tot_ticks += 1
|
|
if r[i][0] - r[i - 1][0] > 0.01:
|
|
rises += 1
|
|
pre.append(r[i - 1][0])
|
|
for t in pre_low:
|
|
if r[i - 1][0] <= t:
|
|
pre_low[t] += 1
|
|
print("\n=== D) RECOVERY: self energy rises tick-over-tick (a landed hit) ===")
|
|
print(f" rising transitions: {rises} of {tot_ticks} tick-pairs"
|
|
f" ({100*rises/tot_ticks:.3f}%), i.e. ~{rises/len(rounds):.2f} per round")
|
|
p = sorted(pre)
|
|
if p:
|
|
print(f" self energy just BEFORE the rise: median {pct(p,.5):.2f}"
|
|
f" p10 {pct(p,.10):.2f} p90 {pct(p,.90):.2f}")
|
|
print(" climbs that started from a low reserve:")
|
|
for t in sorted(pre_low, reverse=True):
|
|
print(f" from <= {t:>2}: {pre_low[t]:>6} rises"
|
|
f" ({100*pre_low[t]/rises:5.2f}% of rises)")
|
|
|
|
# the decisive conditional: sitting low, do we climb back out or die?
|
|
# "death" counts the ONE tick that crosses 0. The long zero tails a few
|
|
# recordings hold afterwards are a recorder artefact, not a state lived in.
|
|
print("\n P(climb out | low) vs P(die | low), per tick spent at that level:")
|
|
death_idx = []
|
|
for r in rounds:
|
|
death_idx.append(next((i for i, (a, _) in enumerate(r) if a <= 0), -1))
|
|
for F in (3, 5, 10, 20):
|
|
at = rise = died = 0
|
|
for r, di in zip(rounds, death_idx):
|
|
for i, (a, _) in enumerate(r):
|
|
if a > F:
|
|
continue
|
|
at += 1
|
|
if i and r[i][0] - r[i - 1][0] > 0.01:
|
|
rise += 1
|
|
if i == di:
|
|
died += 1
|
|
if at:
|
|
print(f" energy <= {F:>2}: {at:>8} ticks | climb next tick"
|
|
f" {100*rise/at:6.3f}% | killed on this tick {100*died/at:6.3f}%"
|
|
f" -> dying is {died/max(1,rise):.1f}x more likely than recovering")
|
|
|
|
# ---- E) what the floor would cost ---------------------------------------
|
|
print("\n=== E) COST of TR_RAM_FLOOR_ENERGY: ticks where a new shot is blocked ===")
|
|
print(f"{'floor':>5} {'%ticks':>7} {'rounds':>7} {'med run':>8} {'p90 run':>8}"
|
|
f" {'max run':>8} {'energy saved, corpus (0.1..3.0 p)':>34}"
|
|
f" {'per med run @1.0p':>19}")
|
|
for F in (3, 5, 10, 20):
|
|
tot, hit, lens = 0, 0, []
|
|
for r in rounds:
|
|
cur, got = 0, False
|
|
for a, _ in r:
|
|
if a <= F:
|
|
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()
|
|
# The gun may not fire more often than 1/(10*heat) ticks, so the floor
|
|
# can never save more than the suppressed ticks x power-per-shot x
|
|
# shots-per-tick. Report the bracket: 0.1 power (cheapest legal shot) to
|
|
# 3.0 power (most expensive legal shot).
|
|
med = pct(lens, .5) if lens else 0
|
|
lo, hi = tot * per_tick(0.1), tot * per_tick(3.0)
|
|
mid = med * per_tick(1.0)
|
|
print(f"{F:>5} {100*tot/N:>6.2f}% {hit:>7} {med:>8.0f} "
|
|
f"{pct(lens,.9) if lens else 0:>8.0f} {lens[-1] if lens else 0:>8}"
|
|
f" {lo:>7.0f} .. {hi:>7.0f} {mid:>6.2f}")
|
|
print(" energy saved over the WHOLE corpus, heat-limited: the 0.1..3.0 power")
|
|
print(" bracket, then the p=1.0 column = what one median suppressed RUN is worth")
|
|
print(" (1.0 power is the mode of the measured landed-hit histogram).")
|
|
print(" Median run lengths 34/53/89/139 ticks; one 1.0-power landed hit = 3.0.")
|
|
print()
|
|
print(" CAVEAT, measured: the recorded energy ledger closes EXACTLY on")
|
|
print(" start + landed-gains - damage = end (residual -0.00 over 35065 rounds),")
|
|
print(" i.e. these captures DO NOT charge the firepower cost. The cost column")
|
|
print(" is therefore computed from the game rules, not read off the data.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|