lead capture by range: we under-lead (0.40->0.135) — a RANGE effect, not a power one
Measure, for every shot ModularBot fires at the real DrussGT, the lead we actually applied vs the lead the enemy's motion required, from the recorded live battles (/tmp/tfil_ab2, 70 battles / 490 rounds / 54926 shots, plus a 35-battle powtest replication of a different binary). - requiredLead from an AIM-INDEPENDENT interception solve (bullet speed vs enemy truth), appliedLead from the server-recorded bullet bearing. - capture = applied/required, guarded at 2px lateral lead (1.6% excluded); headline metric is the robust proportional slope. - validation: hits 11.6px mean miss / 80.8% inside 18px, misses 134px, 11.6x separation; 496/496 death + 70/70 owner attributions correct. Direct answer: capture falls with RANGE (capSlp 0.401 -> 0.135, and |err|/tolerance 1.27 -> 7.54) but is FLAT across fired POWER within a band (450+, enemy alive: 0.154 / 0.127 / 0.127). The sub-0.5 long-range shots (1.18% hit) are finishKill endgame shots at a near-dead DrussGT, not a lead-capture failure. A naive linear predictor captures 0.29-0.60; we reach 46-67% of that, so the under-lead is real but capture=1.0 is unattainable against a dodger (oracle required lead).
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MEASURE LEAD CAPTURE BY RANGE: how much of the required lead do we apply?
|
||||
|
||||
Reads the recent LIVE recorded battles vs the real DrussGT
|
||||
(``/tmp/tfil_ab2/out/<ARM>/run<N>.jsonl`` per-tick worldstate +
|
||||
``.events.jsonl`` fire/hit sidecar + ``.jsonl.rounds.json`` tick index,
|
||||
produced by ``tools/robocode_shim/run_bridge_battle.sh`` / ``tools/ab/ab_run.sh``)
|
||||
and, for every shot WE fire, compares the lead we actually put into the bullet
|
||||
against the lead the enemy's motion required -- split by RANGE band and then by
|
||||
fired POWER inside each band.
|
||||
|
||||
WHICH SIDE IS US
|
||||
----------------
|
||||
``TrBattleCapture`` writes the movement subject (the bot whose name matches
|
||||
``--subject``, here DrussGT) as ``e*`` and the adversary (ModularBot, our bot)
|
||||
as ``s*``. So our worldstate track is ``sx,sy,se`` and DrussGT's is
|
||||
``ex,ey,ee``. Owner ids in the event sidecar are NOT stable across runs, so
|
||||
every run is attributed independently: a fire event's (x,y) is the firing
|
||||
tank's centre and its energy drops by exactly the fired power one tick later.
|
||||
Attribution is cross-checked against the server ``death`` events (496/496).
|
||||
|
||||
THE DECOMPOSITION (all angles in deg, all relative to the LOS at the FIRE tick)
|
||||
------------------------------------------------------------------------------
|
||||
O = the firing tank's centre at the fire tick (the bullet line origin)
|
||||
v = 20 - 3p px/tick
|
||||
t* = the AIM-INDEPENDENT interception tick: the first tick k with
|
||||
|E(t0+k) - O| <= v*k. It depends only on the enemy's recorded
|
||||
truth and the bullet speed, never on our aim.
|
||||
required = bearing(O -> E(t*)) - LOS (the lead a perfect gun would need)
|
||||
applied = bullet bearing (server-recorded, perfect truth) - LOS
|
||||
leadErr = applied - required (wrapped)
|
||||
capture = applied / required (guarded: only where |required| >= 2px at range)
|
||||
miss_px = |E(t*) - (O + v*t* * u)| (bullet line vs enemy at t*)
|
||||
|
||||
CAPTURE < 1 IS NOT BY ITSELF A BUG
|
||||
----------------------------------
|
||||
``required`` uses the enemy's ACTUAL future dodge (perfect information), which
|
||||
no real gun can know. So the script also computes a NAIVE LINEAR PREDICTOR
|
||||
control -- the same bullet speed, aimed at the intercept of a straight-line
|
||||
continuation of the enemy's last LIN_M ticks of velocity. That control is the
|
||||
ceiling a trivial predictive gun reaches against this dodger; our capture is
|
||||
read against it, not only against 1.0.
|
||||
|
||||
MEASURED = every number printed. INFERRED = the causal reading in the doc.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
|
||||
US_SIDE = "s" # adversary = ModularBot = us
|
||||
BOT_RADIUS = 18.0
|
||||
SPEED_A, SPEED_B = 20.0, 3.0 # bullet speed = 20 - 3*p px/tick
|
||||
MAX_FLIGHT = 220
|
||||
MIN_REQ_PX = 2.0 # capture defined only above this lateral lead
|
||||
LIN_M = 4 # ticks of velocity for the naive-predictor control
|
||||
|
||||
BANDS = [(0, 100), (100, 200), (200, 300), (300, 450), (450, 1e9)]
|
||||
BAND_LABELS = ["0-100", "100-200", "200-300", "300-450", "450+"]
|
||||
PWR_EDGES = [0.0, 0.5, 0.75, 1.0, 1.5, 100.0]
|
||||
PWR_LABELS = ["<0.50", "0.50-0.75", "0.75-1.00", "1.00-1.50", ">=1.50"]
|
||||
|
||||
|
||||
def wrap180(x):
|
||||
"""Signed angular difference in (-180, 180]."""
|
||||
x = (x + 180.0) % 360.0 - 180.0
|
||||
return x + 360.0 if x <= -180.0 else x
|
||||
|
||||
|
||||
def band_of(r):
|
||||
for (lo, hi), lab in zip(BANDS, BAND_LABELS):
|
||||
if lo <= r < hi:
|
||||
return lab
|
||||
return BAND_LABELS[-1]
|
||||
|
||||
|
||||
def pbin(p):
|
||||
for i in range(len(PWR_EDGES) - 1):
|
||||
if PWR_EDGES[i] <= p < PWR_EDGES[i + 1]:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def mean(xs):
|
||||
return statistics.fmean(xs) if xs else float("nan")
|
||||
|
||||
|
||||
def median(xs):
|
||||
return statistics.median(xs) if xs else float("nan")
|
||||
|
||||
|
||||
def stdev(xs):
|
||||
return statistics.pstdev(xs) if len(xs) > 1 else float("nan")
|
||||
|
||||
|
||||
def _rows(path):
|
||||
out = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
o = json.loads(line)
|
||||
if "tick" in o:
|
||||
out.append(o)
|
||||
return out
|
||||
|
||||
|
||||
class Run:
|
||||
"""One live battle: capture rows, event sidecar, per-round tick index."""
|
||||
|
||||
def __init__(self, cap_path, events_path, rounds_path):
|
||||
self.cap_path = cap_path
|
||||
self.rows = _rows(cap_path)
|
||||
self.by_tick = {r["tick"]: r for r in self.rows}
|
||||
self.events = [json.loads(l) for l in open(events_path) if l.strip()]
|
||||
self.rounds = json.load(open(rounds_path))["rounds"]
|
||||
self.start = {r["round"]: r["startTick"] for r in self.rounds}
|
||||
self.count = {r["round"]: r["count"] for r in self.rounds}
|
||||
self.owner_side = self._resolve_owner_side()
|
||||
self.resolution = {}
|
||||
for ev in self.events:
|
||||
if ev.get("type") in ("hit", "hitwall", "hitbullet"):
|
||||
self.resolution[(ev["round"], ev["owner"], ev["bullet"])] = ev["type"]
|
||||
|
||||
# -- attribution ------------------------------------------------------
|
||||
def _match(self, t, side, ev):
|
||||
r, r1 = self.by_tick.get(t), self.by_tick.get(t + 1)
|
||||
if r is None or r1 is None:
|
||||
return False
|
||||
if abs(r[side + "x"] - ev["x"]) > 0.02 or abs(r[side + "y"] - ev["y"]) > 0.02:
|
||||
return False
|
||||
ef = "ee" if side == "e" else "se"
|
||||
return abs((r[ef] - r1[ef]) - ev["power"]) < 0.02
|
||||
|
||||
def _resolve_owner_side(self):
|
||||
votes = collections.defaultdict(collections.Counter)
|
||||
for ev in self.events:
|
||||
if ev.get("type") != "fire":
|
||||
continue
|
||||
guess = self.start.get(ev["round"], 0) + ev["tick"]
|
||||
for t in range(guess - 8, guess + 9):
|
||||
for side in ("e", "s"):
|
||||
if self._match(t, side, ev):
|
||||
votes[ev["owner"]][side] += 1
|
||||
return {o: c.most_common(1)[0][0] for o, c in votes.items() if c}
|
||||
|
||||
def align(self, ev):
|
||||
guess = self.start.get(ev["round"], 0) + ev["tick"]
|
||||
side = self.owner_side.get(ev["owner"])
|
||||
best = None
|
||||
for t in range(guess - 8, guess + 9):
|
||||
for sd in (("e", "s") if side is None else (side,)):
|
||||
if self._match(t, sd, ev):
|
||||
d = abs(t - guess)
|
||||
if best is None or d < best[0]:
|
||||
best = (d, t, sd)
|
||||
return None if best is None else best[1]
|
||||
|
||||
def n_position_matches(self, ev):
|
||||
n = 0
|
||||
guess = self.start.get(ev["round"], 0) + ev["tick"]
|
||||
for t in range(guess - 8, guess + 9):
|
||||
for sd in ("e", "s"):
|
||||
r = self.by_tick.get(t)
|
||||
if r is None:
|
||||
continue
|
||||
if abs(r[sd + "x"] - ev["x"]) <= 0.02 and abs(r[sd + "y"] - ev["y"]) <= 0.02:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
def death_side_ok(self):
|
||||
"""Every death must land on a side whose energy has collapsed to 0."""
|
||||
out = []
|
||||
for ev in self.events:
|
||||
if ev.get("type") != "death":
|
||||
continue
|
||||
side = self.owner_side.get(ev["victim"])
|
||||
g = self.start.get(ev["round"], 0) + ev["tick"]
|
||||
ekey = "ee" if side == "e" else "se"
|
||||
ok = False
|
||||
for t in range(g - 8, g + 10):
|
||||
r = self.by_tick.get(t)
|
||||
if r is not None and r[ekey] <= 1e-6:
|
||||
ok = True
|
||||
break
|
||||
out.append(ok)
|
||||
return out
|
||||
|
||||
# -- per-shot extraction ---------------------------------------------
|
||||
def shots(self):
|
||||
for ev in self.events:
|
||||
if ev.get("type") != "fire":
|
||||
continue
|
||||
if self.owner_side.get(ev["owner"]) != US_SIDE:
|
||||
continue
|
||||
t0 = self.align(ev)
|
||||
if t0 is None:
|
||||
continue
|
||||
s = self._shot(ev, t0)
|
||||
if s is not None:
|
||||
yield s
|
||||
|
||||
def _shot(self, ev, t0):
|
||||
r0 = self.by_tick[t0]
|
||||
p = ev["power"]
|
||||
v = SPEED_A - SPEED_B * p
|
||||
th = math.radians(ev["dir"])
|
||||
ux, uy = math.cos(th), math.sin(th)
|
||||
x0, y0 = ev["x"], ev["y"] # tank centre == bullet line origin
|
||||
rnd = ev["round"]
|
||||
end = self.start[rnd] + self.count[rnd]
|
||||
rng = math.hypot(r0["ex"] - x0, r0["ey"] - y0)
|
||||
|
||||
# aim-independent interception tick (oracle)
|
||||
tstar = None
|
||||
for k in range(1, MAX_FLIGHT + 1):
|
||||
if t0 + k >= end:
|
||||
break
|
||||
r = self.by_tick.get(t0 + k)
|
||||
if r is None:
|
||||
break
|
||||
if math.hypot(r["ex"] - x0, r["ey"] - y0) <= v * k:
|
||||
tstar = (k, r)
|
||||
break
|
||||
if tstar is None:
|
||||
return None
|
||||
kstar, ea = tstar
|
||||
|
||||
# miss distance: perpendicular of the enemy at the tick our (actual) bullet
|
||||
# reaches its along-track plane, relative to our bullet's real line.
|
||||
# Same definition as common_libs/tests/analyze_drussgt_dodge_vs_power.py.
|
||||
perp_arr = None
|
||||
for k in range(1, MAX_FLIGHT + 1):
|
||||
if t0 + k >= end:
|
||||
break
|
||||
r = self.by_tick.get(t0 + k)
|
||||
if r is None:
|
||||
break
|
||||
dx, dy = r["ex"] - x0, r["ey"] - y0
|
||||
al = dx * ux + dy * uy
|
||||
if al > 0 and v * k >= al:
|
||||
perp_arr = dx * uy - dy * ux
|
||||
break
|
||||
miss_px = abs(perp_arr) if perp_arr is not None else float("nan")
|
||||
|
||||
los = math.degrees(math.atan2(r0["ey"] - y0, r0["ex"] - x0))
|
||||
required = wrap180(math.degrees(math.atan2(ea["ey"] - y0, ea["ex"] - x0)) - los)
|
||||
applied = wrap180(ev["dir"] - los)
|
||||
leaderr = wrap180(applied - required)
|
||||
|
||||
# naive linear-predictor control: continue the last LIN_M ticks of velocity
|
||||
lin = None
|
||||
rp = self.by_tick.get(t0 - LIN_M)
|
||||
if rp is not None:
|
||||
vx = (r0["ex"] - rp["ex"]) / LIN_M
|
||||
vy = (r0["ey"] - rp["ey"]) / LIN_M
|
||||
kk = rng / v
|
||||
for _ in range(40):
|
||||
kk = math.hypot(r0["ex"] + vx * kk - x0, r0["ey"] + vy * kk - y0) / v
|
||||
lin = wrap180(math.degrees(math.atan2(r0["ey"] + vy * kk - y0,
|
||||
r0["ex"] + vx * kk - x0)) - los)
|
||||
|
||||
req_px = abs(math.radians(required)) * rng
|
||||
capture = (applied / required) if (req_px >= MIN_REQ_PX and abs(required) > 1e-9) else None
|
||||
capture_lin = (lin / required) if (lin is not None and req_px >= MIN_REQ_PX
|
||||
and abs(required) > 1e-9) else None
|
||||
|
||||
kind = self.resolution.get((rnd, ev["owner"], ev["bullet"]))
|
||||
return dict(
|
||||
run=self.cap_path, rnd=rnd, tick=t0, power=p, speed=v,
|
||||
range=rng, band=band_of(rng), pbin=pbin(p), flight=kstar,
|
||||
required=required, applied=applied, leaderr=leaderr, lin=lin,
|
||||
capture=capture, capture_lin=capture_lin, req_px=req_px,
|
||||
miss_px=miss_px,
|
||||
tolerance_deg=math.degrees(math.atan(BOT_RADIUS / rng)),
|
||||
hit=1.0 if kind == "hit" else 0.0,
|
||||
resolution=kind, npos=self.n_position_matches(ev),
|
||||
our_energy=r0["se"], enemy_energy=r0["ee"], rel_tick=t0 - self.start[rnd],
|
||||
)
|
||||
|
||||
|
||||
def discover_tfil(root):
|
||||
runs = []
|
||||
for sub in sorted(os.listdir(root)):
|
||||
d = os.path.join(root, sub)
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for fn in sorted(os.listdir(d)):
|
||||
if not fn.endswith(".jsonl") or fn.endswith(".events.jsonl"):
|
||||
continue
|
||||
cap = os.path.join(d, fn)
|
||||
ev, rj = cap[:-6] + ".events.jsonl", cap + ".rounds.json"
|
||||
if os.path.exists(ev) and os.path.exists(rj):
|
||||
runs.append(Run(cap, ev, rj))
|
||||
return runs
|
||||
|
||||
|
||||
def discover_powtest(root):
|
||||
"""powtest layout: cap_<arm>_r<N>.jsonl + events_<arm>_r<N>.json + .rounds.json."""
|
||||
runs = []
|
||||
if not os.path.isdir(root):
|
||||
return runs
|
||||
for fn in sorted(os.listdir(root)):
|
||||
if not (fn.startswith("cap_") and fn.endswith(".jsonl")):
|
||||
continue
|
||||
cap = os.path.join(root, fn)
|
||||
stem = fn[4:].replace(".jsonl", ".json")
|
||||
ev = os.path.join(root, "events_" + stem)
|
||||
rj = cap + ".rounds.json"
|
||||
if os.path.exists(ev) and os.path.exists(rj):
|
||||
runs.append(Run(cap, ev, rj))
|
||||
return runs
|
||||
|
||||
|
||||
# --------------------------------------------------------------- statistics
|
||||
def slope(xs, ys):
|
||||
d = sum(x * x for x in xs)
|
||||
return sum(x * y for x, y in zip(xs, ys)) / d if d > 0 else float("nan")
|
||||
|
||||
|
||||
def cell(shots):
|
||||
caps = [s["capture"] for s in shots if s["capture"] is not None]
|
||||
lins = [s["capture_lin"] for s in shots if s["capture_lin"] is not None]
|
||||
req = [s["required"] for s in shots]
|
||||
resolved = [s for s in shots if s["resolution"] in ("hit", "hitwall", "hitbullet")]
|
||||
return dict(
|
||||
n=len(shots), nc=len(caps), excl=len(shots) - len(caps),
|
||||
unresolved=len(shots) - len(resolved),
|
||||
range=mean([s["range"] for s in shots]),
|
||||
req=mean(req),
|
||||
app=mean([s["applied"] for s in shots]),
|
||||
cap=mean(caps), cap_med=median(caps), cap_slope=slope(req, [s["applied"] for s in shots]),
|
||||
cap_lin=mean(lins), cap_lin_slope=slope(req, [s["lin"] for s in shots if s["lin"] is not None]),
|
||||
ae=mean([abs(s["leaderr"]) for s in shots]),
|
||||
ae_sd=stdev([abs(s["leaderr"]) for s in shots]),
|
||||
ae_px=mean([abs(math.radians(s["leaderr"])) * s["range"] for s in shots]),
|
||||
miss=mean([s["miss_px"] for s in shots]),
|
||||
med_miss=median([s["miss_px"] for s in shots]),
|
||||
tol=mean([s["tolerance_deg"] for s in shots]),
|
||||
hit=mean([s["hit"] for s in resolved]),
|
||||
enE=mean([s["enemy_energy"] for s in shots]),
|
||||
flight=mean([s["flight"] for s in shots]),
|
||||
)
|
||||
|
||||
|
||||
def table(cells, title):
|
||||
print(f"\n{title}")
|
||||
hdr = (f"{'cell':<24} {'n':>6} {'excl':>5} {'range':>5} {'flt':>4} {'reqL':>7} {'appL':>7} "
|
||||
f"{'capt':>6} {'capSlp':>7} {'capLin':>7} {'|err|':>6} {'+/-':>5} {'|err|px':>7} "
|
||||
f"{'tol':>5} {'hit%':>6}")
|
||||
print(hdr)
|
||||
print("-" * len(hdr))
|
||||
for name, sh in cells:
|
||||
if not sh:
|
||||
continue
|
||||
c = cell(sh)
|
||||
print(f"{name:<24} {c['n']:>6} {c['excl']:>5} {c['range']:>5.0f} {c['flight']:>4.1f} "
|
||||
f"{c['req']:>7.2f} {c['app']:>7.2f} {c['cap']:>6.2f} {c['cap_slope']:>7.3f} "
|
||||
f"{c['cap_lin_slope']:>7.3f} {c['ae']:>6.2f} {c['ae_sd']:>5.2f} {c['ae_px']:>7.1f} "
|
||||
f"{c['tol']:>5.2f} {100*c['hit']:>5.2f}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--tfil", default="/tmp/tfil_ab2/out")
|
||||
ap.add_argument("--powtest", default="/tmp/powtest")
|
||||
ap.add_argument("--json", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
runs = discover_tfil(args.tfil)
|
||||
print("=" * 132)
|
||||
print("LEAD CAPTURE BY RANGE -- live ModularBot vs real DrussGT")
|
||||
print("=" * 132)
|
||||
print(f"corpus : {args.tfil}")
|
||||
print(f"battles (runs) : {len(runs)}")
|
||||
if not runs:
|
||||
raise SystemExit("no runs found")
|
||||
|
||||
amb = 0
|
||||
death_ok = death_n = 0
|
||||
pow_all = collections.defaultdict(collections.Counter)
|
||||
sideowners = collections.Counter()
|
||||
allshots = []
|
||||
for rn in runs:
|
||||
for o, sd in rn.owner_side.items():
|
||||
sideowners[(sd, o)] += 1
|
||||
amb += sum(1 for ev in rn.events if ev.get("type") == "fire"
|
||||
and rn.n_position_matches(ev) > 1)
|
||||
for ev in rn.events:
|
||||
if ev.get("type") == "fire":
|
||||
pow_all[rn.owner_side.get(ev["owner"], "?")][round(ev["power"], 2)] += 1
|
||||
ds = rn.death_side_ok()
|
||||
death_n += len(ds)
|
||||
death_ok += sum(ds)
|
||||
allshots.extend(rn.shots())
|
||||
|
||||
sides = collections.Counter(tuple(sorted(rn.owner_side.values())) for rn in runs)
|
||||
print(f"per-run owner maps: {dict(sides)} (expect all ('e','s'))")
|
||||
print(f"runs with a broken owner map : {sum(1 for rn in runs if sorted(rn.owner_side.values()) != ['e','s'])}")
|
||||
print(f"fire events w/ ambiguous position : {amb}")
|
||||
print(f"death events side-validated : {death_ok}/{death_n}")
|
||||
for sd in ("s", "e"):
|
||||
tot = sum(pow_all[sd].values())
|
||||
top = pow_all[sd].most_common(6)
|
||||
print(f" side {sd} = {'US (ModularBot)' if sd == US_SIDE else 'DrussGT'} "
|
||||
f"n={tot:>6} top powers: {top}")
|
||||
|
||||
hits = [s for s in allshots if s["hit"] == 1.0]
|
||||
misses = [s for s in allshots if s["resolution"] in ("hitwall", "hitbullet")
|
||||
and s["hit"] == 0.0]
|
||||
unresolved = [s for s in allshots if s["resolution"] is None]
|
||||
print("\n" + "=" * 132)
|
||||
print("SAMPLE")
|
||||
print("=" * 132)
|
||||
print(f"our shots (aligned, with an interception) : {len(allshots)}")
|
||||
print(f" server hit / hitwall / hitbullet / other : "
|
||||
f"{sum(1 for s in allshots if s['resolution']=='hit')} / "
|
||||
f"{sum(1 for s in allshots if s['resolution']=='hitwall')} / "
|
||||
f"{sum(1 for s in allshots if s['resolution']=='hitbullet')} / "
|
||||
f"{len(unresolved)}")
|
||||
print(f"capture excluded (|required| < {MIN_REQ_PX} px lateral) : "
|
||||
f"{sum(1 for s in allshots if s['capture'] is None)}")
|
||||
|
||||
print("\n" + "=" * 132)
|
||||
print("GEOMETRY VALIDATION (hits must separate from misses)")
|
||||
print("=" * 132)
|
||||
hdr = f"{'':<16}{'n':>7} {'|leadErr|deg':>13} {'|leadErr|px':>12} {'misPx mean':>11} {'misPx med':>10} {'<18px':>7}"
|
||||
print(hdr)
|
||||
for lab, grp in (("HITS", hits), ("MISSES", misses)):
|
||||
if not grp:
|
||||
continue
|
||||
ae = [abs(s["leaderr"]) for s in grp]
|
||||
apx = [abs(math.radians(s["leaderr"])) * s["range"] for s in grp]
|
||||
mp = [s["miss_px"] for s in grp]
|
||||
print(f"{lab:<16}{len(grp):>7} {mean(ae):>13.3f} {mean(apx):>12.1f} "
|
||||
f"{mean(mp):>11.1f} {median(mp):>10.1f} {100*sum(1 for x in mp if x<18)/len(mp):>6.1f}%")
|
||||
if hits and misses:
|
||||
sep = mean([s["miss_px"] for s in misses]) / max(1e-9, mean([s["miss_px"] for s in hits]))
|
||||
print(f"\nmiss separation ratio (misses/hits) = {sep:.2f}x "
|
||||
f"-> {'OK (bearing recovery validated)' if sep > 2 else 'WEAK - bearing recovery suspect'}")
|
||||
print(f"overall measured hit rate = {100*mean([s['hit'] for s in allshots]):.2f}%")
|
||||
|
||||
cells = [(lab, [s for s in allshots if s["band"] == lab]) for lab in BAND_LABELS]
|
||||
print("\n" + "=" * 132)
|
||||
print("MAIN TABLE -- all our shots, by RANGE band")
|
||||
print("=" * 132)
|
||||
print("reqL/appL = mean signed lead vs LOS (deg). capt = mean ratio, capSlp = robust proportional")
|
||||
print("capture, capLin = the same slope for a naive linear predictor (control).")
|
||||
table(cells, "by range band")
|
||||
|
||||
print("\n" + "=" * 132)
|
||||
print("POWER WITHIN RANGE BAND -- this is what separates 'range' from 'power'")
|
||||
print("=" * 132)
|
||||
for lab in BAND_LABELS:
|
||||
sub = [s for s in allshots if s["band"] == lab]
|
||||
if not sub:
|
||||
continue
|
||||
pcs = [(f"{lab} {PWR_LABELS[i]}", [s for s in sub if s["pbin"] == i])
|
||||
for i in range(len(PWR_LABELS))]
|
||||
table(pcs, f"range {lab}")
|
||||
|
||||
# ---- endgame confound and its removal --------------------------------
|
||||
print("\n" + "=" * 132)
|
||||
print("CONFOUND CHECK -- the sub-0.5-power shots at long range are the ENDGAME finish")
|
||||
print("=" * 132)
|
||||
print("hit% of our 450+ shots, by the ENEMY's remaining energy at the fire tick:")
|
||||
sub = [s for s in allshots if s["band"] == "450+"]
|
||||
for lo, hi in ((0, 2), (2, 5), (5, 10), (10, 20), (20, 40), (40, 150)):
|
||||
g = [s for s in sub if lo <= s["enemy_energy"] < hi]
|
||||
if g:
|
||||
c = cell(g)
|
||||
print(f" enemy energy [{lo:>3},{hi:>3}) n={c['n']:>6} meanP={mean([x['power'] for x in g]):>5.2f} "
|
||||
f"capSlp={c['cap_slope']:>6.3f} hit%={100*c['hit']:>5.2f}")
|
||||
print("\nsame table, EXCLUDING shots fired while the enemy still has >= 5 energy")
|
||||
print("(this removes the near-dead-target endgame and leaves the clean power comparison):")
|
||||
for lab in BAND_LABELS:
|
||||
sub = [s for s in allshots if s["band"] == lab and s["enemy_energy"] >= 5.0]
|
||||
if not sub:
|
||||
continue
|
||||
pcs = [(f"{lab} {PWR_LABELS[i]}", [s for s in sub if s["pbin"] == i])
|
||||
for i in range(len(PWR_LABELS))]
|
||||
table(pcs, f"range {lab} (enemy energy >= 5)")
|
||||
|
||||
# ---- response curve ---------------------------------------------------
|
||||
print("\n" + "=" * 132)
|
||||
print("RESPONSE CURVE -- do we move the aim in proportion to the required lead?")
|
||||
print("=" * 132)
|
||||
for lab in ("200-300", "300-450", "450+"):
|
||||
g = sorted([s for s in allshots if s["band"] == lab], key=lambda s: s["required"])
|
||||
if len(g) < 100:
|
||||
continue
|
||||
nb = 8
|
||||
print(f"\nrange {lab} (deciles of required lead -> mean applied lead):")
|
||||
for i in range(nb):
|
||||
q = g[i * len(g) // nb:(i + 1) * len(g) // nb]
|
||||
c = cell(q)
|
||||
print(f" required [{q[0]['required']:>7.2f},{q[-1]['required']:>7.2f}] "
|
||||
f"meanReq={c['req']:>7.2f} meanApp={c['app']:>7.2f} "
|
||||
f"hit%={100*c['hit']:>5.2f} n={c['n']:>5}")
|
||||
|
||||
print("\n" + "=" * 132)
|
||||
print("DIRECT ANSWER -- does capture degrade with range / power?")
|
||||
print("=" * 132)
|
||||
print(f"{'range band':<12} {'n':>6} {'capSlp':>7} {'capLin':>7} {'us/lin':>7} "
|
||||
f"{'medcap':>7} {'|req|':>6} {'|app|':>6} {'|err|px':>8} {'tol':>5} {'|err|/tol':>9} {'hit%':>6}")
|
||||
for lab, sh in cells:
|
||||
if not sh:
|
||||
continue
|
||||
c = cell(sh)
|
||||
ratio = c["ae_px"] / (c["tol"] * math.pi / 180.0 * c["range"])
|
||||
areq = mean([abs(s["required"]) for s in sh])
|
||||
aapp = mean([abs(s["applied"]) for s in sh])
|
||||
print(f"{lab:<12} {c['n']:>6} {c['cap_slope']:>7.3f} {c['cap_lin_slope']:>7.3f} "
|
||||
f"{c['cap_slope']/c['cap_lin_slope'] if c['cap_lin_slope'] else float('nan'):>7.2f} "
|
||||
f"{c['cap_med']:>7.3f} {areq:>6.2f} {aapp:>6.2f} {c['ae_px']:>8.1f} {c['tol']:>5.2f} "
|
||||
f"{ratio:>9.2f} {100*c['hit']:>5.2f}")
|
||||
print("\n(capSlp = our proportional lead capture; capLin = naive linear predictor control;")
|
||||
print(" us/lin = our capture / the trivial-predictor ceiling; |req|/|app| = mean ABSOLUTE")
|
||||
print(" lead magnitude in deg; |err|/tol = mean lead error px / 18px)")
|
||||
print("\nPOWER WITHIN 450+ BAND, enemy energy >= 5 (removes the endgame confound):")
|
||||
sub = [s for s in allshots if s["band"] == "450+" and s["enemy_energy"] >= 5.0]
|
||||
for i in range(len(PWR_LABELS)):
|
||||
g = [s for s in sub if s["pbin"] == i]
|
||||
if not g:
|
||||
continue
|
||||
c = cell(g)
|
||||
print(f" power {PWR_LABELS[i]:<11} n={c['n']:>6} meanP={mean([s['power'] for s in g]):>5.2f} "
|
||||
f"meanRange={c['range']:>4.0f} capSlp={c['cap_slope']:>6.3f} "
|
||||
f"capLin={c['cap_lin_slope']:>6.3f} |err|px={c['ae_px']:>6.1f} hit%={100*c['hit']:>5.2f}")
|
||||
|
||||
if args.powtest:
|
||||
pruns = discover_powtest(args.powtest)
|
||||
if pruns:
|
||||
pshots = [s for rn in pruns for s in rn.shots()]
|
||||
print("\n" + "=" * 132)
|
||||
print(f"ROBUSTNESS CROSS-CHECK -- powtest corpus ({len(pruns)} runs, "
|
||||
f"{len(pshots)} shots, DIFFERENT binary)")
|
||||
print("=" * 132)
|
||||
pc = [(lab, [s for s in pshots if s["band"] == lab]) for lab in BAND_LABELS]
|
||||
table(pc, "by range band (powtest)")
|
||||
|
||||
if args.json:
|
||||
blob = dict(
|
||||
corpus=args.tfil, runs=len(runs), shots=len(allshots),
|
||||
hits=len(hits), misses=len(misses),
|
||||
excl=sum(1 for s in allshots if s["capture"] is None),
|
||||
death_ok=death_ok, death_n=death_n, amb=amb,
|
||||
overall_hit=mean([s["hit"] for s in allshots]),
|
||||
hit_ae=mean([abs(s["leaderr"]) for s in hits]),
|
||||
miss_ae=mean([abs(s["leaderr"]) for s in misses]),
|
||||
hit_miss_px=mean([s["miss_px"] for s in hits]),
|
||||
miss_miss_px=mean([s["miss_px"] for s in misses]),
|
||||
pwr_hist={sd: dict(pow_all[sd]) for sd in pow_all},
|
||||
bands={lab: cell([s for s in allshots if s["band"] == lab]) for lab in BAND_LABELS},
|
||||
bands_enemy_alive={lab: cell([s for s in allshots if s["band"] == lab
|
||||
and s["enemy_energy"] >= 5.0])
|
||||
for lab in BAND_LABELS},
|
||||
band_power_alive={lab: {PWR_LABELS[i]: cell([s for s in allshots
|
||||
if s["band"] == lab
|
||||
and s["pbin"] == i
|
||||
and s["enemy_energy"] >= 5.0])
|
||||
for i in range(len(PWR_LABELS))}
|
||||
for lab in BAND_LABELS},
|
||||
hit_by_enemy_energy={f"{lo}-{hi}": cell([s for s in allshots
|
||||
if s["band"] == "450+"
|
||||
and lo <= s["enemy_energy"] < hi])
|
||||
for lo, hi in ((0, 2), (2, 5), (5, 10), (10, 20), (20, 40), (40, 150))},
|
||||
band_power={lab: {PWR_LABELS[i]: cell([s for s in allshots
|
||||
if s["band"] == lab and s["pbin"] == i])
|
||||
for i in range(len(PWR_LABELS))} for lab in BAND_LABELS},
|
||||
)
|
||||
with open(args.json, "w") as f:
|
||||
json.dump(blob, f, indent=2)
|
||||
print(f"\n[json] wrote {args.json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,213 @@
|
||||
====================================================================================================================================
|
||||
LEAD CAPTURE BY RANGE -- live ModularBot vs real DrussGT
|
||||
====================================================================================================================================
|
||||
corpus : /tmp/tfil_ab2/out
|
||||
battles (runs) : 70
|
||||
per-run owner maps: {('e', 's'): 70} (expect all ('e','s'))
|
||||
runs with a broken owner map : 0
|
||||
fire events w/ ambiguous position : 12112
|
||||
death events side-validated : 496/496
|
||||
side s = US (ModularBot) n= 56242 top powers: [(1.0, 32044), (0.5, 14556), (0.1, 807), (0.51, 225), (0.55, 208), (0.53, 199)]
|
||||
side e = DrussGT n= 67065 top powers: [(0.15, 27209), (0.95, 22789), (0.45, 5274), (0.25, 3071), (0.65, 2862), (0.35, 2340)]
|
||||
|
||||
====================================================================================================================================
|
||||
SAMPLE
|
||||
====================================================================================================================================
|
||||
our shots (aligned, with an interception) : 54926
|
||||
server hit / hitwall / hitbullet / other : 5480 / 47101 / 1203 / 1142
|
||||
capture excluded (|required| < 2.0 px lateral) : 887
|
||||
|
||||
====================================================================================================================================
|
||||
GEOMETRY VALIDATION (hits must separate from misses)
|
||||
====================================================================================================================================
|
||||
n |leadErr|deg |leadErr|px misPx mean misPx med <18px
|
||||
HITS 5480 1.478 11.4 11.6 10.8 80.8%
|
||||
MISSES 48304 16.724 141.3 134.1 123.7 0.8%
|
||||
|
||||
miss separation ratio (misses/hits) = 11.59x -> OK (bearing recovery validated)
|
||||
overall measured hit rate = 9.98%
|
||||
|
||||
====================================================================================================================================
|
||||
MAIN TABLE -- all our shots, by RANGE band
|
||||
====================================================================================================================================
|
||||
reqL/appL = mean signed lead vs LOS (deg). capt = mean ratio, capSlp = robust proportional
|
||||
capture, capLin = the same slope for a naive linear predictor (control).
|
||||
|
||||
by range band
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
0-100 27 1 81 10.3 1.51 -0.23 0.72 0.401 0.597 16.76 11.13 23.2 13.01 44.00
|
||||
100-200 274 14 162 14.7 -0.95 2.23 0.07 0.391 0.600 17.74 13.38 50.1 6.57 25.28
|
||||
200-300 990 30 261 17.9 -1.67 0.37 0.04 0.246 0.428 16.65 11.97 75.8 3.99 16.04
|
||||
300-450 17178 259 405 24.5 -0.56 0.40 0.01 0.198 0.404 15.98 11.27 112.9 2.56 11.79
|
||||
450+ 36457 583 535 31.0 0.19 -0.44 0.15 0.135 0.291 14.71 10.36 137.0 1.95 9.14
|
||||
|
||||
====================================================================================================================================
|
||||
POWER WITHIN RANGE BAND -- this is what separates 'range' from 'power'
|
||||
====================================================================================================================================
|
||||
|
||||
range 0-100
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
0-100 0.50-0.75 6 1 83 6.5 -4.82 -1.09 0.41 0.448 0.669 12.35 3.75 17.7 12.61 66.67
|
||||
0-100 1.00-1.50 2 0 77 7.5 -28.19 -7.90 0.27 0.292 0.730 20.29 10.29 27.8 13.17 50.00
|
||||
0-100 >=1.50 19 0 80 11.8 6.63 0.85 0.84 0.405 0.574 17.78 12.32 24.4 13.12 35.29
|
||||
|
||||
range 100-200
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
100-200 <0.50 5 0 161 10.4 4.77 -1.52 -0.08 0.436 0.495 14.84 8.18 41.5 6.48 20.00
|
||||
100-200 0.50-0.75 53 12 166 10.8 1.78 5.83 0.04 0.481 0.567 14.31 8.72 41.2 6.38 30.77
|
||||
100-200 0.75-1.00 5 0 177 11.2 0.60 -4.71 -1.53 0.527 0.394 11.72 7.59 35.6 5.89 40.00
|
||||
100-200 1.00-1.50 23 0 168 12.5 -0.21 6.11 -0.04 0.442 0.616 14.50 11.98 46.5 6.47 42.86
|
||||
100-200 >=1.50 188 2 159 16.3 -2.00 1.02 0.13 0.375 0.606 19.34 14.50 53.7 6.65 21.43
|
||||
|
||||
range 200-300
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
200-300 <0.50 6 0 257 15.7 -0.40 -3.70 0.06 0.512 0.839 10.39 8.54 50.9 4.07 50.00
|
||||
200-300 0.50-0.75 130 12 267 16.1 -1.54 1.25 -0.40 0.202 0.415 15.92 10.93 74.3 3.90 18.55
|
||||
200-300 0.75-1.00 21 4 263 17.1 -3.60 3.14 0.34 0.135 -0.015 17.32 10.06 79.1 3.96 11.11
|
||||
200-300 1.00-1.50 780 13 261 17.8 -1.29 0.29 0.08 0.287 0.462 16.04 11.33 73.0 3.99 15.92
|
||||
200-300 >=1.50 53 1 248 25.3 -6.97 -1.28 0.29 0.063 0.291 27.83 17.66 122.1 4.20 8.70
|
||||
|
||||
range 300-450
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
300-450 <0.50 333 63 409 22.0 -0.93 0.92 -0.67 0.094 0.493 16.30 9.09 116.4 2.54 3.07
|
||||
300-450 0.50-0.75 5268 82 411 23.4 0.12 0.75 0.25 0.216 0.431 15.16 10.68 108.9 2.52 11.65
|
||||
300-450 0.75-1.00 880 12 408 24.7 -0.72 -0.36 -0.19 0.203 0.458 16.00 11.28 113.6 2.55 13.14
|
||||
300-450 1.00-1.50 10696 102 402 25.0 -0.86 0.27 -0.07 0.191 0.386 16.38 11.59 114.8 2.59 12.02
|
||||
300-450 >=1.50 1 0 312 22.0 -7.39 22.61 -3.06 -3.062 -3.217 30.00 nan 163.1 3.31 0.00
|
||||
|
||||
range 450+
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
450+ <0.50 950 120 537 28.2 0.80 0.67 0.02 0.106 0.302 14.61 8.72 136.8 1.94 1.18
|
||||
450+ 0.50-0.75 12957 205 536 29.6 0.32 -0.12 0.13 0.151 0.314 14.04 9.96 131.1 1.94 10.20
|
||||
450+ 0.75-1.00 2424 28 540 31.6 0.52 -0.41 0.01 0.132 0.284 14.92 10.42 140.6 1.93 9.50
|
||||
450+ 1.00-1.50 20126 230 534 32.0 0.05 -0.71 0.18 0.126 0.279 15.11 10.66 140.4 1.95 8.80
|
||||
|
||||
====================================================================================================================================
|
||||
CONFOUND CHECK -- the sub-0.5-power shots at long range are the ENDGAME finish
|
||||
====================================================================================================================================
|
||||
hit% of our 450+ shots, by the ENEMY's remaining energy at the fire tick:
|
||||
enemy energy [ 0, 2) n= 1017 meanP= 0.22 capSlp= 0.111 hit%= 1.10
|
||||
enemy energy [ 2, 5) n= 1076 meanP= 0.54 capSlp= 0.128 hit%= 8.55
|
||||
enemy energy [ 5, 10) n= 2614 meanP= 0.59 capSlp= 0.135 hit%= 9.69
|
||||
enemy energy [ 10, 20) n= 6758 meanP= 0.63 capSlp= 0.146 hit%= 9.85
|
||||
enemy energy [ 20, 40) n= 10733 meanP= 0.83 capSlp= 0.147 hit%= 9.43
|
||||
enemy energy [ 40,150) n= 14259 meanP= 0.97 capSlp= 0.122 hit%= 9.12
|
||||
|
||||
same table, EXCLUDING shots fired while the enemy still has >= 5 energy
|
||||
(this removes the near-dead-target endgame and leaves the clean power comparison):
|
||||
|
||||
range 0-100 (enemy energy >= 5)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
0-100 0.50-0.75 5 0 80 6.6 -5.78 -4.34 0.41 0.448 0.669 11.80 3.88 16.2 13.00 80.00
|
||||
0-100 1.00-1.50 2 0 77 7.5 -28.19 -7.90 0.27 0.292 0.730 20.29 10.29 27.8 13.17 50.00
|
||||
0-100 >=1.50 16 0 81 11.3 5.37 0.41 0.98 0.503 0.605 14.99 11.43 20.8 13.05 40.00
|
||||
|
||||
range 100-200 (enemy energy >= 5)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
100-200 0.50-0.75 42 2 168 11.2 2.51 2.50 0.03 0.481 0.573 12.93 8.67 37.8 6.33 35.71
|
||||
100-200 0.75-1.00 5 0 177 11.2 0.60 -4.71 -1.53 0.527 0.394 11.72 7.59 35.6 5.89 40.00
|
||||
100-200 1.00-1.50 23 0 168 12.5 -0.21 6.11 -0.04 0.442 0.616 14.50 11.98 46.5 6.47 42.86
|
||||
100-200 >=1.50 175 2 161 16.3 -2.60 1.10 0.05 0.384 0.590 19.19 14.48 54.1 6.57 22.35
|
||||
|
||||
range 200-300 (enemy energy >= 5)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
200-300 <0.50 6 0 257 15.7 -0.40 -3.70 0.06 0.512 0.839 10.39 8.54 50.9 4.07 50.00
|
||||
200-300 0.50-0.75 119 2 267 16.2 -1.88 -0.63 -0.41 0.192 0.412 15.49 11.23 72.5 3.89 20.35
|
||||
200-300 0.75-1.00 17 0 265 17.5 -4.44 -0.78 0.34 0.135 -0.015 16.75 10.98 77.4 3.93 14.29
|
||||
200-300 1.00-1.50 776 9 261 17.8 -1.30 0.17 0.08 0.287 0.462 16.00 11.34 72.8 3.98 16.01
|
||||
200-300 >=1.50 40 0 249 25.5 -3.41 -0.27 0.03 0.061 0.290 26.76 18.77 118.8 4.19 11.11
|
||||
|
||||
range 300-450 (enemy energy >= 5)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
300-450 <0.50 4 0 381 21.5 2.94 -0.28 -1.41 1.202 1.280 17.26 10.28 109.0 2.76 25.00
|
||||
300-450 0.50-0.75 4825 54 412 23.5 0.14 0.58 0.24 0.220 0.430 15.08 10.64 108.4 2.52 11.82
|
||||
300-450 0.75-1.00 860 9 408 24.8 -0.72 -0.53 -0.19 0.205 0.453 15.94 11.27 113.3 2.54 13.32
|
||||
300-450 1.00-1.50 10681 99 402 25.0 -0.87 0.26 -0.07 0.191 0.385 16.38 11.60 114.8 2.59 12.03
|
||||
300-450 >=1.50 1 0 312 22.0 -7.39 22.61 -3.06 -3.062 -3.217 30.00 nan 163.1 3.31 0.00
|
||||
|
||||
range 450+ (enemy energy >= 5)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
450+ <0.50 4 0 519 26.8 -1.51 -11.72 -5.43 -3.021 -1.809 26.98 6.38 247.2 2.00 0.00
|
||||
450+ 0.50-0.75 11882 137 535 29.5 0.31 -0.21 0.16 0.154 0.312 13.97 9.96 130.3 1.95 10.39
|
||||
450+ 0.75-1.00 2377 28 540 31.6 0.47 -0.45 0.01 0.127 0.275 14.94 10.41 140.7 1.93 9.60
|
||||
450+ 1.00-1.50 20101 230 534 32.0 0.04 -0.71 0.18 0.127 0.279 15.11 10.66 140.4 1.95 8.80
|
||||
|
||||
====================================================================================================================================
|
||||
RESPONSE CURVE -- do we move the aim in proportion to the required lead?
|
||||
====================================================================================================================================
|
||||
|
||||
range 200-300 (deciles of required lead -> mean applied lead):
|
||||
required [ -46.11, -23.72] meanReq= -27.79 meanApp= -9.18 hit%=19.67 n= 123
|
||||
required [ -23.71, -16.96] meanReq= -19.97 meanApp= -1.30 hit%=14.53 n= 124
|
||||
required [ -16.83, -11.27] meanReq= -13.99 meanApp= -0.85 hit%=14.29 n= 124
|
||||
required [ -11.23, -2.84] meanReq= -7.43 meanApp= 0.34 hit%=14.41 n= 124
|
||||
required [ -2.76, 5.10] meanReq= 1.01 meanApp= 1.67 hit%= 9.24 n= 123
|
||||
required [ 5.11, 13.22] meanReq= 9.12 meanApp= -3.36 hit%=14.88 n= 124
|
||||
required [ 13.23, 23.92] meanReq= 18.32 meanApp= 3.04 hit%=11.76 n= 124
|
||||
required [ 23.94, 45.18] meanReq= 27.20 meanApp= 12.55 hit%=29.41 n= 124
|
||||
|
||||
range 300-450 (deciles of required lead -> mean applied lead):
|
||||
required [ -28.84, -19.87] meanReq= -24.21 meanApp= -6.04 hit%=13.39 n= 2147
|
||||
required [ -19.87, -13.44] meanReq= -16.43 meanApp= -0.20 hit%= 9.09 n= 2147
|
||||
required [ -13.44, -7.81] meanReq= -10.63 meanApp= -0.19 hit%=10.22 n= 2147
|
||||
required [ -7.80, -1.44] meanReq= -4.66 meanApp= -0.12 hit%=11.82 n= 2148
|
||||
required [ -1.44, 5.26] meanReq= 1.83 meanApp= 0.34 hit%= 9.96 n= 2147
|
||||
required [ 5.26, 12.33] meanReq= 8.84 meanApp= 0.39 hit%= 9.00 n= 2147
|
||||
required [ 12.33, 20.39] meanReq= 16.05 meanApp= 0.44 hit%= 9.60 n= 2147
|
||||
required [ 20.39, 28.98] meanReq= 24.76 meanApp= 8.54 hit%=21.20 n= 2148
|
||||
|
||||
range 450+ (deciles of required lead -> mean applied lead):
|
||||
required [ -28.60, -17.03] meanReq= -21.60 meanApp= -4.09 hit%= 9.27 n= 4557
|
||||
required [ -17.03, -10.56] meanReq= -13.70 meanApp= -1.51 hit%= 7.92 n= 4557
|
||||
required [ -10.56, -5.13] meanReq= -7.79 meanApp= -1.00 hit%=10.29 n= 4557
|
||||
required [ -5.13, 0.00] meanReq= -2.51 meanApp= -0.55 hit%=10.08 n= 4557
|
||||
required [ 0.00, 5.27] meanReq= 2.50 meanApp= 0.36 hit%= 9.95 n= 4557
|
||||
required [ 5.27, 11.06] meanReq= 8.12 meanApp= -0.16 hit%= 8.75 n= 4557
|
||||
required [ 11.06, 17.66] meanReq= 14.26 meanApp= 0.14 hit%= 7.22 n= 4557
|
||||
required [ 17.66, 28.54] meanReq= 22.26 meanApp= 3.26 hit%= 9.69 n= 4558
|
||||
|
||||
====================================================================================================================================
|
||||
DIRECT ANSWER -- does capture degrade with range / power?
|
||||
====================================================================================================================================
|
||||
range band n capSlp capLin us/lin medcap |req| |app| |err|px tol |err|/tol hit%
|
||||
0-100 27 0.401 0.597 0.67 0.450 24.77 13.38 23.2 13.01 1.27 44.00
|
||||
100-200 274 0.391 0.600 0.65 0.442 20.21 16.95 50.1 6.57 2.70 25.28
|
||||
200-300 990 0.246 0.428 0.58 0.238 15.71 13.84 75.8 3.99 4.17 16.04
|
||||
300-450 17178 0.198 0.404 0.49 0.174 13.46 13.09 112.9 2.56 6.23 11.79
|
||||
450+ 36457 0.135 0.291 0.46 0.120 11.59 11.43 137.0 1.95 7.54 9.14
|
||||
|
||||
(capSlp = our proportional lead capture; capLin = naive linear predictor control;
|
||||
us/lin = our capture / the trivial-predictor ceiling; |req|/|app| = mean ABSOLUTE
|
||||
lead magnitude in deg; |err|/tol = mean lead error px / 18px)
|
||||
|
||||
POWER WITHIN 450+ BAND, enemy energy >= 5 (removes the endgame confound):
|
||||
power <0.50 n= 4 meanP= 0.10 meanRange= 519 capSlp=-3.021 capLin=-1.809 |err|px= 247.2 hit%= 0.00
|
||||
power 0.50-0.75 n= 11882 meanP= 0.53 meanRange= 535 capSlp= 0.154 capLin= 0.312 |err|px= 130.3 hit%=10.39
|
||||
power 0.75-1.00 n= 2377 meanP= 0.87 meanRange= 540 capSlp= 0.127 capLin= 0.275 |err|px= 140.7 hit%= 9.60
|
||||
power 1.00-1.50 n= 20101 meanP= 1.00 meanRange= 534 capSlp= 0.127 capLin= 0.279 |err|px= 140.4 hit%= 8.80
|
||||
|
||||
====================================================================================================================================
|
||||
ROBUSTNESS CROSS-CHECK -- powtest corpus (35 runs, 24277 shots, DIFFERENT binary)
|
||||
====================================================================================================================================
|
||||
|
||||
by range band (powtest)
|
||||
cell n excl range flt reqL appL capt capSlp capLin |err| +/- |err|px tol hit%
|
||||
-------------------------------------------------------------------------------------------------------------------------
|
||||
0-100 6 0 91 11.2 -6.18 -0.00 0.60 0.529 0.496 14.10 10.10 20.8 11.38 60.00
|
||||
100-200 83 0 170 15.8 -3.28 -2.27 0.89 0.354 0.470 19.49 14.92 58.1 6.16 23.46
|
||||
200-300 442 3 260 17.6 -2.70 0.80 -0.02 0.283 0.380 16.47 11.92 74.2 4.01 16.55
|
||||
300-450 9843 102 405 24.9 -1.13 0.09 0.05 0.219 0.417 16.12 11.52 114.1 2.56 11.90
|
||||
450+ 13903 143 525 30.9 0.12 -0.88 0.18 0.136 0.301 15.14 10.80 138.6 1.98 9.24
|
||||
|
||||
[json] wrote common_libs/tests/fixtures/lead_capture_by_range_results.json
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,307 @@
|
||||
# Lead capture by range: how much of the required lead do we actually apply?
|
||||
|
||||
**Question (the user's hypothesis).** *"DrussGT is dodging more when low power only
|
||||
because DrussGT is moving faster, more far away from the head-on and our displacement
|
||||
capacity, so we try to hit him but we miss as we are not capable of reaching the
|
||||
correct angle."*
|
||||
|
||||
**Answer: half right, and the half that is wrong matters.** We genuinely **do not
|
||||
apply the required lead, and the shortfall grows with range** — capture falls from
|
||||
**0.40 at 0–200 px to 0.135 at 450+ px** on 54 926 shots, so at long range we put
|
||||
**~13 %** of the lead a perfect gun would need into the bullet, and miss by ~137 px
|
||||
against an 18 px bot. But this is a **RANGE** effect, **not a POWER** effect: inside a
|
||||
range band, capture is the same at 0.5 power as at 1.0 power. Low-power shots miss
|
||||
more because *they are long-range shots*, plus a separate endgame confound, **not**
|
||||
because low power costs us displacement capacity.
|
||||
|
||||
The other half: capture below 1.0 is **partly unavoidable**. `requiredLead` uses the
|
||||
enemy's *actual* future dodge (perfect information), which no real gun can know. A
|
||||
trivial straight-line predictor — the same bullet, aimed at the intercept of the
|
||||
enemy's last 4 ticks of velocity — captures only **0.29–0.60**. We capture **46–67 %**
|
||||
of that trivial ceiling. So the under-lead is real and fixable, but roughly half of
|
||||
the distance from us to "perfect lead" is the dodger's intrinsic unpredictability.
|
||||
|
||||
Measured on **70 real live battles / 490 rounds / 54 926 shots** against the real,
|
||||
unmodified DrussGT (`/tmp/tfil_ab2/out/`), replicated on **35 more battles / 24 277
|
||||
shots** of a *different* ModularBot build (`/tmp/powtest/`). All live Tank Royale
|
||||
battles recorded through `tools/robocode_shim/run_bridge_battle.sh`; no offline
|
||||
fixture replay.
|
||||
|
||||
---
|
||||
|
||||
## 1. The decomposition
|
||||
|
||||
For every shot **we (ModularBot)** fire at DrussGT, with power `p`, all angles in
|
||||
degrees relative to the **line of sight (LOS) at the fire tick**:
|
||||
|
||||
* `O` = the firing tank's centre at the fire tick (= the bullet-line origin; the
|
||||
server's fire `(x,y)` is the tank centre to 0.02 px, verified);
|
||||
* `v = 20 − 3p` px/tick, so **low power is a faster bullet**;
|
||||
* `t*` = the **aim-independent** interception tick: the first tick `k` with
|
||||
`|E(t₀+k) − O| ≤ v·k`, where `E` is DrussGT's recorded true position. It depends
|
||||
only on the enemy's truth and the bullet speed, **never on our aim**;
|
||||
* **`requiredLead`** = `bearing(O → E(t*)) − LOS`;
|
||||
* **`appliedLead`** = our bullet's server-recorded bearing `− LOS`;
|
||||
* **`leadError`** = `appliedLead − requiredLead` (wrapped to ±180°);
|
||||
* **`capture`** = `appliedLead / requiredLead`.
|
||||
|
||||
`capture` is guarded: it is defined only when the required lateral lead is at least
|
||||
**2 px** at the fire range (`|requiredLead|·range ≥ 2 px`). This excludes **887 of
|
||||
54 926 shots (1.6 %)**. The mean of the *ratio* is a noisy statistic (small
|
||||
denominators); the headline capture is therefore the **proportional slope**
|
||||
`Σ(applied·required)/Σ(required²)` computed cell-by-cell, labelled `capSlp`. The mean
|
||||
ratio (`capt`) and its median (`medcap`) are printed too and tell the same story.
|
||||
|
||||
Arrival-adjacent quantities use two ticks, both non-circular:
|
||||
|
||||
* `miss_px` = perpendicular distance between DrussGT's true position and our bullet's
|
||||
real line at the tick the bullet reaches DrussGT's along-track plane — the **same
|
||||
definition used in `docs/drussgt_dodge_vs_power.md`**, so the validation numbers
|
||||
match that job exactly;
|
||||
* `flight` = `t*` (ticks in the air).
|
||||
|
||||
### Attribution (stated explicitly, this has bitten the project before)
|
||||
|
||||
* In the capture rows **`e*` is the SUBJECT = DrussGT** and **`s*` is the adversary =
|
||||
ModularBot = us**, by construction of
|
||||
`tools/robocode_shim/src/robocode_shim/TrBattleCapture.java` (`en` = the bot whose
|
||||
name contains "DrussGT", written `e*`; `sh` = the other, written `s*`).
|
||||
* The event sidecar's `owner` id is a Tank Royale id and **is not stable across runs**.
|
||||
It is recovered **per battle** from the fire geometry: the owner's position equals
|
||||
the fire event's `(x,y)` and its energy drops by exactly `power` on the next capture
|
||||
row. **70/70 battles** resolve to the two sides `('e','s')`.
|
||||
* Cross-check: for **496/496 death events** the mapped victim is the bot whose energy
|
||||
is ~0 at the end of that round.
|
||||
* Fingerprint check on the way round: our recovered side fires
|
||||
`{1.00 ×32044, 0.50 ×14556, 0.10 ×807, …}` — the documented ModularBot policy
|
||||
(spike at `TR_POWER_ENERGY_MIN = 0.5` under 20 energy, spike at the 1.0 range cap
|
||||
beyond `TR_POWER_FAR_DIST = 200`, linear slope between). DrussGT's recovered side
|
||||
fires `{0.15 ×27209, 0.95 ×22789, 0.45 ×5274, …}` — a completely different,
|
||||
distance-quantised curve. **No power-value heuristic is used anywhere** (the old
|
||||
`{1.0, 1.5, 2.0, 3.0}` assumption is exactly what mis-attributed ModularBot in a
|
||||
previous job).
|
||||
|
||||
Because the four-decimal positions repeat when a tank is stationary, 12 112 fire
|
||||
events have more than one *position* match inside the ±8-tick search window; the
|
||||
energy-drop term of the match disambiguates all of them, and the 496/496 death check
|
||||
plus the hit geometry below validate the result end to end.
|
||||
|
||||
---
|
||||
|
||||
## 2. Geometry validation: hits separate from misses
|
||||
|
||||
| | n | mean |leadError| (deg) | mean |leadError| (px) | miss_px mean | miss_px median | fraction < 18 px |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **HITS** (server truth) | 5 480 | **1.478** | 11.4 | **11.6** | 10.8 | **80.8 %** |
|
||||
| **MISSES** (hitwall/hitbullet) | 48 304 | **16.724** | 141.3 | **134.1** | 123.7 | **0.8 %** |
|
||||
|
||||
Miss separation ratio (misses/hits) = **11.59×**. The hits show a mean miss of
|
||||
**11.6 px** and **80.8 % inside 18 px**, matching the previously recorded live
|
||||
measurement (11.6 px, 80.6 %) exactly — the bearing recovery is correct. Overall
|
||||
measured hit rate **9.98 %**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Main result: by range band
|
||||
|
||||
`capt` = mean of the ratio, `capSlp` = proportional capture (robust), `capLin` = the
|
||||
same slope for the **naive linear predictor control**, `|req|`/`|app|` = mean *absolute*
|
||||
lead magnitude in degrees, `tol` = angular tolerance `atan(18/range)`.
|
||||
|
||||
| range | n | range px | flight | |req|° | |app|° | capt | **capSlp** | capLin | |err|° | |err|px | tol° | |err|/tol | hit % |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 0–100 | 27 | 81 | 10.3 | 24.8 | 13.4 | 0.72 | **0.401** | 0.597 | 16.8 | 23.2 | 13.01 | 1.27 | 44.00 |
|
||||
| 100–200 | 274 | 162 | 14.7 | 20.2 | 17.0 | 0.07 | **0.391** | 0.600 | 17.7 | 50.1 | 6.57 | 2.70 | 25.28 |
|
||||
| 200–300 | 990 | 261 | 17.9 | 15.7 | 13.8 | 0.04 | **0.246** | 0.428 | 16.7 | 75.8 | 3.99 | 4.17 | 16.04 |
|
||||
| 300–450 | 17 178 | 405 | 24.5 | 13.5 | 13.1 | 0.01 | **0.198** | 0.404 | 16.0 | 112.9 | 2.56 | 6.23 | 11.79 |
|
||||
| 450+ | 36 457 | 535 | 31.0 | 11.6 | 11.4 | 0.15 | **0.135** | 0.291 | 14.7 | 137.0 | 1.95 | 7.54 | 9.14 |
|
||||
|
||||
Read this table twice. Two things happen at once and they are different:
|
||||
|
||||
1. **The angular error `|leadError|` is roughly constant (~15–18°) at every range**
|
||||
(it does not shrink with distance).
|
||||
2. **The window shrinks**: the 18 px bot spans 13° at 100 px but only 1.95° at 500 px,
|
||||
because `tol = atan(18/range)`.
|
||||
|
||||
So `|err|/tol` grows from **1.27 to 7.54**: the same angular miss that was survivable
|
||||
up close becomes fatal at distance. This is the "tighter angular window" half of the
|
||||
story — but it is *not* the whole story, because `capSlp` genuinely **falls with range**
|
||||
(0.40 → 0.135). We are not applying a constant fraction of a shrinking lead; we are
|
||||
applying a *shrinking fraction* of a roughly constant-magnitude lead.
|
||||
|
||||
---
|
||||
|
||||
## 4. Power does NOT change capture (the 'range vs power' split)
|
||||
|
||||
Within the 450+ band, restricting to shots fired while **DrussGT still has ≥ 5 energy**
|
||||
(which removes the endgame confound of §5):
|
||||
|
||||
| 450+, enemy alive | n | mean p | range px | **capSlp** | capLin | |err|px | hit % |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 0.50–0.75 | 11 882 | 0.53 | 535 | **0.154** | 0.312 | 130.3 | 10.39 |
|
||||
| 0.75–1.00 | 2 377 | 0.87 | 540 | **0.127** | 0.275 | 140.7 | 9.60 |
|
||||
| 1.00–1.50 | 20 101 | 1.00 | 534 | **0.127** | 0.279 | 140.4 | 8.80 |
|
||||
|
||||
**Flat.** Same result in the 300–450 band (`capSlp` 0.220 / 0.205 / 0.191 for the same
|
||||
three power bins). At 450+ our policy fires only 0.50 or 1.00 in this regime, so the
|
||||
comparison is exactly "our fastest long-range bullet" (p = 0.5, v = 18.5, flight 29.5
|
||||
ticks) versus "our standard bullet" (p = 1.0, v = 17, flight 32 ticks). The faster
|
||||
low-power bullet has a **shorter** flight and a **smaller** required lead by
|
||||
construction, and we capture **the same fraction** of it. Power is not the driver.
|
||||
|
||||
---
|
||||
|
||||
## 5. The endgame confound (why raw per-power hit rates lie)
|
||||
|
||||
Our power policy fires **sub-0.5 power only to finish a nearly-dead enemy**. At 450+,
|
||||
**944 of the 950 sub-0.5 shots have the enemy's energy below 2** (mean **1.1**), because
|
||||
`prFinishKill` cuts the power when the enemy's remaining energy is already tiny. Those
|
||||
shots hit at **1.18 %**, which looks like a catastrophic low-power failure — it is not:
|
||||
|
||||
| 450+, by enemy energy at the fire tick | n | mean p | capSlp | hit % |
|
||||
|---|---|---|---|---|
|
||||
| [0, 2) | 1 017 | 0.22 | 0.111 | **1.10** |
|
||||
| [2, 5) | 1 076 | 0.54 | 0.128 | 8.55 |
|
||||
| [5, 10) | 2 614 | 0.59 | 0.135 | 9.69 |
|
||||
| [10, 20) | 6 758 | 0.63 | 0.146 | 9.85 |
|
||||
| [20, 40) | 10 733 | 0.83 | 0.147 | 9.43 |
|
||||
| [40, 150) | 14 259 | 0.97 | 0.122 | 9.12 |
|
||||
|
||||
The hit rate collapses **only when the target is already effectively dead**, and it
|
||||
collapses for **high-power shots too** (73 shots at p ≥ 0.5 against a < 2-energy
|
||||
DrussGT hit 0.00 %). Every other enemy-energy stratum sits at 8.5–9.9 %. So the
|
||||
"low power hits 1 %" reading is a *dead-target* artifact, not a lead-capture failure.
|
||||
Capture itself is flat across enemy energy (0.111 → 0.147 → 0.122).
|
||||
|
||||
---
|
||||
|
||||
## 6. Do we move the aim in proportion to the required lead? (response curve)
|
||||
|
||||
At 450+, split the shots into deciles of `requiredLead` and look at the **mean applied
|
||||
lead** in each:
|
||||
|
||||
| requiredLead decile (mean) | −21.6 | −13.7 | −7.8 | −2.5 | +2.5 | +8.1 | +14.3 | +22.3 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **mean appliedLead** | −4.09 | −1.51 | −1.00 | −0.55 | +0.36 | −0.16 | +0.14 | +3.26 |
|
||||
| hit % | 9.27 | 7.92 | 10.29 | 10.08 | 9.95 | 8.75 | 7.22 | 9.69 |
|
||||
|
||||
The required lead swings across **±22°**, and our mean applied lead responds by about
|
||||
**±4°** — and the hit rate does **not** depend on the size of the required lead at all.
|
||||
This is the cleanest single number in the report: the bullet is aimed, to first order,
|
||||
**at the line of sight, not at the interception point**. Consistently,
|
||||
`mean|appliedLead| ≈ mean|requiredLead|` (11.4° vs 11.6° at 450+) — we have as much
|
||||
*dispersion* of aim as the target has motion, but almost **no correlation** with it
|
||||
(`capSlp = 0.135`).
|
||||
|
||||
---
|
||||
|
||||
## 7. The ceiling: what a trivial predictor would do
|
||||
|
||||
`requiredLead` is an **oracle** (it uses DrussGT's actual future dodge). To separate
|
||||
"our gun is bad" from "the dodge is unpredictable", the analyzer also computes a
|
||||
**naive linear-predictor control**: same bullet speed, aimed at the intercept of a
|
||||
straight-line continuation of DrussGT's last 4 ticks of velocity. Its capture
|
||||
(`capLin`) and the ratio are in §3:
|
||||
|
||||
| range | our capSlp | naive-linear capLin | **us / linear** |
|
||||
|---|---|---|---|
|
||||
| 0–100 | 0.401 | 0.597 | 0.67 |
|
||||
| 100–200 | 0.391 | 0.600 | 0.65 |
|
||||
| 200–300 | 0.246 | 0.428 | 0.58 |
|
||||
| 300–450 | 0.198 | 0.404 | 0.49 |
|
||||
| 450+ | 0.135 | 0.291 | **0.46** |
|
||||
|
||||
So a *trivial* predictor still only reaches 0.29 at long range — most of the oracle
|
||||
lead is genuinely unattainable against a strong dodger. But we reach only **46 %** of
|
||||
that trivial benchmark. The under-lead is therefore real and worth fixing, while the
|
||||
gap from the trivial benchmark to 1.0 is the dodger's unpredictability and is not.
|
||||
|
||||
Corroboration: the powtest corpus — a **different** ModularBot binary — reproduces the
|
||||
shape almost exactly (`capSlp` 0.529 / 0.354 / 0.283 / 0.219 / 0.136 by the same range
|
||||
bands). It is a property of the architecture, not of one build.
|
||||
|
||||
---
|
||||
|
||||
## 8. Caveats and sample sizes
|
||||
|
||||
* **The oracle caveat.** `requiredLead` uses perfect future information. `capture = 1.0`
|
||||
is *unattainable* against a bot that dodges; it is a diagnostic, not a target. Compare
|
||||
our capture to the `capLin` control, not to 1.0.
|
||||
* **Weak correlation, large dispersion.** At long range our applied lead is essentially
|
||||
an aim with mean 0 and ~11.5° dispersion that is only weakly proportional to the
|
||||
required lead (proportional slope 0.135). "We apply 13 % of the lead"
|
||||
is a proportional-fit statement; per shot, what we actually do is *aim near the LOS
|
||||
with a wide error*.
|
||||
* **Stale live world state.** The recorded `dir` is what the **live** ModularBot fired
|
||||
using its own stale between-scan world state; the capture's positions are perfect
|
||||
truth. The measured `leadError` therefore folds in the bot's scan staleness. That is
|
||||
the correct thing to measure (it is the error the bullet actually carries), but it is
|
||||
not the same as the gun's internal prediction error.
|
||||
* **Stratification.** Power is not randomised: it is assigned by the range cap, the
|
||||
energy slope and the finishing rule. Every power comparison above is made **within a
|
||||
range band** and (in §4) with the endgame removed. The raw cross-range power
|
||||
distribution is reported in the captured output.
|
||||
* **Sample sizes.** The short bands are thin (0–100: n = 27; 100–200: n = 274) — treat
|
||||
their capture as indicative. Everything from 200 px out is large (990 → 36 457).
|
||||
The 450+ sub-0.5 power cell *with the enemy alive* is n = 4 and should be ignored
|
||||
(it is printed for completeness; the real sub-0.5 shots are the endgame of §5).
|
||||
* **Rotation direction.** `requiredLead`/`appliedLead` are signed about the LOS;
|
||||
`capSlp` additionally assumes a proportional (through-origin) relation, which is the
|
||||
right first-order model for a lead gun but not exact for large angles.
|
||||
|
||||
---
|
||||
|
||||
## 9. Verdict
|
||||
|
||||
**MEASURED**
|
||||
|
||||
* Capture falls monotonically with range: **capSlp 0.401 → 0.391 → 0.246 → 0.198 →
|
||||
0.135**. At 450+ we apply **~13 %** of the required lead and miss by **~137 px**
|
||||
against an 18 px bot; `|leadError|/tolerance` grows **1.27 → 7.54**.
|
||||
* Capture is **flat across fired power within a range band**
|
||||
(450+, enemy alive: 0.154 / 0.127 / 0.127 for p = 0.53 / 0.87 / 1.00).
|
||||
* The applied lead barely responds to the required lead: across required-lead deciles
|
||||
spanning ±22°, the mean applied lead moves by ~±4° and the hit rate is flat.
|
||||
* A naive linear predictor captures 0.29–0.60; we capture **46–67 %** of it.
|
||||
* Hits validate the geometry (11.6 px mean, 80.8 % inside 18 px) and separate from
|
||||
misses by **11.6×**; **496/496** deaths and **70/70** owner maps resolve correctly.
|
||||
* The sub-0.5-power long-range shots (1.18 % hit) are **endgame shots at a near-dead
|
||||
DrussGT** (mean enemy energy 1.1), and high-power shots there miss just as much.
|
||||
|
||||
**INFERRED**
|
||||
|
||||
* The gun is, to first order, **a line-of-sight / weak-lead gun against DrussGT**, not
|
||||
an intercept-point gun: the bullet direction tracks the enemy's current line rather
|
||||
than where the enemy will be, which is why capture is low and why it degrades with
|
||||
range while the angular error stays ~constant.
|
||||
* The user's mechanism — "we are not capable of reaching the correct angle at range" —
|
||||
is **confirmed for range**, but **not through power**: low power does not reduce our
|
||||
displacement capacity here (it is a faster bullet and we capture the same fraction).
|
||||
The low-power/long-range association is a policy artifact (the range cap), so
|
||||
*fixing the lead at range fixes the low-power miss rate too*.
|
||||
* Where to look next: the gap between our capture and the trivial-linear control
|
||||
(0.46×) is the actionable, fixable part. The gap from the trivial control to 1.0 is
|
||||
the dodger's unpredictability and should not be chased.
|
||||
|
||||
---
|
||||
|
||||
## 10. Reproducing
|
||||
|
||||
```bash
|
||||
# corpora (live captures; not in the repo, ~500 MB total)
|
||||
# /tmp/tfil_ab2/out/<A..E>/run*.jsonl{,.events.jsonl,.rounds.json}
|
||||
# /tmp/powtest/{cap_<arm>_r<n>.jsonl,events_<arm>_r<n>.json,cap_*_r<n>.jsonl.rounds.json}
|
||||
|
||||
python3 common_libs/tests/analyze_lead_capture_by_range.py \
|
||||
--tfil /tmp/tfil_ab2/out --powtest /tmp/powtest \
|
||||
--json common_libs/tests/fixtures/lead_capture_by_range_results.json \
|
||||
| tee common_libs/tests/fixtures/lead_capture_by_range_output.txt
|
||||
```
|
||||
|
||||
`common_libs/tests/fixtures/lead_capture_by_range_output.txt` is the verbatim captured
|
||||
output every table above is taken from; `..._results.json` is the same numbers as JSON.
|
||||
Runtime ≈ 15 s for both corpora, single-threaded pure Python (no numpy).
|
||||
|
||||
To rebuild a corpus, `tools/robocode_shim/make_botdir.sh /tmp/tr_bots/DrussGT` and then
|
||||
`TR_EVENTS_OUT=<run>.events.jsonl tools/robocode_shim/run_bridge_battle.sh
|
||||
<adversaryBotDir> 7 <run>.jsonl`.
|
||||
Reference in New Issue
Block a user