merge j160-ramfloor: the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger (default off)
TR_RAM_FLOOR_ENERGY and TR_RAM_ENEMY_ENERGY, both default 0.0, plus common_libs/movements/ram_decision.nim, the fire gate in ModularBot.nim, two offline measure_* tools and the A/B record. The j163 A/B (450 runs/arm, 0 env mis-set) is a clean negative: round-win 40.30% -> 39.70%, sign-flip p=0.7676, MDE 4.73 pp. DO NOT ADOPT; TR_RAM_FLOOR_ENERGY stays 0.0. The mechanism barely fired (0.04% of ticks, not the 9.6% the offline ruler predicted), so the null does not prove the knob inert. # Conflicts: # common_libs/tests/test_tfil_commit_env.nim
This commit is contained in:
Executable
+278
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,57 @@
|
||||
recordings=8149 rounds=35163 ticks=34461805
|
||||
|
||||
=== A) how each round ends ===
|
||||
self reached energy<=0 : 21518 rounds ( 61.2%)
|
||||
only the enemy did : 12906 rounds ( 36.7%)
|
||||
both in the same round : 347 rounds ( 1.0%)
|
||||
neither (truncated) : 392 rounds ( 1.1%)
|
||||
|
||||
=== B) SELF ENERGY AT DEATH (last value above 0 before the kill) ===
|
||||
n=21865 min 0.00 p10 0.10 median 0.83 mean 2.50 p90 8.90 max 24.83
|
||||
<= 0 energy: 0 ( 0.0% of self deaths, 0.00% of all rounds)
|
||||
<= 1 energy: 12217 ( 55.9% of self deaths, 34.74% of all rounds)
|
||||
<= 3 energy: 16793 ( 76.8% of self deaths, 47.76% of all rounds)
|
||||
<= 5 energy: 18420 ( 84.2% of self deaths, 52.38% of all rounds)
|
||||
<= 10 energy: 20290 ( 92.8% of self deaths, 57.70% of all rounds)
|
||||
<= 20 energy: 21864 (100.0% of self deaths, 62.18% of all rounds)
|
||||
crossing value: median -0.40 p10 -6.90 p90 0.00 (negative = overshoot of the killing hit)
|
||||
reserve that WOULD have survived the killing blow (overshoot):
|
||||
median 0.40 p75 2.00 p90 6.90 p99 15.00 max 19.50
|
||||
a reserve of 3 would have absorbed it in 17301 self deaths ( 79.1%)
|
||||
a reserve of 5 would have absorbed it in 18610 self deaths ( 85.1%)
|
||||
a reserve of 10 would have absorbed it in 20690 self deaths ( 94.6%)
|
||||
a reserve of 20 would have absorbed it in 21865 self deaths (100.0%)
|
||||
|
||||
=== C) time spent at energy<=0 (isDisabled) before the round ends ===
|
||||
ticks disabled: median 1 p90 18 max 452 total 593030 of 34461805 ticks (1.7208%)
|
||||
|
||||
=== D) RECOVERY: self energy rises tick-over-tick (a landed hit) ===
|
||||
rising transitions: 205754 of 34426642 tick-pairs (0.598%), i.e. ~5.85 per round
|
||||
self energy just BEFORE the rise: median 45.24 p10 8.04 p90 89.00
|
||||
climbs that started from a low reserve:
|
||||
from <= 20: 52256 rises (25.40% of rises)
|
||||
from <= 10: 25352 rises (12.32% of rises)
|
||||
from <= 5: 12812 rises ( 6.23% of rises)
|
||||
from <= 3: 8013 rises ( 3.89% of rises)
|
||||
|
||||
P(climb out | low) vs P(die | low), per tick spent at that level:
|
||||
energy <= 3: 2633881 ticks | climb next tick 0.130% | killed on this tick 0.830% -> dying is 6.4x more likely than recovering
|
||||
energy <= 5: 3299807 ticks | climb next tick 0.232% | killed on this tick 0.663% -> dying is 2.9x more likely than recovering
|
||||
energy <= 10: 5002524 ticks | climb next tick 0.365% | killed on this tick 0.437% -> dying is 1.2x more likely than recovering
|
||||
energy <= 20: 8526129 ticks | climb next tick 0.500% | killed on this tick 0.256% -> dying is 0.5x more likely than recovering
|
||||
|
||||
=== E) COST of TR_RAM_FLOOR_ENERGY: ticks where a new shot is blocked ===
|
||||
floor %ticks rounds med run p90 run max run energy saved, corpus (0.1..3.0 p) per med run @1.0p
|
||||
3 7.64% 23086 34 299 1104 25822 .. 493853 2.83
|
||||
5 9.58% 23739 53 330 1104 32351 .. 618714 4.42
|
||||
10 14.52% 25211 89 433 1141 49044 .. 937973 7.42
|
||||
20 24.74% 27799 139 621 2059 83590 .. 1598649 11.58
|
||||
energy saved over the WHOLE corpus, heat-limited: the 0.1..3.0 power
|
||||
bracket, then the p=1.0 column = what one median suppressed RUN is worth
|
||||
(1.0 power is the mode of the measured landed-hit histogram).
|
||||
Median run lengths 34/53/89/139 ticks; one 1.0-power landed hit = 3.0.
|
||||
|
||||
CAVEAT, measured: the recorded energy ledger closes EXACTLY on
|
||||
start + landed-gains - damage = end (residual -0.00 over 35065 rounds),
|
||||
i.e. these captures DO NOT charge the firepower cost. The cost column
|
||||
is therefore computed from the game rules, not read off the data.
|
||||
@@ -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()
|
||||
@@ -40,6 +40,7 @@
|
||||
import std/[os, json, random, math, sequtils, sets]
|
||||
import std/strutils except fromHex # `fromHex` would clash with color.fromHex
|
||||
import gun_harness/gun_interface
|
||||
import movements/ram_decision
|
||||
# Private-field access: include (do NOT import) the shipped mover.
|
||||
include movements/the_floor_is_lava
|
||||
# j165: the TFIL-RING fork's arrival commitment. `tfil_ring_replay` includes
|
||||
@@ -1607,6 +1608,110 @@ when declared(TfilRingCommitArrival):
|
||||
firstArmed >= 0 and armed[firstArmed].picked and
|
||||
abs(states[firstArmed].selfSpeed) < 4.0
|
||||
|
||||
# ── j160: the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger ─────
|
||||
proc testJ160() =
|
||||
## j160 — the energy-reserve FIRING FLOOR (TR_RAM_FLOOR_ENERGY) and the
|
||||
## ENEMY-EXHAUSTION ram trigger (TR_RAM_ENEMY_ENERGY). Both default 0.0 =
|
||||
## today's behaviour. Pure logic only; no bot, no server.
|
||||
const F = 5.0
|
||||
|
||||
# 1. DEFAULT PARITY, floor: with the knob unset the floor blocks NOTHING over
|
||||
# a grid that includes every measured low-energy region (<=5: 9.4% of
|
||||
# ticks in the 8612-round closed-loop corpus, p01 self energy = 0.2).
|
||||
var blocked = 0
|
||||
for e in [-1.0, 0.0, 0.1, 1.0, 2.5, 5.0, 7.0, 20.0, 46.0, 100.0, 120.0]:
|
||||
if fireFloorBlocks(0.0, e): inc blocked
|
||||
check "j160: TR_RAM_FLOOR_ENERGY unset (=0) suppresses fire on NO input, " &
|
||||
"so the default path is byte-for-byte today's (" & $blocked & " blocked)",
|
||||
blocked == 0
|
||||
|
||||
# 2. DEFAULT PARITY, trigger: with the knob unset the reason over a grid is
|
||||
# the PRE-j160 result — the new arm is unreachable, and every old arm still
|
||||
# returns exactly what it returned before.
|
||||
var newArm, mismatch: int
|
||||
for dist in [10.0, 100.0, 299.0, 301.0, 500.0]:
|
||||
for se in [0.5, 5.0, 19.0, 21.0, 60.0, 100.0]:
|
||||
for ee in [0.0, 1.0, 5.0, 19.9, 20.0, 40.0, 100.0]:
|
||||
let inp = RamInputs(dist: dist, selfEnergy: se, enemyEnergy: ee)
|
||||
let got = ramTrigger(inp)
|
||||
if got == rrExhausted: inc newArm
|
||||
# the pre-j160 body, verbatim
|
||||
var want: RamReason = rrNone
|
||||
if ee > 0.0:
|
||||
if dist < RamFinisherDist and ee < RamFinisherEnergy and se > ee: want = rrFinisher
|
||||
elif se < RamDesperationEnergy and ee < RamDesperationEnergy and dist < RamDesperationDist: want = rrDesperation
|
||||
if got != want: inc mismatch
|
||||
check "j160: TR_RAM_ENEMY_ENERGY unset (=0) makes the exhaustion arm " &
|
||||
"unreachable (" & $newArm & " hits) and leaves the old finisher / " &
|
||||
"desperation verdicts identical (" & $mismatch & " mismatches over 210 " &
|
||||
"input combinations)",
|
||||
newArm == 0 and mismatch == 0
|
||||
|
||||
# 3. the floor suppresses AT the threshold, not above it.
|
||||
check "j160: the floor blocks AT the threshold (self == 5.0 <= floor 5.0)",
|
||||
fireFloorBlocks(F, 5.0)
|
||||
check "j160: the floor blocks just below it and not just above it — one " &
|
||||
"tick of hysteresis, no dead band",
|
||||
fireFloorBlocks(F, 4.999) and not fireFloorBlocks(F, 5.001)
|
||||
|
||||
# 4. it never suppresses while we are healthy, at ANY floor setting.
|
||||
var healthy = 0
|
||||
for floor in [0.5, 1.0, 5.0, 20.0, 25.0, 40.0]:
|
||||
for e in [floor, 46.0, 60.0, 100.0, 120.0]:
|
||||
if e > floor and fireFloorBlocks(floor, e): inc healthy
|
||||
check "j160: the floor NEVER blocks above its own threshold — healthy energy " &
|
||||
"fires for every floor/energy pair (" & $healthy & " violations)",
|
||||
healthy == 0
|
||||
|
||||
# 5. the trigger switches to ram EXACTLY at the tolerance, no earlier.
|
||||
let inp2 = RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 10.0)
|
||||
check "j160: the exhaustion trigger fires EXACTLY at TR_RAM_ENEMY_ENERGY " &
|
||||
"(enemy 10.0 <= tol 10.0) and not one tick above (10.001)",
|
||||
ramTrigger(inp2, enemyEnergyTol = 10.0) == rrExhausted and
|
||||
ramTrigger(RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 10.001),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 6. the surplus guard survives: 0.6/contact is applied to BOTH bots, so we
|
||||
# only ram an exhausted enemy while WE hold the surplus.
|
||||
check "j160: the exhaustion trigger keeps the finisher's energy-surplus " &
|
||||
"guard — an exhausted enemy while WE are lower is a ram we lose",
|
||||
ramTrigger(RamInputs(dist: 250.0, selfEnergy: 2.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 7. it is the finisher's own shape: the existing range guard still applies.
|
||||
check "j160: the exhaustion trigger keeps the finisher's 300px range guard " &
|
||||
"(enemy exhausted at 301px is not a ram)",
|
||||
ramTrigger(RamInputs(dist: 301.0, selfEnergy: 60.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) != rrExhausted
|
||||
|
||||
# 8. COMPOSITION: ramming WINS. The floor is the reserve FOR the ram, so once
|
||||
# the ram is engaged the reserve is being spent, not held. No starvation:
|
||||
# the floor alone can never make us unable to close.
|
||||
check "j160: RAMMING wins the conflict — at self energy 0.1 (below any " &
|
||||
"sane floor) an engaged ram is never floor-blocked, so the two " &
|
||||
"compose instead of deadlocking each other",
|
||||
fireFloorBlocks(F, 0.1, ramming = true) == false and
|
||||
fireFloorBlocks(F, 0.1, ramming = false) == true
|
||||
|
||||
# 9. and the floor can never be engaged at all without self energy being
|
||||
# genuinely low — the guard the owner asked for, stated as a property.
|
||||
var unsafe = 0
|
||||
for floor in [0.5, 5.0, 20.0, 25.0]:
|
||||
for e in [0.0, 1.0, 10.0, 25.0, 50.0, 100.0]:
|
||||
if fireFloorBlocks(floor, e, ramming = false) and e > floor: inc unsafe
|
||||
check "j160: the floor is a LOW-ENERGY guard only — it can never suppress " &
|
||||
"fire while we are healthy, in any configuration (" & $unsafe & ")",
|
||||
unsafe == 0
|
||||
|
||||
# 10. both knobs together: the exhausted trigger still fires while the floor
|
||||
# is at full strength, and the floor still holds when no ram is engaged.
|
||||
check "j160: both knobs ON compose — exhaustion (enemy 3, us 60) still " &
|
||||
"ram-bypasses the floor, and a non-ramming low-energy tick still holds",
|
||||
fireFloorBlocks(F, 3.0, ramming = false) and
|
||||
ramTrigger(RamInputs(dist: 100.0, selfEnergy: 60.0, enemyEnergy: 3.0),
|
||||
enemyEnergyTol = 10.0) == rrExhausted and
|
||||
not fireFloorBlocks(F, 3.0, ramming = true)
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testDefaultParity()
|
||||
@@ -1624,6 +1729,7 @@ when declared(loadTfilCommitEnv):
|
||||
testJ153()
|
||||
when declared(TfilRingCommitArrival):
|
||||
testJ165()
|
||||
testJ160()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
|
||||
Reference in New Issue
Block a user