DrussGT dodge vs fired power: no movement response once range is controlled
Answers the user's hypothesis that DrussGT dodges low-power shots better. Measured on 70 live battles / 490 rounds / 54939 real shots vs real DrussGT (/tmp/tfil_ab2) and replicated on 35 more battles / 24280 shots (/tmp/powtest). Power is not randomly assigned - our policy caps it by RANGE (TR_POWER_FAR_DIST=200 -> 1.0) and by OUR OWN ENERGY (the slope), so inside a range band power is almost a deterministic function of our energy and a naive low-vs-high comparison is secretly a losing-vs-healthy comparison. Everything is stratified by range band and backed by a within-band shuffled-label null (arrival re-derived, so the null keeps the kinematic channel), a round-cluster bootstrap, and a within-shot CONTROL window 40 ticks later when the bullet is long gone. RESULT: no behavioural response. In band 450+ the raw miss distance at arrival is +8.25 px [+5.39,+11.25] for HIGH power - but per flight tick it is 4.52 vs 4.51 px/tick (delta -0.01 [-0.12,+0.10]), i.e. entirely the 2.13-tick longer flight window of the slower bullet. Fixed-12-tick lateral displacement is flat (55.63 vs 55.47, -0.15 [-1.15,+0.84]) and turn rate / speed are flat. The whole difference is already present 5 ticks after the trigger pull (+4.2 px) and is just as large in the bullet-free control window (+5.6 px), so it is a property of the low-energy situation, not of the shot. Hit rate is flat (0.10 vs 0.09). Corpus/attribution notes: e*=DrussGT (subject), s*=ModularBot, per TrBattleCapture.java; the Tank-Royale owner id is NOT stable across runs and is recovered per battle from fire geometry + the energy decrement, cross-checked on 496/496 death events. Geometry validated on the server's own hits (mean miss 11.6 px, 80.6% inside the 18 px radius).
This commit is contained in:
@@ -0,0 +1,795 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Does DrussGT's dodge quality change with the bullet power WE fire?
|
||||||
|
|
||||||
|
Reads REAL live-vs-real-DrussGT captures (per-tick worldstate jsonl + a
|
||||||
|
fire/hit event sidecar + a per-round tick index) produced by
|
||||||
|
``tools/robocode_shim/run_bridge_battle.sh``, and measures DrussGT's movement
|
||||||
|
response per shot, conditioned on the power ModularBot fired. No offline
|
||||||
|
fixture replay, no simulator: these are the recorded live battles.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
WHY THE OBVIOUS ANALYSIS IS WRONG
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Power is NOT randomly assigned. Our power policy (common_libs/gun_harness/
|
||||||
|
virtual_bullets.nim) only ever *caps* the gun's preference, by:
|
||||||
|
* RANGE -- TR_POWER_FAR_DIST=200 -> cap 1.0 beyond it (prFar);
|
||||||
|
* OUR ENERGY -- a linear slope from TR_POWER_ENERGY_MIN=0.5 at <=20 energy to
|
||||||
|
the cap at >=80 (prEnergySlope);
|
||||||
|
* the finishing rule (prFinishKill) and the sub-average-chances rule.
|
||||||
|
So "shots we fired at low power" are disproportionately long-range shots and
|
||||||
|
late/low-energy shots. Every comparison below is therefore stratified by the
|
||||||
|
fire-time range band, the headline is the stratified one, and the between-group
|
||||||
|
difference is compared against a NULL that permutes the power labels inside the
|
||||||
|
band (re-deriving the arrival tick so the kinematic channel survives the null).
|
||||||
|
We also report the confounder itself (our energy, enemy energy) per cell.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
WHAT IS MEASURED
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Per shot fired by US (ModularBot) at DrussGT. u = (cos dir, sin dir) is the
|
||||||
|
direction the bullet was fired; the bullet flies from the fire position along u
|
||||||
|
at v = 20 - 3p px/tick.
|
||||||
|
(a) MISS AT ARRIVAL -- perpendicular px between DrussGT's position on the
|
||||||
|
tick our bullet reaches its along-track plane and our bullet's intended
|
||||||
|
line. = |perp(perpendicular offset)| at that tick. Bot radius 18 px.
|
||||||
|
(b) LATERAL DISPLACEMENT -- perp offset change over the flight window, and the
|
||||||
|
SAME over a FIXED 12-tick window after the shot (power-neutral).
|
||||||
|
(c) RESPONSE LATENCY -- ticks from our fire until DrussGT's heading has turned
|
||||||
|
> LAT_DEG from its fire-tick heading.
|
||||||
|
(d) MEAN TURN RATE and MEAN SPEED over the flight window and the fixed window.
|
||||||
|
(e) DODGE DIRECTION -- did it move FURTHER off the aim line (|perp| grows).
|
||||||
|
(f) FLIGHT WINDOW LENGTH -- low power is a FASTER bullet, so a SHORTER window.
|
||||||
|
Outcome: hit rate per cell, resolved from the real hit/hitwall events.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
THE CONTROL
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
A real bullet response must appear in the window the bullet is in the air. A
|
||||||
|
phase/distance artifact would appear just as strongly in a LATER window of the
|
||||||
|
same shot, when the bullet is long gone. So every metric is also computed over
|
||||||
|
a CONTROL window at [fire+CTRL_OFF, fire+CTRL_OFF+12] and the two are compared.
|
||||||
|
Anything that shows up in both is a property of the situation, not of the shot.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
python3 common_libs/tests/analyze_drussgt_dodge_vs_power.py \
|
||||||
|
--tfil /tmp/tfil_ab2/out --powtest /tmp/powtest \
|
||||||
|
--json common_libs/tests/fixtures/dodge_vs_power_results.json
|
||||||
|
|
||||||
|
MEASURED = the numbers in the tables below (real recorded battles).
|
||||||
|
INFERRED = the attributions and the causal reading; both are flagged in the doc.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import statistics
|
||||||
|
|
||||||
|
BOT_RADIUS = 18.0
|
||||||
|
SPEED_A, SPEED_B = 20.0, 3.0 # bullet speed = 20 - 3p
|
||||||
|
LAT_DEG = 15.0 # heading turn that counts as a response
|
||||||
|
FIXED_WIN = 12 # power-neutral movement window (ticks)
|
||||||
|
CTRL_OFF = 40 # control window starts here after the fire
|
||||||
|
RANGE_MATCH_PX = 50 # range sub-bin used to match on range as well
|
||||||
|
MAX_FLIGHT = 220
|
||||||
|
BANDS = [(0, 100), (100, 200), (200, 300), (300, 450), (450, 10000)]
|
||||||
|
BAND_LABELS = ["0-100", "100-200", "200-300", "300-450", "450+"]
|
||||||
|
PWR_EDGES = [0.0, 0.5, 0.75, 1.0, 1.5, 4.0]
|
||||||
|
PWR_LABELS = ["<0.50", "0.50-0.75", "0.75-1.00", "1.00-1.50", ">=1.50"]
|
||||||
|
|
||||||
|
|
||||||
|
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 band_of(r):
|
||||||
|
for i, (lo, hi) in enumerate(BANDS):
|
||||||
|
if lo <= r < hi:
|
||||||
|
return BAND_LABELS[i]
|
||||||
|
return BAND_LABELS[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def wrap180(x):
|
||||||
|
return abs(((x + 180.0) % 360.0) - 180.0)
|
||||||
|
|
||||||
|
|
||||||
|
def mean(xs):
|
||||||
|
return statistics.fmean(xs) if xs else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def pct(xs, q):
|
||||||
|
if not xs:
|
||||||
|
return float("nan")
|
||||||
|
xs = sorted(xs)
|
||||||
|
return xs[min(len(xs) - 1, int(q * len(xs)))]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------- loader
|
||||||
|
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):
|
||||||
|
"""Tank-Royale owner id -> 'e' (DrussGT, the subject) or 's' (us).
|
||||||
|
|
||||||
|
The id is NOT stable across runs (bot start order varies), so it is
|
||||||
|
recovered per battle: a fire event's (x,y) is the firing bot's own
|
||||||
|
position and that bot's energy drops by exactly the power one tick
|
||||||
|
later. Cross-checked against the death events (see main())."""
|
||||||
|
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]
|
||||||
|
|
||||||
|
# -- per-shot extraction ---------------------------------------------
|
||||||
|
def shots(self):
|
||||||
|
for ev in self.events:
|
||||||
|
if ev.get("type") != "fire" or self.owner_side.get(ev["owner"]) != "s":
|
||||||
|
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"]
|
||||||
|
th = math.radians(ev["dir"])
|
||||||
|
ux, uy = math.cos(th), math.sin(th)
|
||||||
|
x0, y0 = ev["x"], ev["y"]
|
||||||
|
rnd = ev["round"]
|
||||||
|
end = self.start[rnd] + self.count[rnd]
|
||||||
|
|
||||||
|
rng = math.hypot(r0["ex"] - x0, r0["ey"] - y0)
|
||||||
|
# per-tick geometry along/perpendicular to the aim line, up to the round
|
||||||
|
# end (capped); kept so the permutation null can re-derive the arrival.
|
||||||
|
along, perp, rows = [], [], []
|
||||||
|
karr = None
|
||||||
|
perp_arr = None
|
||||||
|
MIN_V = SPEED_A - SPEED_B * PWR_EDGES[-2] # slowest bullet the null can make
|
||||||
|
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
|
||||||
|
along.append(dx * ux + dy * uy)
|
||||||
|
perp.append(dx * uy - dy * ux)
|
||||||
|
rows.append(r)
|
||||||
|
if karr is None and along[-1] > 0 and (SPEED_A - SPEED_B * p) * k >= along[-1]:
|
||||||
|
karr, perp_arr = k, perp[-1]
|
||||||
|
# stop once the real arrival is known, the null's slower arrival could
|
||||||
|
# still land, and the control window has been collected
|
||||||
|
if karr is not None and k >= CTRL_OFF + FIXED_WIN and MIN_V * k > along[-1]:
|
||||||
|
break
|
||||||
|
if karr is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
kmax = len(along)
|
||||||
|
g0 = (r0["ex"] - x0) * uy - (r0["ey"] - y0) * ux
|
||||||
|
|
||||||
|
# (b) fixed 12-tick window
|
||||||
|
lat_fixed = None
|
||||||
|
if kmax >= FIXED_WIN:
|
||||||
|
lat_fixed = perp[FIXED_WIN - 1] - g0
|
||||||
|
# control window (bullet long gone, same phase and range)
|
||||||
|
lat_ctrl = None
|
||||||
|
if kmax >= CTRL_OFF + FIXED_WIN:
|
||||||
|
lat_ctrl = perp[CTRL_OFF + FIXED_WIN - 1] - perp[CTRL_OFF - 1]
|
||||||
|
|
||||||
|
# (c) response latency
|
||||||
|
h0 = r0["eh"]
|
||||||
|
lat = None
|
||||||
|
for i in range(karr):
|
||||||
|
if wrap180(rows[i]["eh"] - h0) > LAT_DEG:
|
||||||
|
lat = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
def kin(lo, n):
|
||||||
|
"""mean |turn rate| and mean |speed| over ticks [lo+1, lo+n]."""
|
||||||
|
if lo > len(rows):
|
||||||
|
return None, None
|
||||||
|
tt, hh = [], []
|
||||||
|
prev = r0 if lo == 0 else rows[lo - 1]
|
||||||
|
for j in range(lo, min(lo + n, kmax)):
|
||||||
|
r = rows[j]
|
||||||
|
tt.append(wrap180(r["eh"] - prev["eh"]))
|
||||||
|
hh.append(abs(r["es"]))
|
||||||
|
prev = r
|
||||||
|
if not tt:
|
||||||
|
return None, None
|
||||||
|
return mean(tt), mean(hh)
|
||||||
|
|
||||||
|
tr_f, sp_f = kin(0, karr)
|
||||||
|
tr_x, sp_x = kin(0, FIXED_WIN)
|
||||||
|
tr_c, sp_c = kin(CTRL_OFF, FIXED_WIN)
|
||||||
|
kind = self.resolution.get((rnd, ev["owner"], ev["bullet"]))
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
run=self.cap_path, rnd=rnd, tick=t0, power=p, speed=SPEED_A - SPEED_B * p,
|
||||||
|
range=rng, band=band_of(rng), flight=karr,
|
||||||
|
perp_fire=g0, perp_arr=perp_arr, miss=abs(perp_arr),
|
||||||
|
miss_per_tick=abs(perp_arr) / karr,
|
||||||
|
lat_disp=perp_arr - g0, lat_disp_abs=abs(perp_arr - g0),
|
||||||
|
lat_disp_per_tick=abs(perp_arr - g0) / karr,
|
||||||
|
lat_fixed=lat_fixed, lat_fixed_abs=None if lat_fixed is None else abs(lat_fixed),
|
||||||
|
lat_ctrl=lat_ctrl, lat_ctrl_abs=None if lat_ctrl is None else abs(lat_ctrl),
|
||||||
|
dodge_further=1.0 if abs(perp_arr) > abs(g0) else 0.0,
|
||||||
|
sign_consist=1.0 if (perp_arr - g0) * perp_arr > 0 else 0.0,
|
||||||
|
latency=lat, turn_flight=tr_f, speed_flight=sp_f,
|
||||||
|
turn_fixed=tr_x, speed_fixed=sp_x,
|
||||||
|
turn_ctrl=tr_c, speed_ctrl=sp_c,
|
||||||
|
resolution=kind, hit=1.0 if kind == "hit" else 0.0,
|
||||||
|
our_energy=r0["se"], enemy_energy=r0["ee"],
|
||||||
|
_dir=ev["dir"], _x=x0, _y=y0, _along=along, _perp=perp,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _arrival(along, perp, v):
|
||||||
|
for k in range(1, len(along) + 1):
|
||||||
|
if along[k - 1] > 0 and v * k >= along[k - 1]:
|
||||||
|
return k, perp[k - 1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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, arms=None):
|
||||||
|
runs = []
|
||||||
|
for fn in sorted(os.listdir(root)):
|
||||||
|
if not (fn.startswith("cap_") and fn.endswith(".jsonl")):
|
||||||
|
continue
|
||||||
|
arm = fn[4:].split("_r")[0]
|
||||||
|
if arms and arm not in arms:
|
||||||
|
continue
|
||||||
|
cap = os.path.join(root, fn)
|
||||||
|
ev = os.path.join(root, "events_" + fn[4:].replace(".jsonl", ".json"))
|
||||||
|
rj = cap + ".rounds.json"
|
||||||
|
if os.path.exists(ev) and os.path.exists(rj):
|
||||||
|
runs.append(Run(cap, ev, rj))
|
||||||
|
return runs
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- statistics
|
||||||
|
def cell_stats(shots, key, only=None):
|
||||||
|
"""{(band, pbin): (n, mean, sd)}"""
|
||||||
|
acc = collections.defaultdict(list)
|
||||||
|
for s in shots:
|
||||||
|
if only and not only(s):
|
||||||
|
continue
|
||||||
|
v = s.get(key)
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
i = pbin(s["power"])
|
||||||
|
if i is None:
|
||||||
|
continue
|
||||||
|
acc[(s["band"], i)].append(v)
|
||||||
|
return {k: (len(v), mean(v), statistics.pstdev(v) if len(v) > 1 else 0.0)
|
||||||
|
for k, v in acc.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def perm_key(s, p):
|
||||||
|
"""Re-derive the arrival-dependent metrics for a substituted power."""
|
||||||
|
v = SPEED_A - SPEED_B * p
|
||||||
|
arr = Run._arrival(s["_along"], s["_perp"], v)
|
||||||
|
if arr is None:
|
||||||
|
return None
|
||||||
|
k, pe = arr
|
||||||
|
return {"power": p, "flight": k, "miss": abs(pe),
|
||||||
|
"miss_per_tick": abs(pe) / k,
|
||||||
|
"lat_disp_abs": abs(pe - s["perp_fire"]),
|
||||||
|
"lat_disp_per_tick": abs(pe - s["perp_fire"]) / k}
|
||||||
|
|
||||||
|
|
||||||
|
ARRIVAL_METRICS = ("miss", "lat_disp_abs", "flight", "miss_per_tick",
|
||||||
|
"lat_disp_per_tick")
|
||||||
|
|
||||||
|
|
||||||
|
def perm_contrast_null(shots, key, reps, seed=4242):
|
||||||
|
"""Per range band, the permutation null of delta = mean(HIGH) - mean(LOW).
|
||||||
|
|
||||||
|
For the arrival-dependent metrics each shot carries TWO precomputed values:
|
||||||
|
the metric it would have had at a typical LOW power and at a typical HIGH
|
||||||
|
power of that band (arrival tick re-derived, so the kinematic channel is
|
||||||
|
preserved). The null then draws a random subset of n_high shots to be the
|
||||||
|
HIGH arm -- which is exactly a permutation of the power values among the
|
||||||
|
shots of the band. Metrics that do not depend mechanically on power get the
|
||||||
|
same value in both arms, i.e. a plain label permutation.
|
||||||
|
|
||||||
|
Returns {band: (observed delta, null sd, two-sided p)}.
|
||||||
|
"""
|
||||||
|
rng = random.Random(seed)
|
||||||
|
by_band = collections.defaultdict(list)
|
||||||
|
for s in shots:
|
||||||
|
if grp(s) is not None and s.get(key) is not None:
|
||||||
|
by_band[s["band"]].append(s)
|
||||||
|
out = {}
|
||||||
|
for b, ss in by_band.items():
|
||||||
|
lo = [s for s in ss if grp(s) == "LOW"]
|
||||||
|
hi = [s for s in ss if grp(s) == "HIGH"]
|
||||||
|
if len(lo) < 15 or len(hi) < 15:
|
||||||
|
continue
|
||||||
|
p_lo = mean([s["power"] for s in lo])
|
||||||
|
p_hi = mean([s["power"] for s in hi])
|
||||||
|
if key in ARRIVAL_METRICS:
|
||||||
|
def val(s, p):
|
||||||
|
d = perm_key(s, p)
|
||||||
|
return None if d is None else d[key]
|
||||||
|
lows = [val(s, p_lo) for s in ss]
|
||||||
|
highs = [val(s, p_hi) for s in ss]
|
||||||
|
keep = [i for i in range(len(ss)) if lows[i] is not None and highs[i] is not None]
|
||||||
|
lows = [lows[i] for i in keep]
|
||||||
|
highs = [highs[i] for i in keep]
|
||||||
|
tag = [1 if grp(ss[i]) == "HIGH" else 0 for i in keep]
|
||||||
|
obs = (mean([highs[j] for j in range(len(keep)) if tag[j] == 1])
|
||||||
|
- mean([lows[j] for j in range(len(keep)) if tag[j] == 0]))
|
||||||
|
else:
|
||||||
|
vals = [s[key] for s in ss]
|
||||||
|
lows = highs = vals
|
||||||
|
tag = [1 if grp(s) == "HIGH" else 0 for s in ss]
|
||||||
|
obs = mean([v for v, t in zip(vals, tag) if t == 1]) - \
|
||||||
|
mean([v for v, t in zip(vals, tag) if t == 0])
|
||||||
|
n = len(keep) if key in ARRIVAL_METRICS else len(ss)
|
||||||
|
k = sum(tag)
|
||||||
|
deltas = []
|
||||||
|
for _ in range(reps):
|
||||||
|
idx = list(range(n))
|
||||||
|
rng.shuffle(idx)
|
||||||
|
sel = set(idx[:k])
|
||||||
|
if key in ARRIVAL_METRICS:
|
||||||
|
deltas.append(mean([highs[j] for j in sel]) -
|
||||||
|
mean([lows[j] for j in range(n) if j not in sel]))
|
||||||
|
else:
|
||||||
|
deltas.append(mean([lows[j] for j in sel]) -
|
||||||
|
mean([lows[j] for j in range(n) if j not in sel]))
|
||||||
|
p_hi_ = (sum(1 for x in deltas if x >= obs) + 1) / (reps + 1)
|
||||||
|
p_lo_ = (sum(1 for x in deltas if x <= obs) + 1) / (reps + 1)
|
||||||
|
out[b] = (obs, statistics.pstdev(deltas), min(1.0, 2 * min(p_hi_, p_lo_)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def fixed_horizon(shots, ks=(5, 10, 15, 20, 25, 30)):
|
||||||
|
"""The confound-free test.
|
||||||
|
|
||||||
|
Instead of the arrival tick (which power itself moves, and which also depends
|
||||||
|
on DrussGT's radial velocity), measure |perpendicular offset from the aim
|
||||||
|
line| at a FIXED number of ticks after the fire, and the same quantity in the
|
||||||
|
CONTROL window CTRL_OFF ticks later (bullet long gone). A response to the
|
||||||
|
shot would be ~0 at small k and grow with k. A phase/distance difference is
|
||||||
|
present already at k=5 and identical in the bullet-free control window.
|
||||||
|
Returns {band: {k: (low, high, nlow, nhigh, ctrl_low, ctrl_high)}}."""
|
||||||
|
out = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
ss = [s for s in shots if s["band"] == b and grp(s) is not None]
|
||||||
|
if len(ss) < 200:
|
||||||
|
continue
|
||||||
|
for k in ks:
|
||||||
|
lo = [abs(s["_perp"][k - 1]) for s in ss
|
||||||
|
if grp(s) == "LOW" and len(s["_perp"]) >= k]
|
||||||
|
hi = [abs(s["_perp"][k - 1]) for s in ss
|
||||||
|
if grp(s) == "HIGH" and len(s["_perp"]) >= k]
|
||||||
|
cl = [abs(s["_perp"][k + CTRL_OFF - 1]) for s in ss
|
||||||
|
if grp(s) == "LOW" and len(s["_perp"]) >= k + CTRL_OFF]
|
||||||
|
ch = [abs(s["_perp"][k + CTRL_OFF - 1]) for s in ss
|
||||||
|
if grp(s) == "HIGH" and len(s["_perp"]) >= k + CTRL_OFF]
|
||||||
|
out.setdefault(b, {})[k] = (mean(lo), mean(hi), len(lo), len(hi),
|
||||||
|
mean(cl), mean(ch))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def flight_matched(shots):
|
||||||
|
_doc_extra = None
|
||||||
|
"""The kinematic channel removed outright.
|
||||||
|
|
||||||
|
Two matching variables at once, because they are both mechanical and both
|
||||||
|
differ between the arms: (i) the flight window itself (v = 20-3p, so a slower
|
||||||
|
bullet flies longer and DrussGT drifts further off the line) and (ii) the fire
|
||||||
|
RANGE inside the band (at a fixed flight window, a slower bullet implies a
|
||||||
|
shorter range, and the gun's aim error grows with range). Cells are
|
||||||
|
(integer flight window, 50 px range sub-bin) and only cells with >= MIN_CELL
|
||||||
|
shots in BOTH arms contribute; the per-cell differences are pooled with n
|
||||||
|
weights. Anything left here is not a flight-time or a range artefact.
|
||||||
|
Returns {band: (delta, n_used, cells, null_sd, p)}.
|
||||||
|
"""
|
||||||
|
MIN_CELL = 8
|
||||||
|
out = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
ss = [s for s in shots if s["band"] == b and grp(s) is not None]
|
||||||
|
by = collections.defaultdict(lambda: {"LOW": [], "HIGH": []})
|
||||||
|
for s in ss:
|
||||||
|
by[(s["flight"], int(s["range"] // RANGE_MATCH_PX))][grp(s)].append(s)
|
||||||
|
pairs = [(fl, d) for fl, d in by.items()
|
||||||
|
if len(d["LOW"]) >= MIN_CELL and len(d["HIGH"]) >= MIN_CELL]
|
||||||
|
if not pairs:
|
||||||
|
continue
|
||||||
|
num = den = 0.0
|
||||||
|
for fl, d in pairs:
|
||||||
|
w = len(d["LOW"]) + len(d["HIGH"])
|
||||||
|
num += w * (mean([s["miss"] for s in d["HIGH"]])
|
||||||
|
- mean([s["miss"] for s in d["LOW"]]))
|
||||||
|
den += w
|
||||||
|
cells = [(fl, d["LOW"], d["HIGH"]) for fl, d in pairs]
|
||||||
|
out[b] = (num / den, int(den), len(pairs), cells)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def flight_matched_null(shots, reps=400, seed=808):
|
||||||
|
"""Permutation null of the flight-matched contrast (labels shuffled inside
|
||||||
|
each band, so the flight bins and their sizes are preserved)."""
|
||||||
|
rng = random.Random(seed)
|
||||||
|
res = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
ss = [s for s in shots if s["band"] == b and grp(s) is not None]
|
||||||
|
if not ss:
|
||||||
|
continue
|
||||||
|
vals = [s["miss"] for s in ss]
|
||||||
|
fls = [s["flight"] for s in ss]
|
||||||
|
tag = [grp(s) == "HIGH" for s in ss]
|
||||||
|
n = len(ss)
|
||||||
|
k = sum(tag)
|
||||||
|
if k < 20 or n - k < 20:
|
||||||
|
continue
|
||||||
|
rgs = [s["range"] for s in ss]
|
||||||
|
bins = collections.defaultdict(list)
|
||||||
|
for i in range(n):
|
||||||
|
bins[(fls[i], int(rgs[i] // RANGE_MATCH_PX))].append(i)
|
||||||
|
pool = [ix for ix in bins.values() if len(ix) >= 16]
|
||||||
|
if not pool:
|
||||||
|
continue
|
||||||
|
ds = []
|
||||||
|
for _ in range(reps):
|
||||||
|
perm = tag[:]
|
||||||
|
# shuffle labels within flight bin (preserves the bin sizes and the
|
||||||
|
# flight distribution of both arms)
|
||||||
|
for ix in pool:
|
||||||
|
sub = [perm[i] for i in ix]
|
||||||
|
rng.shuffle(sub)
|
||||||
|
for i, t in zip(ix, sub):
|
||||||
|
perm[i] = t
|
||||||
|
num = den = 0.0
|
||||||
|
for ix in pool:
|
||||||
|
a = [vals[i] for i in ix if not perm[i]]
|
||||||
|
c = [vals[i] for i in ix if perm[i]]
|
||||||
|
if len(a) >= 8 and len(c) >= 8:
|
||||||
|
num += (len(a) + len(c)) * (mean(c) - mean(a))
|
||||||
|
den += len(a) + len(c)
|
||||||
|
if den:
|
||||||
|
ds.append(num / den)
|
||||||
|
obs_num = obs_den = 0.0
|
||||||
|
for ix in pool:
|
||||||
|
a = [vals[i] for i in ix if not tag[i]]
|
||||||
|
c = [vals[i] for i in ix if tag[i]]
|
||||||
|
if len(a) >= 8 and len(c) >= 8:
|
||||||
|
obs_num += (len(a) + len(c)) * (mean(c) - mean(a))
|
||||||
|
obs_den += len(a) + len(c)
|
||||||
|
if ds and obs_den:
|
||||||
|
obs = obs_num / obs_den
|
||||||
|
p_hi = (sum(1 for x in ds if x >= obs) + 1) / (len(ds) + 1)
|
||||||
|
p_lo = (sum(1 for x in ds if x <= obs) + 1) / (len(ds) + 1)
|
||||||
|
res[b] = (obs, obs_den, statistics.pstdev(ds),
|
||||||
|
min(1.0, 2 * min(p_hi, p_lo)))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def grp(s):
|
||||||
|
return "LOW" if 0.5 <= s["power"] < 0.75 else (
|
||||||
|
"HIGH" if 1.0 <= s["power"] < 1.5 else None)
|
||||||
|
|
||||||
|
|
||||||
|
def cluster_boot(shots, key, group_fn, reps=2000, seed=99):
|
||||||
|
"""Round-cluster bootstrap over the per-run aggregates of `key` by group."""
|
||||||
|
by_run = collections.defaultdict(list)
|
||||||
|
for s in shots:
|
||||||
|
if s.get(key) is not None:
|
||||||
|
by_run[s["run"]].append(s)
|
||||||
|
runs = list(by_run)
|
||||||
|
# pre-aggregate per run per group
|
||||||
|
agg = {}
|
||||||
|
for r in runs:
|
||||||
|
g = collections.defaultdict(lambda: [0.0, 0])
|
||||||
|
for s in by_run[r]:
|
||||||
|
grp = group_fn(s)
|
||||||
|
if grp is None:
|
||||||
|
continue
|
||||||
|
g[grp][0] += s[key]
|
||||||
|
g[grp][1] += 1
|
||||||
|
agg[r] = {k: (v[0], v[1]) for k, v in g.items()}
|
||||||
|
rng = random.Random(seed)
|
||||||
|
out = collections.defaultdict(list)
|
||||||
|
for _ in range(reps):
|
||||||
|
tot = collections.defaultdict(lambda: [0.0, 0])
|
||||||
|
for _ in runs:
|
||||||
|
r = rng.choice(runs)
|
||||||
|
for k, (s_, c_) in agg[r].items():
|
||||||
|
tot[k][0] += s_
|
||||||
|
tot[k][1] += c_
|
||||||
|
for k, (s_, c_) in tot.items():
|
||||||
|
if c_:
|
||||||
|
out[k].append(s_ / c_)
|
||||||
|
return {k: (pct(v, .025), pct(v, .975)) for k, v in out.items()}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------- report
|
||||||
|
def report(name, runs, shots, reps, out):
|
||||||
|
print("=" * 104)
|
||||||
|
print("CORPUS %-10s %d battles %d rounds %d usable shots by US (ModularBot)"
|
||||||
|
% (name, len(runs), sum(len(r.rounds) for r in runs), len(shots)))
|
||||||
|
|
||||||
|
# ---- attribution cross-check against the death events
|
||||||
|
bad = tot = 0
|
||||||
|
for r in runs:
|
||||||
|
for ev in r.events:
|
||||||
|
if ev.get("type") != "death":
|
||||||
|
continue
|
||||||
|
end = r.start[ev["round"]] + r.count[ev["round"]] - 1
|
||||||
|
row = r.by_tick.get(end)
|
||||||
|
if row is None:
|
||||||
|
continue
|
||||||
|
tot += 1
|
||||||
|
vs = r.owner_side.get(ev["victim"])
|
||||||
|
if (vs == "e" and row["ee"] > 1.0) or (vs == "s" and row["se"] > 1.0):
|
||||||
|
bad += 1
|
||||||
|
print(" attribution: owner id -> {e=DrussGT, s=us} recovered per battle from fire"
|
||||||
|
" geometry+energy;\n cross-checked on the death events: %d/%d"
|
||||||
|
" deaths have the mapped victim at ~0 energy" % (tot - bad, tot))
|
||||||
|
print(" (attribution is per-battle because the Tank Royale owner id is not stable"
|
||||||
|
" across runs)")
|
||||||
|
|
||||||
|
# ---- the confounder, made visible
|
||||||
|
print("\n >> THE CONFOUNDER IS REAL: our fired power is a range + energy cap")
|
||||||
|
print(" %-9s %-11s %7s %8s %9s %9s %9s" %
|
||||||
|
("band", "power", "shots", "firePx", "ourE", "enemyE", "flight"))
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
for i in range(len(PWR_EDGES) - 1):
|
||||||
|
ss = [s for s in shots if s["band"] == b and pbin(s["power"]) == i]
|
||||||
|
if len(ss) < 5:
|
||||||
|
continue
|
||||||
|
print(" %-9s %-11s %7d %8.1f %9.1f %9.1f %9.2f" % (
|
||||||
|
b, PWR_LABELS[i], len(ss), mean([s["range"] for s in ss]),
|
||||||
|
mean([s["our_energy"] for s in ss]),
|
||||||
|
mean([s["enemy_energy"] for s in ss]),
|
||||||
|
mean([s["flight"] for s in ss])))
|
||||||
|
|
||||||
|
RES = out
|
||||||
|
for key, title in (
|
||||||
|
("miss", "(a) MISS AT ARRIVAL px - perpendicular offset of DrussGT from our aim line"),
|
||||||
|
("miss_per_tick", "(a') MISS AT ARRIVAL / flight ticks (px per tick - power-neutral)"),
|
||||||
|
("lat_disp_abs", "(b) LATERAL DISPLACEMENT px over the flight window"),
|
||||||
|
("lat_disp_per_tick", "(b/ ) LATERAL DISPLACEMENT / flight ticks"
|
||||||
|
" (px per tick - power-neutral)"),
|
||||||
|
("lat_fixed_abs", "(b') LATERAL DISPLACEMENT px over a FIXED 12-tick window"
|
||||||
|
" (power-neutral)"),
|
||||||
|
("lat_ctrl_abs", "(b'') CONTROL: lateral displacement px, SAME shot, 40 ticks later"
|
||||||
|
" (bullet gone)"),
|
||||||
|
("flight", "(f) FLIGHT WINDOW LENGTH ticks (low power = FASTER bullet = shorter)"),
|
||||||
|
("latency", "(c) RESPONSE LATENCY ticks to a >%g deg heading turn" % LAT_DEG),
|
||||||
|
("turn_flight", "(d) MEAN TURN RATE deg/tick, flight window"),
|
||||||
|
("turn_fixed", "(d') MEAN TURN RATE deg/tick, fixed 12-tick window"),
|
||||||
|
("turn_ctrl", "(d'') CONTROL: mean turn rate deg/tick, 40 ticks later"),
|
||||||
|
("speed_flight", "(d) MEAN SPEED px/tick, flight window"),
|
||||||
|
("speed_ctrl", "(d'') CONTROL: mean speed px/tick, 40 ticks later"),
|
||||||
|
("dodge_further", "(e) fraction where |perp| GREW (moved further off the line)"),
|
||||||
|
("hit", "OUTCOME hit rate (real server events)"),
|
||||||
|
):
|
||||||
|
cs = cell_stats(shots, key)
|
||||||
|
print("\n %s" % title)
|
||||||
|
print(" %-9s" % "band" + "".join("%22s" % l for l in PWR_LABELS))
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
cells, ns = [], []
|
||||||
|
for i in range(len(PWR_EDGES) - 1):
|
||||||
|
n, m, sd = cs.get((b, i), (0, float("nan"), 0.0))
|
||||||
|
cells.append(m)
|
||||||
|
ns.append(n)
|
||||||
|
RES.setdefault("tables", {}).setdefault(key, {}).setdefault(b, {})[
|
||||||
|
PWR_LABELS[i]] = {"n": n, "mean": None if n == 0 else m, "sd": sd}
|
||||||
|
print(" %-9s" % b + "".join(
|
||||||
|
"%22s" % ("%.2f" % c if c == c else "-") for c in cells))
|
||||||
|
print(" %-9s" % "" + "".join("%22s" % ("n=%d" % n) for n in ns))
|
||||||
|
|
||||||
|
# ---- shuffled-label null, per band, on the LOW-vs-HIGH contrast
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print("SHUFFLED-LABEL NULL (labels permuted WITHIN each range band, %d reps)" % reps)
|
||||||
|
print(" For the arrival-dependent metrics the arrival tick is RE-DERIVED from the"
|
||||||
|
" permuted power,\n so the null keeps the kinematic channel (a slower bullet"
|
||||||
|
" really does arrive later) and destroys\n only the response. p is the two-sided"
|
||||||
|
" permutation p on delta = mean(HIGH) - mean(LOW) inside that band.")
|
||||||
|
RES["perm"] = {}
|
||||||
|
for key in ("miss", "miss_per_tick", "lat_disp_abs", "flight",
|
||||||
|
"lat_fixed_abs", "lat_ctrl_abs", "latency", "turn_fixed", "turn_ctrl",
|
||||||
|
"speed_fixed", "hit"):
|
||||||
|
print(" %-16s" % key, end="")
|
||||||
|
cells = perm_contrast_null(shots, key, reps)
|
||||||
|
RES["perm"][key] = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
if b in cells:
|
||||||
|
d, sd, p2 = cells[b]
|
||||||
|
print(" %s d=%+.3f(null sd=%.3f,p=%.3f)" % (b, d, sd, p2), end="")
|
||||||
|
RES["perm"][key][b] = {"delta": d, "null_sd": sd, "p_two": p2}
|
||||||
|
else:
|
||||||
|
print(" %s n/a" % b, end="")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---- the decisive fixed-horizon / control-window test
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print("FIXED-HORIZON TEST (no arrival tick, no flight-window confound)")
|
||||||
|
print(" |perpendicular offset of DrussGT from our aim line| at k ticks after the fire"
|
||||||
|
" (flight window),\n and the same quantity in the CONTROL window at k+%d ticks,"
|
||||||
|
" when the bullet is long gone.\n A response to our shot would be ~0 at k=5 and"
|
||||||
|
" grow with k; a phase/geometry difference is\n present at k=5 and identical in"
|
||||||
|
" the control." % CTRL_OFF)
|
||||||
|
fh = fixed_horizon(shots)
|
||||||
|
RES["fixed_horizon"] = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
if b not in fh:
|
||||||
|
continue
|
||||||
|
print(" band %s (nLOW=%d nHIGH=%d per k)" % (b, fh[b][5][2], fh[b][5][3]))
|
||||||
|
for k, (lo, hi, nlo, nhi, cl, ch) in sorted(fh[b].items()):
|
||||||
|
print(" k=%2d flight: LOW=%7.1f HIGH=%7.1f d=%+6.2f | control(+%d):"
|
||||||
|
" LOW=%7.1f HIGH=%7.1f d=%+6.2f"
|
||||||
|
% (k, lo, hi, hi - lo, CTRL_OFF, cl, ch, ch - cl))
|
||||||
|
RES["fixed_horizon"].setdefault(b, {})[k] = {
|
||||||
|
"low": lo, "high": hi, "delta": hi - lo, "n_low": nlo, "n_high": nhi,
|
||||||
|
"ctrl_low": cl, "ctrl_high": ch, "ctrl_delta": ch - cl}
|
||||||
|
|
||||||
|
# ---- flight-matched contrast: the kinematic channel removed
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print("FLIGHT- *AND* RANGE-MATCHED CONTRAST (kinematics and range both removed)")
|
||||||
|
print(" Within each range band we compare LOW vs HIGH power only inside the SAME cell"
|
||||||
|
" = (integer\n flight window, 50 px range sub-bin), pooling the per-cell differences"
|
||||||
|
" with n weights. A longer\n window lets DrussGT drift further off the line; at a"
|
||||||
|
" fixed window a slower bullet implies a\n shorter range and the gun's aim error"
|
||||||
|
" grows with range. Anything left here is neither.")
|
||||||
|
fm = flight_matched(shots)
|
||||||
|
fnull = flight_matched_null(shots, reps)
|
||||||
|
print(" %-9s %8s %8s %8s | %10s %8s %8s" %
|
||||||
|
("band", "obs d", "n used", "bins", "null sd", "p_two", ""))
|
||||||
|
RES["flight_matched"] = {}
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
if b in fnull:
|
||||||
|
obs, nused, sd, p2 = fnull[b]
|
||||||
|
print(" %-9s %+8.2f %8d %8d | %10.2f %8.3f"
|
||||||
|
% (b, obs, nused, fm.get(b, (0, 0, 0, []))[2], sd, p2))
|
||||||
|
RES["flight_matched"][b] = {"delta": obs, "n": nused, "null_sd": sd,
|
||||||
|
"p_two": p2}
|
||||||
|
|
||||||
|
# ---- the headline within-band contrast: LOW (0.50-0.75) vs HIGH (1.00-1.50)
|
||||||
|
print("\n" + "-" * 104)
|
||||||
|
print("HEADLINE WITHIN-BAND CONTRAST LOW p in [0.50,0.75) vs HIGH p in [1.00,1.50)")
|
||||||
|
print(" (HELD fixed inside each range band; 95%% CI from a ROUND-cluster bootstrap)"
|
||||||
|
"\n")
|
||||||
|
|
||||||
|
hdr = (" %-14s %-9s %7s %7s | %8s %8s %8s %8s" %
|
||||||
|
("metric", "band", "nLOW", "nHIGH", "LOW", "HIGH", "delta", "95% CI"))
|
||||||
|
for key in ("miss", "miss_per_tick", "lat_disp_abs", "lat_disp_per_tick",
|
||||||
|
"lat_fixed_abs", "lat_ctrl_abs", "flight", "latency", "turn_flight",
|
||||||
|
"turn_fixed", "turn_ctrl", "speed_flight", "speed_ctrl", "hit"):
|
||||||
|
print(hdr)
|
||||||
|
for b in BAND_LABELS:
|
||||||
|
ss = [s for s in shots if s["band"] == b and grp(s) and s.get(key) is not None]
|
||||||
|
lo = [s[key] for s in ss if grp(s) == "LOW"]
|
||||||
|
hi = [s[key] for s in ss if grp(s) == "HIGH"]
|
||||||
|
if len(lo) < 15 or len(hi) < 15:
|
||||||
|
print(" %-14s %-9s %7d %7d | (too few for a contrast)" %
|
||||||
|
(key, b, len(lo), len(hi)))
|
||||||
|
RES.setdefault("contrast", {}).setdefault(key, {})[b] = {
|
||||||
|
"n_low": len(lo), "n_high": len(hi)}
|
||||||
|
continue
|
||||||
|
ci = cluster_boot(ss, key, grp)
|
||||||
|
d = mean(hi) - mean(lo)
|
||||||
|
print(" %-14s %-9s %7d %7d | %8.2f %8.2f %+8.2f [%+.2f,%+.2f]" % (
|
||||||
|
key, b, len(lo), len(hi), mean(lo), mean(hi), d,
|
||||||
|
ci.get("HIGH", (float("nan"),) * 2)[0] - ci.get("LOW", (0, 0))[1],
|
||||||
|
ci.get("HIGH", (0, 0))[1] - ci.get("LOW", (float("nan"),) * 2)[0]))
|
||||||
|
RES.setdefault("contrast", {}).setdefault(key, {})[b] = {
|
||||||
|
"n_low": len(lo), "n_high": len(hi), "low": mean(lo), "high": mean(hi),
|
||||||
|
"delta": d,
|
||||||
|
"ci_lo": ci.get("HIGH", (float("nan"),) * 2)[0] - ci.get("LOW", (0, 0))[1],
|
||||||
|
"ci_hi": ci.get("HIGH", (0, 0))[1] - ci.get("LOW", (float("nan"),) * 2)[0]}
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ---- power histogram of OUR shots, for the record
|
||||||
|
h = collections.Counter(round(s["power"], 2) for s in shots)
|
||||||
|
print(" OUR fired-power histogram (exact values, top 12):")
|
||||||
|
print(" " + " ".join("%.2f:%d" % (p, n) for p, n in h.most_common(12)))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--tfil", default=None)
|
||||||
|
ap.add_argument("--powtest", default=None)
|
||||||
|
ap.add_argument("--json", default=None)
|
||||||
|
ap.add_argument("--reps", type=int, default=800)
|
||||||
|
a = ap.parse_args()
|
||||||
|
out = {}
|
||||||
|
for name, runs in (("tfil_ab2", discover_tfil(a.tfil) if a.tfil else []),
|
||||||
|
("powtest", discover_powtest(a.powtest) if a.powtest else [])):
|
||||||
|
if not runs:
|
||||||
|
continue
|
||||||
|
shots = [s for r in runs for s in r.shots()]
|
||||||
|
out[name] = {"battles": len(runs), "shots": len(shots)}
|
||||||
|
report(name, runs, shots, a.reps, out[name])
|
||||||
|
if a.json:
|
||||||
|
with open(a.json, "w") as f:
|
||||||
|
json.dump(out, f, indent=1, sort_keys=True)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,760 @@
|
|||||||
|
========================================================================================================
|
||||||
|
CORPUS tfil_ab2 70 battles 490 rounds 54939 usable shots by US (ModularBot)
|
||||||
|
attribution: owner id -> {e=DrussGT, s=us} recovered per battle from fire geometry+energy;
|
||||||
|
cross-checked on the death events: 496/496 deaths have the mapped victim at ~0 energy
|
||||||
|
(attribution is per-battle because the Tank Royale owner id is not stable across runs)
|
||||||
|
|
||||||
|
>> THE CONFOUNDER IS REAL: our fired power is a range + energy cap
|
||||||
|
band power shots firePx ourE enemyE flight
|
||||||
|
0-100 0.50-0.75 6 82.7 15.5 16.8 6.50
|
||||||
|
0-100 >=1.50 19 80.3 63.7 57.0 10.89
|
||||||
|
100-200 <0.50 5 160.9 8.6 0.6 10.00
|
||||||
|
100-200 0.50-0.75 53 165.8 13.2 18.8 10.42
|
||||||
|
100-200 0.75-1.00 5 177.3 29.0 33.7 11.00
|
||||||
|
100-200 1.00-1.50 23 168.4 85.0 83.1 11.87
|
||||||
|
100-200 >=1.50 191 158.7 75.8 71.0 14.86
|
||||||
|
200-300 <0.50 6 257.0 99.7 99.8 15.33
|
||||||
|
200-300 0.50-0.75 130 266.8 12.7 20.0 15.28
|
||||||
|
200-300 0.75-1.00 21 262.7 28.8 26.7 16.14
|
||||||
|
200-300 1.00-1.50 780 261.2 83.6 81.5 16.86
|
||||||
|
200-300 >=1.50 56 248.4 32.0 15.7 21.95
|
||||||
|
300-450 <0.50 333 409.1 14.5 1.9 20.96
|
||||||
|
300-450 0.50-0.75 5272 411.3 13.8 19.2 22.36
|
||||||
|
300-450 0.75-1.00 880 407.7 29.0 28.5 23.49
|
||||||
|
300-450 1.00-1.50 10696 402.0 69.2 62.6 23.70
|
||||||
|
450+ <0.50 950 537.0 14.2 1.1 27.03
|
||||||
|
450+ 0.50-0.75 12960 536.1 13.7 18.6 28.38
|
||||||
|
450+ 0.75-1.00 2424 540.4 28.8 26.4 30.18
|
||||||
|
450+ 1.00-1.50 20126 534.2 63.0 53.8 30.52
|
||||||
|
|
||||||
|
(a) MISS AT ARRIVAL px - perpendicular offset of DrussGT from our aim line
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 23.96 - 32.97 37.21
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 46.00 43.03 33.87 47.92 59.00
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 53.19 74.31 79.82 74.72 113.81
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 112.84 104.67 109.32 110.55 177.09
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 130.86 124.01 132.35 132.26 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(a') MISS AT ARRIVAL / flight ticks (px per tick - power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 3.72 - 4.85 3.61
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 4.77 4.45 3.32 4.05 4.37
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 3.41 5.01 5.18 4.67 5.47
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 5.52 4.85 4.85 4.88 9.84
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 4.97 4.52 4.55 4.51 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(b) LATERAL DISPLACEMENT px over the flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 30.64 - 42.81 47.99
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 46.73 37.57 45.85 56.50 64.60
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 79.80 59.91 58.51 71.56 103.43
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 62.79 88.47 94.34 91.94 57.29
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 78.40 97.42 102.03 103.30 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(b/ ) LATERAL DISPLACEMENT / flight ticks (px per tick - power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 4.66 - 6.54 4.54
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 4.38 3.44 3.77 4.76 4.22
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 5.13 3.79 3.45 4.12 4.75
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 2.96 3.94 4.00 3.86 3.18
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 2.90 3.47 3.41 3.42 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(b') LATERAL DISPLACEMENT px over a FIXED 12-tick window (power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 59.58 - 85.08 55.36
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 56.15 43.26 46.77 59.47 51.69
|
||||||
|
n=5 n=52 n=5 n=23 n=188
|
||||||
|
200-300 64.29 47.20 40.99 50.09 54.53
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 42.81 54.20 54.14 52.75 38.69
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 45.56 55.63 54.97 55.47 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(b'') CONTROL: lateral displacement px, SAME shot, 40 ticks later (bullet gone)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 47.22 - 59.18 54.33
|
||||||
|
n=0 n=5 n=0 n=2 n=15
|
||||||
|
100-200 46.20 46.92 12.30 50.59 45.10
|
||||||
|
n=2 n=41 n=5 n=23 n=170
|
||||||
|
200-300 56.80 43.68 49.06 49.21 41.62
|
||||||
|
n=6 n=127 n=21 n=780 n=32
|
||||||
|
300-450 37.42 52.80 53.83 51.95 61.95
|
||||||
|
n=265 n=5155 n=877 n=10695 n=1
|
||||||
|
450+ 42.34 52.03 52.53 52.64 -
|
||||||
|
n=821 n=12828 n=2421 n=20120 n=0
|
||||||
|
|
||||||
|
(f) FLIGHT WINDOW LENGTH ticks (low power = FASTER bullet = shorter)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 6.50 - 6.50 10.89
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 10.00 10.42 11.00 11.87 14.86
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 15.33 15.28 16.14 16.86 21.95
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 20.96 22.36 23.49 23.70 18.00
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 27.03 28.38 30.18 30.52 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(c) RESPONSE LATENCY ticks to a >15 deg heading turn
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 4.20 - 4.00 5.67
|
||||||
|
n=0 n=5 n=0 n=2 n=15
|
||||||
|
100-200 6.25 6.34 5.50 6.44 7.00
|
||||||
|
n=4 n=29 n=2 n=16 n=148
|
||||||
|
200-300 8.33 9.03 10.00 7.68 9.27
|
||||||
|
n=6 n=78 n=11 n=566 n=49
|
||||||
|
300-450 10.99 11.55 11.84 11.46 2.00
|
||||||
|
n=149 n=2968 n=525 n=6860 n=1
|
||||||
|
450+ 13.37 13.60 14.57 14.27 -
|
||||||
|
n=559 n=8682 n=1745 n=13884 n=0
|
||||||
|
|
||||||
|
(d) MEAN TURN RATE deg/tick, flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 2.83 - 3.89 3.48
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 3.77 2.01 3.39 2.86 2.78
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 3.55 2.08 1.72 2.49 2.07
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 1.30 1.48 1.48 1.61 3.30
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 1.37 1.49 1.51 1.46 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(d') MEAN TURN RATE deg/tick, fixed 12-tick window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 2.54 - 2.91 3.43
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 3.64 2.09 3.37 2.66 2.64
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 3.22 1.91 1.45 2.22 2.03
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 1.33 1.46 1.45 1.50 2.78
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 1.36 1.43 1.46 1.28 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(d'') CONTROL: mean turn rate deg/tick, 40 ticks later
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 3.07 - 2.91 3.18
|
||||||
|
n=0 n=5 n=0 n=2 n=15
|
||||||
|
100-200 2.93 2.20 2.61 3.04 2.61
|
||||||
|
n=3 n=44 n=5 n=23 n=174
|
||||||
|
200-300 2.80 1.78 1.35 2.19 3.17
|
||||||
|
n=6 n=127 n=21 n=780 n=37
|
||||||
|
300-450 1.31 1.44 1.38 1.57 2.99
|
||||||
|
n=295 n=5211 n=878 n=10696 n=1
|
||||||
|
450+ 1.30 1.52 1.54 1.58 -
|
||||||
|
n=886 n=12902 n=2423 n=20126 n=0
|
||||||
|
|
||||||
|
(d) MEAN SPEED px/tick, flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 7.39 - 8.00 7.45
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 5.73 6.05 4.83 6.05 5.87
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 6.18 5.39 5.64 5.44 6.20
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 5.41 5.49 5.53 5.49 3.94
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 5.37 5.46 5.44 5.48 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
(d'') CONTROL: mean speed px/tick, 40 ticks later
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 6.07 - 6.23 6.33
|
||||||
|
n=0 n=5 n=0 n=2 n=15
|
||||||
|
100-200 5.23 5.84 3.45 5.86 5.73
|
||||||
|
n=3 n=44 n=5 n=23 n=174
|
||||||
|
200-300 6.32 5.63 6.45 5.48 6.53
|
||||||
|
n=6 n=127 n=21 n=780 n=37
|
||||||
|
300-450 5.48 5.50 5.56 5.45 5.37
|
||||||
|
n=295 n=5211 n=878 n=10696 n=1
|
||||||
|
450+ 5.37 5.46 5.46 5.47 -
|
||||||
|
n=886 n=12902 n=2423 n=20126 n=0
|
||||||
|
|
||||||
|
(e) fraction where |perp| GREW (moved further off the line)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 0.50 - 0.50 0.63
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 0.40 0.40 0.60 0.52 0.60
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 0.50 0.54 0.52 0.59 0.77
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 0.54 0.57 0.59 0.60 1.00
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 0.56 0.59 0.60 0.61 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
OUTCOME hit rate (real server events)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - 0.67 - 0.50 0.32
|
||||||
|
n=0 n=6 n=0 n=2 n=19
|
||||||
|
100-200 0.20 0.30 0.40 0.39 0.20
|
||||||
|
n=5 n=53 n=5 n=23 n=191
|
||||||
|
200-300 0.50 0.18 0.10 0.16 0.07
|
||||||
|
n=6 n=130 n=21 n=780 n=56
|
||||||
|
300-450 0.03 0.11 0.13 0.12 0.00
|
||||||
|
n=333 n=5272 n=880 n=10696 n=1
|
||||||
|
450+ 0.01 0.10 0.09 0.09 -
|
||||||
|
n=950 n=12960 n=2424 n=20126 n=0
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
SHUFFLED-LABEL NULL (labels permuted WITHIN each range band, 400 reps)
|
||||||
|
For the arrival-dependent metrics the arrival tick is RE-DERIVED from the permuted power,
|
||||||
|
so the null keeps the kinematic channel (a slower bullet really does arrive later) and destroys
|
||||||
|
only the response. p is the two-sided permutation p on delta = mean(HIGH) - mean(LOW) inside that band.
|
||||||
|
miss 0-100 n/a 100-200 d=+5.354(null sd=7.077,p=0.893) 200-300 d=+0.348(null sd=4.611,p=0.389) 300-450 d=+5.913(null sd=1.285,p=0.274) 450+ d=+8.238(null sd=1.040,p=0.005)
|
||||||
|
miss_per_tick 0-100 n/a 100-200 d=-0.354(null sd=0.692,p=0.623) 200-300 d=-0.342(null sd=0.332,p=0.524) 300-450 d=+0.026(null sd=0.064,p=0.010) 450+ d=-0.008(null sd=0.040,p=0.005)
|
||||||
|
lat_disp_abs 0-100 n/a 100-200 d=+19.401(null sd=7.561,p=0.075) 200-300 d=+11.592(null sd=3.772,p=0.075) 300-450 d=+3.497(null sd=0.895,p=0.444) 450+ d=+5.865(null sd=0.679,p=0.005)
|
||||||
|
flight 0-100 n/a 100-200 d=+1.473(null sd=0.557,p=0.584) 200-300 d=+1.553(null sd=0.209,p=0.319) 300-450 d=+1.349(null sd=0.044,p=0.005) 450+ d=+2.131(null sd=0.039,p=0.015)
|
||||||
|
lat_fixed_abs 0-100 n/a 100-200 d=+16.205(null sd=6.822,p=0.020) 200-300 d=+2.890(null sd=2.739,p=0.269) 300-450 d=-1.453(null sd=0.521,p=0.005) 450+ d=-0.154(null sd=0.340,p=0.663)
|
||||||
|
lat_ctrl_abs 0-100 n/a 100-200 d=+3.671(null sd=7.943,p=0.683) 200-300 d=+5.532(null sd=2.951,p=0.095) 300-450 d=-0.846(null sd=0.492,p=0.095) 450+ d=+0.610(null sd=0.339,p=0.080)
|
||||||
|
latency 0-100 n/a 100-200 d=+0.093(null sd=1.210,p=0.898) 200-300 d=-1.349(null sd=0.628,p=0.035) 300-450 d=-0.094(null sd=0.145,p=0.484) 450+ d=+0.677(null sd=0.117,p=0.005)
|
||||||
|
turn_fixed 0-100 n/a 100-200 d=+0.574(null sd=0.406,p=0.150) 200-300 d=+0.316(null sd=0.147,p=0.040) 300-450 d=+0.034(null sd=0.021,p=0.125) 450+ d=-0.148(null sd=0.012,p=0.005)
|
||||||
|
turn_ctrl 0-100 n/a 100-200 d=+0.839(null sd=0.442,p=0.060) 200-300 d=+0.407(null sd=0.154,p=0.005) 300-450 d=+0.135(null sd=0.021,p=0.005) 450+ d=+0.064(null sd=0.014,p=0.005)
|
||||||
|
speed_fixed 0-100 n/a 100-200 d=+0.086(null sd=0.533,p=0.913) 200-300 d=+0.021(null sd=0.177,p=0.853) 300-450 d=-0.050(null sd=0.033,p=0.135) 450+ d=-0.007(null sd=0.022,p=0.758)
|
||||||
|
hit 0-100 n/a 100-200 d=+0.089(null sd=0.115,p=0.574) 200-300 d=-0.022(null sd=0.034,p=0.668) 300-450 d=+0.004(null sd=0.005,p=0.429) 450+ d=-0.013(null sd=0.003,p=0.005)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
FIXED-HORIZON TEST (no arrival tick, no flight-window confound)
|
||||||
|
|perpendicular offset of DrussGT from our aim line| at k ticks after the fire (flight window),
|
||||||
|
and the same quantity in the CONTROL window at k+40 ticks, when the bullet is long gone.
|
||||||
|
A response to our shot would be ~0 at k=5 and grow with k; a phase/geometry difference is
|
||||||
|
present at k=5 and identical in the control.
|
||||||
|
band 200-300 (nLOW=130 nHIGH=780 per k)
|
||||||
|
k= 5 flight: LOW= 55.5 HIGH= 54.0 d= -1.50 | control(+40): LOW= 132.6 HIGH= 136.8 d= +4.11
|
||||||
|
k=10 flight: LOW= 60.1 HIGH= 57.4 d= -2.76 | control(+40): LOW= 141.8 HIGH= 144.8 d= +3.00
|
||||||
|
k=15 flight: LOW= 72.4 HIGH= 69.1 d= -3.28 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 83.8 HIGH= 82.5 d= -1.29 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 94.5 HIGH= 93.8 d= -0.69 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 104.2 HIGH= 105.2 d= +1.05 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
band 300-450 (nLOW=5272 nHIGH=10696 per k)
|
||||||
|
k= 5 flight: LOW= 80.1 HIGH= 82.8 d= +2.67 | control(+40): LOW= 140.1 HIGH= 141.5 d= +1.35
|
||||||
|
k=10 flight: LOW= 80.3 HIGH= 83.4 d= +3.17 | control(+40): LOW= 145.8 HIGH= 146.7 d= +0.91
|
||||||
|
k=15 flight: LOW= 87.9 HIGH= 91.4 d= +3.46 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 99.5 HIGH= 102.4 d= +2.95 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 111.0 HIGH= 113.5 d= +2.46 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 120.3 HIGH= 122.5 d= +2.20 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
band 450+ (nLOW=12960 nHIGH=20126 per k)
|
||||||
|
k= 5 flight: LOW= 94.9 HIGH= 99.1 d= +4.21 | control(+40): LOW= 144.2 HIGH= 149.7 d= +5.57
|
||||||
|
k=10 flight: LOW= 95.0 HIGH= 98.9 d= +3.86 | control(+40): LOW= 147.8 HIGH= 153.1 d= +5.23
|
||||||
|
k=15 flight: LOW= 100.7 HIGH= 104.3 d= +3.53 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 109.0 HIGH= 112.5 d= +3.50 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 118.7 HIGH= 122.6 d= +3.83 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 127.7 HIGH= 132.5 d= +4.74 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
FLIGHT- *AND* RANGE-MATCHED CONTRAST (kinematics and range both removed)
|
||||||
|
Within each range band we compare LOW vs HIGH power only inside the SAME cell = (integer
|
||||||
|
flight window, 50 px range sub-bin), pooling the per-cell differences with n weights. A longer
|
||||||
|
window lets DrussGT drift further off the line; at a fixed window a slower bullet implies a
|
||||||
|
shorter range and the gun's aim error grows with range. Anything left here is neither.
|
||||||
|
band obs d n used bins | null sd p_two
|
||||||
|
200-300 +19.25 480 7 | 5.28 0.005
|
||||||
|
300-450 +38.59 15455 28 | 1.17 0.005
|
||||||
|
450+ +59.05 29975 46 | 1.09 0.005
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
HEADLINE WITHIN-BAND CONTRAST LOW p in [0.50,0.75) vs HIGH p in [1.00,1.50)
|
||||||
|
(HELD fixed inside each range band; 95%% CI from a ROUND-cluster bootstrap)
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
miss 0-100 6 2 | (too few for a contrast)
|
||||||
|
miss 100-200 53 23 | 43.03 47.92 +4.89 [-18.33,+31.08]
|
||||||
|
miss 200-300 130 780 | 74.31 74.72 +0.40 [-11.42,+11.93]
|
||||||
|
miss 300-450 5272 10696 | 104.67 110.55 +5.88 [+2.64,+9.07]
|
||||||
|
miss 450+ 12960 20126 | 124.01 132.26 +8.25 [+5.39,+11.25]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
miss_per_tick 0-100 6 2 | (too few for a contrast)
|
||||||
|
miss_per_tick 100-200 53 23 | 4.45 4.05 -0.40 [-2.84,+2.26]
|
||||||
|
miss_per_tick 200-300 130 780 | 5.01 4.67 -0.34 [-1.16,+0.46]
|
||||||
|
miss_per_tick 300-450 5272 10696 | 4.85 4.88 +0.03 [-0.13,+0.17]
|
||||||
|
miss_per_tick 450+ 12960 20126 | 4.52 4.51 -0.01 [-0.12,+0.10]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_disp_abs 0-100 6 2 | (too few for a contrast)
|
||||||
|
lat_disp_abs 100-200 53 23 | 37.57 56.50 +18.93 [-3.78,+41.06]
|
||||||
|
lat_disp_abs 200-300 130 780 | 59.91 71.56 +11.64 [-0.85,+25.19]
|
||||||
|
lat_disp_abs 300-450 5272 10696 | 88.47 91.94 +3.47 [+0.78,+6.28]
|
||||||
|
lat_disp_abs 450+ 12960 20126 | 97.42 103.30 +5.88 [+3.44,+8.31]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_disp_per_tick 0-100 6 2 | (too few for a contrast)
|
||||||
|
lat_disp_per_tick 100-200 53 23 | 3.44 4.76 +1.33 [-0.48,+3.23]
|
||||||
|
lat_disp_per_tick 200-300 130 780 | 3.79 4.12 +0.33 [-0.39,+1.14]
|
||||||
|
lat_disp_per_tick 300-450 5272 10696 | 3.94 3.86 -0.08 [-0.19,+0.03]
|
||||||
|
lat_disp_per_tick 450+ 12960 20126 | 3.47 3.42 -0.04 [-0.12,+0.03]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_fixed_abs 0-100 6 2 | (too few for a contrast)
|
||||||
|
lat_fixed_abs 100-200 52 23 | 43.26 59.47 +16.21 [-4.15,+37.80]
|
||||||
|
lat_fixed_abs 200-300 130 780 | 47.20 50.09 +2.89 [-5.91,+12.89]
|
||||||
|
lat_fixed_abs 300-450 5272 10696 | 54.20 52.75 -1.45 [-2.96,+0.08]
|
||||||
|
lat_fixed_abs 450+ 12960 20126 | 55.63 55.47 -0.15 [-1.15,+0.84]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_ctrl_abs 0-100 5 2 | (too few for a contrast)
|
||||||
|
lat_ctrl_abs 100-200 41 23 | 46.92 50.59 +3.67 [-19.55,+27.70]
|
||||||
|
lat_ctrl_abs 200-300 127 780 | 43.68 49.21 +5.53 [-3.87,+15.33]
|
||||||
|
lat_ctrl_abs 300-450 5155 10695 | 52.80 51.95 -0.85 [-2.20,+0.67]
|
||||||
|
lat_ctrl_abs 450+ 12828 20120 | 52.03 52.64 +0.61 [-0.38,+1.58]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
flight 0-100 6 2 | (too few for a contrast)
|
||||||
|
flight 100-200 53 23 | 10.42 11.87 +1.45 [-0.38,+3.14]
|
||||||
|
flight 200-300 130 780 | 15.28 16.86 +1.58 [+0.96,+2.18]
|
||||||
|
flight 300-450 5272 10696 | 22.36 23.70 +1.35 [+1.18,+1.52]
|
||||||
|
flight 450+ 12960 20126 | 28.38 30.52 +2.13 [+1.81,+2.44]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
latency 0-100 5 2 | (too few for a contrast)
|
||||||
|
latency 100-200 29 16 | 6.34 6.44 +0.09 [-4.23,+3.45]
|
||||||
|
latency 200-300 78 566 | 9.03 7.68 -1.35 [-2.92,+0.21]
|
||||||
|
latency 300-450 2968 6860 | 11.55 11.46 -0.09 [-0.47,+0.30]
|
||||||
|
latency 450+ 8682 13884 | 13.60 14.27 +0.68 [+0.36,+0.99]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_flight 0-100 6 2 | (too few for a contrast)
|
||||||
|
turn_flight 100-200 53 23 | 2.01 2.86 +0.85 [-0.33,+2.21]
|
||||||
|
turn_flight 200-300 130 780 | 2.08 2.49 +0.40 [-0.08,+0.92]
|
||||||
|
turn_flight 300-450 5272 10696 | 1.48 1.61 +0.13 [+0.05,+0.21]
|
||||||
|
turn_flight 450+ 12960 20126 | 1.49 1.46 -0.03 [-0.09,+0.02]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_fixed 0-100 6 2 | (too few for a contrast)
|
||||||
|
turn_fixed 100-200 53 23 | 2.09 2.66 +0.57 [-0.54,+1.89]
|
||||||
|
turn_fixed 200-300 130 780 | 1.91 2.22 +0.32 [-0.15,+0.79]
|
||||||
|
turn_fixed 300-450 5272 10696 | 1.46 1.50 +0.03 [-0.04,+0.11]
|
||||||
|
turn_fixed 450+ 12960 20126 | 1.43 1.28 -0.15 [-0.20,-0.10]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_ctrl 0-100 5 2 | (too few for a contrast)
|
||||||
|
turn_ctrl 100-200 44 23 | 2.20 3.04 +0.84 [-0.35,+2.28]
|
||||||
|
turn_ctrl 200-300 127 780 | 1.78 2.19 +0.41 [-0.04,+0.88]
|
||||||
|
turn_ctrl 300-450 5211 10696 | 1.44 1.57 +0.13 [+0.05,+0.21]
|
||||||
|
turn_ctrl 450+ 12902 20126 | 1.52 1.58 +0.06 [+0.01,+0.12]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
speed_flight 0-100 6 2 | (too few for a contrast)
|
||||||
|
speed_flight 100-200 53 23 | 6.05 6.05 +0.00 [-1.80,+1.55]
|
||||||
|
speed_flight 200-300 130 780 | 5.39 5.44 +0.05 [-0.51,+0.59]
|
||||||
|
speed_flight 300-450 5272 10696 | 5.49 5.49 +0.01 [-0.08,+0.10]
|
||||||
|
speed_flight 450+ 12960 20126 | 5.46 5.48 +0.01 [-0.06,+0.08]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
speed_ctrl 0-100 5 2 | (too few for a contrast)
|
||||||
|
speed_ctrl 100-200 44 23 | 5.84 5.86 +0.01 [-1.55,+1.45]
|
||||||
|
speed_ctrl 200-300 127 780 | 5.63 5.48 -0.16 [-0.82,+0.43]
|
||||||
|
speed_ctrl 300-450 5211 10696 | 5.50 5.45 -0.05 [-0.14,+0.05]
|
||||||
|
speed_ctrl 450+ 12902 20126 | 5.46 5.47 +0.01 [-0.06,+0.09]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
hit 0-100 6 2 | (too few for a contrast)
|
||||||
|
hit 100-200 53 23 | 0.30 0.39 +0.09 [-0.33,+0.44]
|
||||||
|
hit 200-300 130 780 | 0.18 0.16 -0.02 [-0.10,+0.05]
|
||||||
|
hit 300-450 5272 10696 | 0.11 0.12 +0.00 [-0.01,+0.02]
|
||||||
|
hit 450+ 12960 20126 | 0.10 0.09 -0.01 [-0.02,-0.00]
|
||||||
|
|
||||||
|
OUR fired-power histogram (exact values, top 12):
|
||||||
|
1.00:31641 0.50:14182 0.10:627 0.51:222 0.55:203 0.53:195 0.60:193 0.58:192 0.59:192 0.62:190 0.54:189 0.57:184
|
||||||
|
========================================================================================================
|
||||||
|
CORPUS powtest 35 battles 245 rounds 24280 usable shots by US (ModularBot)
|
||||||
|
attribution: owner id -> {e=DrussGT, s=us} recovered per battle from fire geometry+energy;
|
||||||
|
cross-checked on the death events: 246/246 deaths have the mapped victim at ~0 energy
|
||||||
|
(attribution is per-battle because the Tank Royale owner id is not stable across runs)
|
||||||
|
|
||||||
|
>> THE CONFOUNDER IS REAL: our fired power is a range + energy cap
|
||||||
|
band power shots firePx ourE enemyE flight
|
||||||
|
100-200 1.00-1.50 8 179.5 77.7 79.5 13.25
|
||||||
|
100-200 >=1.50 74 169.5 84.3 82.3 14.72
|
||||||
|
200-300 0.50-0.75 16 262.3 21.6 33.5 15.44
|
||||||
|
200-300 1.00-1.50 406 260.5 83.1 82.2 16.51
|
||||||
|
200-300 >=1.50 17 246.1 37.9 22.0 21.00
|
||||||
|
300-450 <0.50 51 410.0 30.7 2.7 21.00
|
||||||
|
300-450 0.50-0.75 1388 412.6 14.4 17.9 22.36
|
||||||
|
300-450 0.75-1.00 254 412.0 29.0 23.1 23.52
|
||||||
|
300-450 1.00-1.50 8151 404.0 59.6 54.1 23.78
|
||||||
|
450+ <0.50 115 533.7 18.7 1.7 26.86
|
||||||
|
450+ 0.50-0.75 2249 524.9 13.6 17.8 27.75
|
||||||
|
450+ 0.75-1.00 456 526.4 28.8 25.2 29.26
|
||||||
|
450+ 1.00-1.50 11085 525.3 52.6 46.0 29.93
|
||||||
|
|
||||||
|
(a) MISS AT ARRIVAL px - perpendicular offset of DrussGT from our aim line
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 28.34 34.35
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 58.35 - 32.74 62.45
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 70.50 71.07 73.03 112.28
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 100.28 106.22 111.86 109.93 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 131.41 122.00 130.11 131.86 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(a') MISS AT ARRIVAL / flight ticks (px per tick - power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 3.15 2.89
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 7.29 - 2.51 4.57
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 4.86 4.68 4.68 5.46
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 4.93 4.94 4.96 4.83 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 5.02 4.56 4.64 4.59 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(b) LATERAL DISPLACEMENT px over the flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 50.56 59.97
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 6.28 - 71.40 63.01
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 58.55 61.90 71.26 109.05
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 68.90 91.35 94.37 93.56 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 104.97 96.90 102.50 102.74 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(b/ ) LATERAL DISPLACEMENT / flight ticks (px per tick - power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 5.62 5.11
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 0.78 - 5.34 4.12
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 3.70 3.43 4.22 5.04
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 3.29 4.07 4.00 3.91 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 3.93 3.53 3.54 3.48 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(b') LATERAL DISPLACEMENT px over a FIXED 12-tick window (power-neutral)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 72.08 63.83
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 3.66 - 63.36 49.33
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 41.73 28.58 50.71 54.65
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 46.10 53.99 54.21 53.41 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 61.58 55.53 56.08 55.85 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(b'') CONTROL: lateral displacement px, SAME shot, 40 ticks later (bullet gone)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 6.62 55.92
|
||||||
|
n=0 n=0 n=0 n=1 n=3
|
||||||
|
100-200 - 20.12 - 53.96 51.68
|
||||||
|
n=0 n=1 n=0 n=8 n=69
|
||||||
|
200-300 - 51.96 42.95 50.50 46.25
|
||||||
|
n=0 n=15 n=3 n=405 n=9
|
||||||
|
300-450 39.77 53.98 53.45 52.69 -
|
||||||
|
n=43 n=1362 n=252 n=8118 n=0
|
||||||
|
450+ 57.71 52.62 51.98 53.00 -
|
||||||
|
n=95 n=2212 n=454 n=11037 n=0
|
||||||
|
|
||||||
|
(f) FLIGHT WINDOW LENGTH ticks (low power = FASTER bullet = shorter)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 9.00 11.50
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 8.00 - 13.25 14.72
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 15.44 16.67 16.51 21.00
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 21.00 22.36 23.52 23.78 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 26.86 27.75 29.26 29.93 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(c) RESPONSE LATENCY ticks to a >15 deg heading turn
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 5.50 4.00
|
||||||
|
n=0 n=0 n=0 n=2 n=3
|
||||||
|
100-200 - 2.00 - 9.25 7.40
|
||||||
|
n=0 n=1 n=0 n=4 n=53
|
||||||
|
200-300 - 9.29 10.50 7.58 9.21
|
||||||
|
n=0 n=7 n=2 n=303 n=14
|
||||||
|
300-450 11.86 12.03 13.06 11.75 -
|
||||||
|
n=22 n=743 n=152 n=5004 n=0
|
||||||
|
450+ 13.79 13.89 14.27 14.36 -
|
||||||
|
n=70 n=1457 n=313 n=7615 n=0
|
||||||
|
|
||||||
|
(d) MEAN TURN RATE deg/tick, flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 2.85 3.72
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 3.76 - 1.52 2.60
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 1.86 1.48 2.49 2.01
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 1.28 1.42 1.43 1.55 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 1.42 1.45 1.41 1.42 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(d') MEAN TURN RATE deg/tick, fixed 12-tick window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 2.98 3.38
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 4.90 - 1.32 2.41
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 1.68 1.29 2.20 2.38
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 1.29 1.39 1.35 1.43 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 1.39 1.40 1.37 1.25 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(d'') CONTROL: mean turn rate deg/tick, 40 ticks later
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 4.12 3.24
|
||||||
|
n=0 n=0 n=0 n=1 n=3
|
||||||
|
100-200 - 4.93 - 2.58 2.32
|
||||||
|
n=0 n=1 n=0 n=8 n=70
|
||||||
|
200-300 - 1.96 2.13 2.24 3.14
|
||||||
|
n=0 n=15 n=3 n=405 n=11
|
||||||
|
300-450 1.11 1.39 1.45 1.55 -
|
||||||
|
n=47 n=1377 n=253 n=8135 n=0
|
||||||
|
450+ 1.34 1.49 1.45 1.54 -
|
||||||
|
n=105 n=2231 n=455 n=11069 n=0
|
||||||
|
|
||||||
|
(d) MEAN SPEED px/tick, flight window
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 8.00 7.01
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 2.25 - 6.77 5.45
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 5.16 5.82 5.48 6.04
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 4.81 5.50 5.56 5.51 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 5.74 5.43 5.54 5.51 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
(d'') CONTROL: mean speed px/tick, 40 ticks later
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 4.08 6.49
|
||||||
|
n=0 n=0 n=0 n=1 n=3
|
||||||
|
100-200 - 4.08 - 5.97 5.88
|
||||||
|
n=0 n=1 n=0 n=8 n=70
|
||||||
|
200-300 - 5.50 5.18 5.61 6.99
|
||||||
|
n=0 n=15 n=3 n=405 n=11
|
||||||
|
300-450 4.60 5.54 5.53 5.47 -
|
||||||
|
n=47 n=1377 n=253 n=8135 n=0
|
||||||
|
450+ 5.66 5.46 5.47 5.51 -
|
||||||
|
n=105 n=2231 n=455 n=11069 n=0
|
||||||
|
|
||||||
|
(e) fraction where |perp| GREW (moved further off the line)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 0.50 0.50
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 1.00 - 0.25 0.58
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 0.56 0.67 0.58 0.82
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 0.55 0.61 0.59 0.59 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 0.59 0.58 0.58 0.61 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
OUTCOME hit rate (real server events)
|
||||||
|
band <0.50 0.50-0.75 0.75-1.00 1.00-1.50 >=1.50
|
||||||
|
0-100 - - - 0.50 0.50
|
||||||
|
n=0 n=0 n=0 n=2 n=4
|
||||||
|
100-200 - 0.00 - 0.50 0.20
|
||||||
|
n=0 n=1 n=0 n=8 n=74
|
||||||
|
200-300 - 0.19 0.33 0.16 0.00
|
||||||
|
n=0 n=16 n=3 n=406 n=17
|
||||||
|
300-450 0.00 0.12 0.12 0.12 -
|
||||||
|
n=51 n=1388 n=254 n=8151 n=0
|
||||||
|
450+ 0.02 0.10 0.10 0.09 -
|
||||||
|
n=115 n=2249 n=456 n=11085 n=0
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
SHUFFLED-LABEL NULL (labels permuted WITHIN each range band, 400 reps)
|
||||||
|
For the arrival-dependent metrics the arrival tick is RE-DERIVED from the permuted power,
|
||||||
|
so the null keeps the kinematic channel (a slower bullet really does arrive later) and destroys
|
||||||
|
only the response. p is the two-sided permutation p on delta = mean(HIGH) - mean(LOW) inside that band.
|
||||||
|
miss 0-100 n/a 100-200 n/a 200-300 d=+3.592(null sd=12.030,p=0.993) 300-450 d=+3.788(null sd=2.135,p=0.793) 450+ d=+9.865(null sd=1.815,p=0.005)
|
||||||
|
miss_per_tick 0-100 n/a 100-200 n/a 200-300 d=-0.124(null sd=0.878,p=0.948) 300-450 d=-0.100(null sd=0.107,p=0.514) 450+ d=+0.031(null sd=0.074,p=0.005)
|
||||||
|
lat_disp_abs 0-100 n/a 100-200 n/a 200-300 d=+13.420(null sd=10.885,p=0.439) 300-450 d=+2.201(null sd=1.619,p=0.200) 450+ d=+5.908(null sd=1.474,p=0.160)
|
||||||
|
flight 0-100 n/a 100-200 n/a 200-300 d=+1.200(null sd=0.588,p=0.933) 300-450 d=+1.419(null sd=0.071,p=0.005) 450+ d=+2.177(null sd=0.078,p=0.818)
|
||||||
|
lat_fixed_abs 0-100 n/a 100-200 n/a 200-300 d=+8.977(null sd=6.784,p=0.195) 300-450 d=-0.580(null sd=0.874,p=0.484) 450+ d=+0.315(null sd=0.672,p=0.603)
|
||||||
|
lat_ctrl_abs 0-100 n/a 100-200 n/a 200-300 d=-1.460(null sd=7.562,p=0.783) 300-450 d=-1.285(null sd=0.876,p=0.155) 450+ d=+0.386(null sd=0.653,p=0.519)
|
||||||
|
latency 0-100 n/a 100-200 n/a 200-300 n/a 300-450 d=-0.285(null sd=0.258,p=0.279) 450+ d=+0.473(null sd=0.236,p=0.045)
|
||||||
|
turn_fixed 0-100 n/a 100-200 n/a 200-300 d=+0.514(null sd=0.383,p=0.195) 300-450 d=+0.040(null sd=0.036,p=0.254) 450+ d=-0.157(null sd=0.027,p=0.005)
|
||||||
|
turn_ctrl 0-100 n/a 100-200 n/a 200-300 d=+0.279(null sd=0.395,p=0.494) 300-450 d=+0.156(null sd=0.040,p=0.005) 450+ d=+0.052(null sd=0.027,p=0.055)
|
||||||
|
speed_fixed 0-100 n/a 100-200 n/a 200-300 d=+0.331(null sd=0.467,p=0.504) 300-450 d=+0.029(null sd=0.055,p=0.648) 450+ d=+0.053(null sd=0.042,p=0.200)
|
||||||
|
hit 0-100 n/a 100-200 n/a 200-300 d=-0.025(null sd=0.094,p=1.000) 300-450 d=-0.002(null sd=0.009,p=0.918) 450+ d=-0.007(null sd=0.006,p=0.344)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
FIXED-HORIZON TEST (no arrival tick, no flight-window confound)
|
||||||
|
|perpendicular offset of DrussGT from our aim line| at k ticks after the fire (flight window),
|
||||||
|
and the same quantity in the CONTROL window at k+40 ticks, when the bullet is long gone.
|
||||||
|
A response to our shot would be ~0 at k=5 and grow with k; a phase/geometry difference is
|
||||||
|
present at k=5 and identical in the control.
|
||||||
|
band 200-300 (nLOW=16 nHIGH=406 per k)
|
||||||
|
k= 5 flight: LOW= 54.8 HIGH= 54.5 d= -0.24 | control(+40): LOW= 123.5 HIGH= 134.1 d=+10.56
|
||||||
|
k=10 flight: LOW= 58.3 HIGH= 57.3 d= -1.02 | control(+40): LOW= 138.4 HIGH= 142.9 d= +4.52
|
||||||
|
k=15 flight: LOW= 69.1 HIGH= 69.6 d= +0.52 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 77.8 HIGH= 83.8 d= +6.03 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 84.9 HIGH= 96.8 d=+11.83 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 98.6 HIGH= 108.8 d=+10.24 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
band 300-450 (nLOW=1388 nHIGH=8151 per k)
|
||||||
|
k= 5 flight: LOW= 78.4 HIGH= 84.0 d= +5.62 | control(+40): LOW= 143.3 HIGH= 141.4 d= -1.87
|
||||||
|
k=10 flight: LOW= 79.1 HIGH= 84.0 d= +4.83 | control(+40): LOW= 149.7 HIGH= 147.2 d= -2.52
|
||||||
|
k=15 flight: LOW= 88.3 HIGH= 91.5 d= +3.15 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 101.0 HIGH= 102.3 d= +1.29 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 112.2 HIGH= 112.9 d= +0.70 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 121.1 HIGH= 121.8 d= +0.60 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
band 450+ (nLOW=2249 nHIGH=11085 per k)
|
||||||
|
k= 5 flight: LOW= 93.0 HIGH= 99.0 d= +5.97 | control(+40): LOW= 143.7 HIGH= 150.3 d= +6.59
|
||||||
|
k=10 flight: LOW= 93.3 HIGH= 99.1 d= +5.83 | control(+40): LOW= 147.7 HIGH= 153.8 d= +6.03
|
||||||
|
k=15 flight: LOW= 99.4 HIGH= 104.9 d= +5.50 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=20 flight: LOW= 108.1 HIGH= 113.2 d= +5.12 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=25 flight: LOW= 118.3 HIGH= 123.5 d= +5.13 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
k=30 flight: LOW= 126.7 HIGH= 133.3 d= +6.61 | control(+40): LOW= nan HIGH= nan d= +nan
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
FLIGHT- *AND* RANGE-MATCHED CONTRAST (kinematics and range both removed)
|
||||||
|
Within each range band we compare LOW vs HIGH power only inside the SAME cell = (integer
|
||||||
|
flight window, 50 px range sub-bin), pooling the per-cell differences with n weights. A longer
|
||||||
|
window lets DrussGT drift further off the line; at a fixed window a slower bullet implies a
|
||||||
|
shorter range and the gun's aim error grows with range. Anything left here is neither.
|
||||||
|
band obs d n used bins | null sd p_two
|
||||||
|
300-450 +37.84 8588 23 | 2.32 0.005
|
||||||
|
450+ +57.39 10809 34 | 2.31 0.005
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------
|
||||||
|
HEADLINE WITHIN-BAND CONTRAST LOW p in [0.50,0.75) vs HIGH p in [1.00,1.50)
|
||||||
|
(HELD fixed inside each range band; 95%% CI from a ROUND-cluster bootstrap)
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
miss 0-100 0 2 | (too few for a contrast)
|
||||||
|
miss 100-200 1 8 | (too few for a contrast)
|
||||||
|
miss 200-300 16 406 | 70.50 73.03 +2.53 [-12.34,+28.80]
|
||||||
|
miss 300-450 1388 8151 | 106.22 109.93 +3.72 [-1.50,+8.65]
|
||||||
|
miss 450+ 2249 11085 | 122.00 131.86 +9.86 [+4.56,+15.25]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
miss_per_tick 0-100 0 2 | (too few for a contrast)
|
||||||
|
miss_per_tick 100-200 1 8 | (too few for a contrast)
|
||||||
|
miss_per_tick 200-300 16 406 | 4.86 4.68 -0.19 [-1.31,+1.76]
|
||||||
|
miss_per_tick 300-450 1388 8151 | 4.94 4.83 -0.10 [-0.37,+0.15]
|
||||||
|
miss_per_tick 450+ 2249 11085 | 4.56 4.59 +0.03 [-0.19,+0.25]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_disp_abs 0-100 0 2 | (too few for a contrast)
|
||||||
|
lat_disp_abs 100-200 1 8 | (too few for a contrast)
|
||||||
|
lat_disp_abs 200-300 16 406 | 58.55 71.26 +12.71 [-18.26,+36.16]
|
||||||
|
lat_disp_abs 300-450 1388 8151 | 91.35 93.56 +2.21 [-2.02,+6.72]
|
||||||
|
lat_disp_abs 450+ 2249 11085 | 96.90 102.74 +5.84 [+1.65,+9.94]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_disp_per_tick 0-100 0 2 | (too few for a contrast)
|
||||||
|
lat_disp_per_tick 100-200 1 8 | (too few for a contrast)
|
||||||
|
lat_disp_per_tick 200-300 16 406 | 3.70 4.22 +0.52 [-0.90,+1.65]
|
||||||
|
lat_disp_per_tick 300-450 1388 8151 | 4.07 3.91 -0.16 [-0.32,+0.02]
|
||||||
|
lat_disp_per_tick 450+ 2249 11085 | 3.53 3.48 -0.05 [-0.20,+0.11]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_fixed_abs 0-100 0 2 | (too few for a contrast)
|
||||||
|
lat_fixed_abs 100-200 1 8 | (too few for a contrast)
|
||||||
|
lat_fixed_abs 200-300 16 406 | 41.73 50.71 +8.98 [-11.36,+23.25]
|
||||||
|
lat_fixed_abs 300-450 1388 8151 | 53.99 53.41 -0.58 [-3.07,+1.89]
|
||||||
|
lat_fixed_abs 450+ 2249 11085 | 55.53 55.85 +0.31 [-1.24,+1.87]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
lat_ctrl_abs 0-100 0 1 | (too few for a contrast)
|
||||||
|
lat_ctrl_abs 100-200 1 8 | (too few for a contrast)
|
||||||
|
lat_ctrl_abs 200-300 15 405 | 51.96 50.50 -1.46 [-21.67,+14.83]
|
||||||
|
lat_ctrl_abs 300-450 1362 8118 | 53.98 52.69 -1.29 [-3.11,+0.47]
|
||||||
|
lat_ctrl_abs 450+ 2212 11037 | 52.62 53.00 +0.39 [-0.83,+1.57]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
flight 0-100 0 2 | (too few for a contrast)
|
||||||
|
flight 100-200 1 8 | (too few for a contrast)
|
||||||
|
flight 200-300 16 406 | 15.44 16.51 +1.07 [-0.61,+2.99]
|
||||||
|
flight 300-450 1388 8151 | 22.36 23.78 +1.42 [+1.15,+1.68]
|
||||||
|
flight 450+ 2249 11085 | 27.75 29.93 +2.18 [+1.63,+2.73]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
latency 0-100 0 2 | (too few for a contrast)
|
||||||
|
latency 100-200 1 4 | (too few for a contrast)
|
||||||
|
latency 200-300 7 303 | (too few for a contrast)
|
||||||
|
latency 300-450 743 5004 | 12.03 11.75 -0.29 [-0.81,+0.25]
|
||||||
|
latency 450+ 1457 7615 | 13.89 14.36 +0.47 [-0.15,+1.14]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_flight 0-100 0 2 | (too few for a contrast)
|
||||||
|
turn_flight 100-200 1 8 | (too few for a contrast)
|
||||||
|
turn_flight 200-300 16 406 | 1.86 2.49 +0.62 [+0.33,+1.09]
|
||||||
|
turn_flight 300-450 1388 8151 | 1.42 1.55 +0.13 [+0.04,+0.23]
|
||||||
|
turn_flight 450+ 2249 11085 | 1.45 1.42 -0.03 [-0.13,+0.06]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_fixed 0-100 0 2 | (too few for a contrast)
|
||||||
|
turn_fixed 100-200 1 8 | (too few for a contrast)
|
||||||
|
turn_fixed 200-300 16 406 | 1.68 2.20 +0.51 [+0.11,+1.18]
|
||||||
|
turn_fixed 300-450 1388 8151 | 1.39 1.43 +0.04 [-0.06,+0.15]
|
||||||
|
turn_fixed 450+ 2249 11085 | 1.40 1.25 -0.16 [-0.26,-0.06]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
turn_ctrl 0-100 0 1 | (too few for a contrast)
|
||||||
|
turn_ctrl 100-200 1 8 | (too few for a contrast)
|
||||||
|
turn_ctrl 200-300 15 405 | 1.96 2.24 +0.28 [-0.78,+0.97]
|
||||||
|
turn_ctrl 300-450 1377 8135 | 1.39 1.55 +0.16 [+0.04,+0.28]
|
||||||
|
turn_ctrl 450+ 2231 11069 | 1.49 1.54 +0.05 [-0.06,+0.15]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
speed_flight 0-100 0 2 | (too few for a contrast)
|
||||||
|
speed_flight 100-200 1 8 | (too few for a contrast)
|
||||||
|
speed_flight 200-300 16 406 | 5.16 5.48 +0.31 [-0.96,+1.28]
|
||||||
|
speed_flight 300-450 1388 8151 | 5.50 5.51 +0.01 [-0.13,+0.14]
|
||||||
|
speed_flight 450+ 2249 11085 | 5.43 5.51 +0.08 [-0.02,+0.19]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
speed_ctrl 0-100 0 1 | (too few for a contrast)
|
||||||
|
speed_ctrl 100-200 1 8 | (too few for a contrast)
|
||||||
|
speed_ctrl 200-300 15 405 | 5.50 5.61 +0.11 [-0.78,+1.18]
|
||||||
|
speed_ctrl 300-450 1377 8135 | 5.54 5.47 -0.07 [-0.20,+0.07]
|
||||||
|
speed_ctrl 450+ 2231 11069 | 5.46 5.51 +0.05 [-0.06,+0.15]
|
||||||
|
|
||||||
|
metric band nLOW nHIGH | LOW HIGH delta 95% CI
|
||||||
|
hit 0-100 0 2 | (too few for a contrast)
|
||||||
|
hit 100-200 1 8 | (too few for a contrast)
|
||||||
|
hit 200-300 16 406 | 0.19 0.16 -0.02 [-0.17,+0.20]
|
||||||
|
hit 300-450 1388 8151 | 0.12 0.12 -0.00 [-0.03,+0.02]
|
||||||
|
hit 450+ 2249 11085 | 0.10 0.09 -0.01 [-0.02,+0.01]
|
||||||
|
|
||||||
|
OUR fired-power histogram (exact values, top 12):
|
||||||
|
1.00:19653 0.50:2760 2.00:64 0.62:61 0.90:56 0.52:56 0.10:56 0.57:54 0.59:53 0.67:49 0.65:47 0.64:44
|
||||||
+6067
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,342 @@
|
|||||||
|
# DrussGT's movement response as a function of the bullet power we fire
|
||||||
|
|
||||||
|
**Question (the user's hypothesis).** *"When low power, more bullets fly and I see
|
||||||
|
DrussGT dodging easier. Is this a DrussGT hidden feature at low energy to suddenly
|
||||||
|
increase the capability to dodge? I doubt."*
|
||||||
|
|
||||||
|
**Answer: no.** Once range is controlled, DrussGT's movement does **not** respond to
|
||||||
|
the power we fire. The perceived "better dodging at low power" is a property of the
|
||||||
|
*situation* the low-power shots are fired in (we are nearly dead, so the whole
|
||||||
|
engagement geometry differs), not of the bullet — and most of the raw difference in
|
||||||
|
miss distance is pure flight-time kinematics: a low-power bullet is a *faster*
|
||||||
|
bullet, so it arrives **sooner** and DrussGT simply has fewer ticks to drift off the
|
||||||
|
line. Every measure that removes the flight window is flat.
|
||||||
|
|
||||||
|
This was measured on **70 real live battles / 490 rounds / 54 939 shots** against the
|
||||||
|
real, unmodified DrussGT (`/tmp/tfil_ab2/`) and **replicated on 35 more battles /
|
||||||
|
245 rounds / 24 280 shots** (`/tmp/powtest/`). Nothing here uses the offline fixture
|
||||||
|
harness; these are real robot-vs-robot Tank Royale battles recorded through
|
||||||
|
`tools/robocode_shim/run_bridge_battle.sh`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What was measured, and how a shot is attributed
|
||||||
|
|
||||||
|
Each shot is analysed from the recorded per-tick worldstate plus the real
|
||||||
|
fire/hit/wall event sidecar. For a shot fired by **ModularBot (us)** at
|
||||||
|
DrussGT with power `p` in direction `dir`:
|
||||||
|
|
||||||
|
* bullet speed `v = 20 - 3p` px/tick (so **low power = faster bullet = shorter
|
||||||
|
flight**);
|
||||||
|
* the bullet flies from the fire position along `u = (cos dir, sin dir)`;
|
||||||
|
* `along(t) = (D(t) - P0)·u` and `perp(t) = (D(t) - P0)×u` are DrussGT's along-track
|
||||||
|
distance and **perpendicular offset from our aim line** at tick `t`;
|
||||||
|
* **arrival tick** `k*` = the first tick where the bullet has travelled at least
|
||||||
|
`along(k*)`;
|
||||||
|
* **(a) miss distance at arrival** = `|perp(k*)|` (bot radius is 18 px).
|
||||||
|
|
||||||
|
**Attribution (this has bitten the project before, so it is stated explicitly).**
|
||||||
|
|
||||||
|
* In the capture rows, **`e*` is the SUBJECT = DrussGT** and **`s*` is the adversary
|
||||||
|
= ModularBot**. That is by construction of
|
||||||
|
`tools/robocode_shim/src/robocode_shim/TrBattleCapture.java` (`en` = the bot whose
|
||||||
|
name contains "DrussGT", written as `e*`; `sh` = the other, written as `s*`).
|
||||||
|
* The event sidecar's `owner` is the Tank Royale bot id, and **it is not stable
|
||||||
|
across runs** (start order varies). It is therefore recovered **per battle** from
|
||||||
|
the fire geometry (`owner`'s position equals the fire event's `x,y`, and its energy
|
||||||
|
drops by exactly `power` on the next capture row).
|
||||||
|
* Cross-check: for **496/496 death events** the mapped victim is the bot whose energy
|
||||||
|
is ~0 at the end of that round. The mapping agrees with the death evidence in
|
||||||
|
every battle.
|
||||||
|
* Sanity check that the mapping is the *right way round*: the recovered
|
||||||
|
ModularBot power histogram is exactly its documented policy
|
||||||
|
(`common_libs/gun_harness/virtual_bullets.nim`): a spike at **0.50**
|
||||||
|
(`TR_POWER_ENERGY_MIN`, when our energy ≤ 20), a spike at **1.00**
|
||||||
|
(`TR_POWER_FAR_CAP`, beyond `TR_POWER_FAR_DIST = 200`), and the linear
|
||||||
|
`TR_POWER_ENERGY_SLOPE` continuum in between.
|
||||||
|
* 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; our shots here are mostly
|
||||||
|
**0.50 and 1.00**, plus a continuum 0.10–0.99).
|
||||||
|
|
||||||
|
**Validation of the geometry (MEASURED).** For shots the *server* recorded as hits,
|
||||||
|
the measured miss distance is **mean 11.6 px, median 10.8 px, 80.6 % below the 18 px
|
||||||
|
bot radius**; for shots that hit a wall it is 133 px. The measurement is therefore
|
||||||
|
correct and the ~18 px figure is the right reference scale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The confound is real and severe (MEASURED)
|
||||||
|
|
||||||
|
Power is **not** randomly assigned. The policy only ever *caps* the gun's preference,
|
||||||
|
by range (`TR_POWER_FAR_DIST=200 → 1.0`), by **our own energy** (a linear slope from
|
||||||
|
0.5 at ≤20 to the cap at ≥80), by the finishing rule, and by the sub-average-chances
|
||||||
|
rule. The consequence is stark — this is the *same* table for both corpora (tfil_ab2):
|
||||||
|
|
||||||
|
| band | power | shots | fire px | OUR energy | enemy energy | flight ticks |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 300-450 | 0.50–0.75 | 5 272 | 411.3 | **13.8** | 19.2 | 22.36 |
|
||||||
|
| 300-450 | 0.75–1.00 | 880 | 407.7 | 29.0 | 28.5 | 23.49 |
|
||||||
|
| 300-450 | 1.00–1.50 | 10 696 | 402.0 | **69.2** | 62.6 | 23.70 |
|
||||||
|
| 450+ | 0.50–0.75 | 12 960 | 536.1 | **13.7** | 18.6 | 28.38 |
|
||||||
|
| 450+ | 0.75–1.00 | 2 424 | 540.4 | 28.8 | 26.4 | 30.18 |
|
||||||
|
| 450+ | 1.00–1.50 | 20 126 | 534.2 | **63.0** | 53.8 | 30.52 |
|
||||||
|
|
||||||
|
Two things follow, and they drive the whole design:
|
||||||
|
|
||||||
|
1. **Range must be held fixed** — hence everything below is stratified by range band,
|
||||||
|
and the headline is the stratified result.
|
||||||
|
2. **Within a range band, our power is almost a deterministic function of our own
|
||||||
|
energy.** Low-power shots are shots fired when *we* are nearly dead. So a naive
|
||||||
|
"low power vs high power" comparison inside a band is secretly a
|
||||||
|
"losing badly vs healthy" comparison. That is why a *within-shot* control is
|
||||||
|
needed to separate the two, and it is provided in §4.
|
||||||
|
|
||||||
|
Because the LOW/HIGH split is also a low-energy/high-energy split, the **whole
|
||||||
|
comparison must be read as an energy-conditioned contrast**, and the burden of proof
|
||||||
|
falls on the *time-resolved* tests, not on the raw miss distance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Headline: within-range-band, LOW vs HIGH power
|
||||||
|
|
||||||
|
`LOW = p ∈ [0.50, 0.75)`, `HIGH = p ∈ [1.00, 1.50)`; 95 % CI from a
|
||||||
|
**round-cluster bootstrap** (resampling battles, not shots — shots inside a battle
|
||||||
|
are correlated). Corpus `tfil_ab2`, 70 battles.
|
||||||
|
|
||||||
|
| metric | band | nLOW | nHIGH | LOW | HIGH | delta | 95 % CI |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **(a) miss at arrival px** | 300-450 | 5 272 | 10 696 | 104.67 | 110.55 | **+5.88** | [+2.64, +9.07] |
|
||||||
|
| **(a) miss at arrival px** | 450+ | 12 960 | 20 126 | 124.01 | 132.26 | **+8.25** | [+5.39, +11.25] |
|
||||||
|
| (b) lateral disp. / flight tick | 450+ | 12 960 | 20 126 | 3.47 | 3.42 | −0.04 | [−0.12, +0.03] |
|
||||||
|
| (b′) lateral disp., FIXED 12 ticks | 450+ | 12 960 | 20 126 | 55.63 | 55.47 | −0.15 | [−1.15, +0.84] |
|
||||||
|
| (b′′) CONTROL, same shot, +40 ticks | 450+ | 12 828 | 20 120 | 52.03 | 52.64 | +0.61 | [−0.38, +1.58] |
|
||||||
|
| (f) flight window ticks | 450+ | 12 960 | 20 126 | 28.38 | 30.52 | **+2.13** | [+1.81, +2.44] |
|
||||||
|
| (c) response latency ticks | 450+ | 8 682 | 13 884 | 13.60 | 14.27 | +0.68 | [+0.36, +0.99] |
|
||||||
|
| (d) turn rate deg/tick (flight) | 450+ | 12 960 | 20 126 | 1.49 | 1.46 | −0.03 | [−0.09, +0.02] |
|
||||||
|
| (d′) turn rate deg/tick (fixed 12) | 450+ | 12 960 | 20 126 | 1.43 | 1.28 | −0.15 | [−0.20, −0.10] |
|
||||||
|
| (d′′) turn rate, CONTROL (+40) | 450+ | 12 902 | 20 126 | 1.52 | 1.58 | +0.06 | [+0.01, +0.12] |
|
||||||
|
| (d) speed px/tick (flight) | 450+ | 12 960 | 20 126 | 5.46 | 5.48 | +0.01 | [−0.06, +0.08] |
|
||||||
|
| OUTCOME hit rate | 450+ | 12 960 | 20 126 | 0.10 | 0.09 | −0.01 | [−0.02, −0.00] |
|
||||||
|
|
||||||
|
`powtest`, 35 battles (replication, same bins):
|
||||||
|
|
||||||
|
| metric | band | nLOW | nHIGH | LOW | HIGH | delta | 95 % CI |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| (a) miss at arrival px | 300-450 | 1 388 | 8 151 | 106.22 | 109.93 | +3.72 | [−1.50, +8.65] |
|
||||||
|
| (a) miss at arrival px | 450+ | 2 249 | 11 085 | 122.00 | 131.86 | **+9.86** | [+4.56, +15.25] |
|
||||||
|
| (b) lateral disp. / flight tick | 450+ | 2 249 | 11 085 | 3.53 | 3.48 | −0.05 | [−0.20, +0.11] |
|
||||||
|
| (b′) lateral disp., FIXED 12 ticks | 450+ | 2 249 | 11 085 | 55.53 | 55.85 | +0.31 | [−1.24, +1.87] |
|
||||||
|
| (f) flight window ticks | 450+ | 2 249 | 11 085 | 27.75 | 29.93 | +2.18 | [+1.63, +2.73] |
|
||||||
|
| OUTCOME hit rate | 450+ | 2 249 | 11 085 | 0.10 | 0.09 | −0.01 | [−0.02, +0.01] |
|
||||||
|
|
||||||
|
**Reading.** The only metric with a non-trivial difference is the raw miss distance,
|
||||||
|
and it is *larger* for **high** power — i.e. the opposite of the hypothesis ("low
|
||||||
|
power is dodged better"). Two things explain it entirely, and neither is a behavioural
|
||||||
|
response.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Why the miss-distance difference is not a dodge: two decisive tests
|
||||||
|
|
||||||
|
### 4.1 The difference is the flight window, not the dodging
|
||||||
|
|
||||||
|
A low-power bullet is *faster*, so it arrives **2.13 ticks sooner** in band 450+
|
||||||
|
(28.38 vs 30.52). DrussGT drifts away from the aim line at roughly **1.7–1.8 px per
|
||||||
|
tick**; 2.13 ticks × ~3.9 px/tick of accumulated miss ≈ **+8 px** — exactly the
|
||||||
|
observed +8.25 px. Normalise by the window and it disappears:
|
||||||
|
|
||||||
|
| metric | band | LOW | HIGH | delta | 95 % CI |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| miss / flight ticks | 300-450 | 4.85 | 4.88 | +0.03 | [−0.13, +0.17] |
|
||||||
|
| miss / flight ticks | 450+ | 4.52 | 4.51 | −0.01 | [−0.12, +0.10] |
|
||||||
|
|
||||||
|
(per-tick miss rate, px/tick). The per-tick dodge rate is **identical**. Same in
|
||||||
|
`powtest`: 4.56 vs 4.59, delta +0.03 [−0.19, +0.25].
|
||||||
|
|
||||||
|
### 4.2 The difference is already present before the bullet can matter, and survives the bullet
|
||||||
|
|
||||||
|
The arrival tick depends on the power, so it is the wrong place to compare. Instead,
|
||||||
|
measure `|perp|` — DrussGT's perpendicular offset from our aim line — at a **fixed
|
||||||
|
number of ticks after the fire**, and again in a **CONTROL window 40 ticks later, when
|
||||||
|
the bullet is long gone**. A response to *our shot* must be ~0 at k=5 and grow with k;
|
||||||
|
a phase/geometry difference is present already at k=5 and identical in the control.
|
||||||
|
|
||||||
|
`tfil_ab2`, band 450+ (nLOW = 12 960, nHIGH = 20 126):
|
||||||
|
|
||||||
|
| k (ticks after fire) | LOW | HIGH | delta | CONTROL (+40 ticks) LOW | HIGH | delta |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 5 | 94.9 | 99.1 | **+4.21** | 144.2 | 149.7 | **+5.57** |
|
||||||
|
| 10 | 95.0 | 98.9 | +3.86 | 147.8 | 153.1 | +5.23 |
|
||||||
|
| 15 | 100.7 | 104.3 | +3.53 | — | — | — |
|
||||||
|
| 20 | 109.0 | 112.5 | +3.50 | — | — | — |
|
||||||
|
| 25 | 118.7 | 122.6 | +3.83 | — | — | — |
|
||||||
|
| 30 | 127.7 | 132.5 | +4.74 | — | — | — |
|
||||||
|
|
||||||
|
`powtest`, band 450+ (nLOW = 2 249, nHIGH = 11 085): k=5 delta **+5.97**, control
|
||||||
|
**+6.59**; identical story.
|
||||||
|
|
||||||
|
The whole difference is present **5 ticks after the trigger pull** (before the miss
|
||||||
|
can be a reaction to a bullet still in flight) and is *at least as large in the window
|
||||||
|
where the bullet has already gone*. It is a standing offset between the two
|
||||||
|
situations, not a dodge.
|
||||||
|
|
||||||
|
Also note the **sign flip**: in band 300-450 the turn rate is +0.03 deg/tick in the
|
||||||
|
flight window and +0.13 deg/tick in the bullet-free control window — the control
|
||||||
|
window shows *more* difference than the flight window. In band 450+ the flight-window
|
||||||
|
turn rate leans one way (−0.15) and the control window the other (+0.06). A real
|
||||||
|
response would behave the opposite way in both.
|
||||||
|
|
||||||
|
### 4.3 What the miss-distance metric actually contains (MEASURED)
|
||||||
|
|
||||||
|
`|perp|` is already 80–95 px only 5 ticks after the fire. **The miss distance is
|
||||||
|
dominated by our own gun's lead/aim error, not by DrussGT's dodge.** It is therefore a
|
||||||
|
weak instrument for "dodge quality" on its own, which is why the flight-normalised and
|
||||||
|
fixed-window measures carry the conclusion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Shuffled-label null (MEASURED)
|
||||||
|
|
||||||
|
Labels are permuted **inside each range band**; for the arrival-dependent metrics the
|
||||||
|
arrival tick is **re-derived** for the permuted power, so the null keeps the kinematic
|
||||||
|
channel and destroys only the response. 400 reps. Corpus `tfil_ab2`, per band, two-sided
|
||||||
|
permutation p on `delta = mean(HIGH) − mean(LOW)`:
|
||||||
|
|
||||||
|
| metric | 100-200 | 200-300 | 300-450 | 450+ |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| miss | d=+5.35, p=0.89 | d=+0.35, p=0.39 | d=+5.91, p=0.27 | d=+8.24, p=0.005 |
|
||||||
|
| miss / flight | d=−0.35, p=0.62 | d=−0.34, p=0.52 | d=+0.03, p=0.01 | d=−0.01, p=0.005 |
|
||||||
|
| lateral / flight | — | — | d=−0.08, p≈0.1 | d=−0.04, p≈0.1 |
|
||||||
|
| lateral, fixed 12 | d=+16.2, p=0.02 | d=+2.89, p=0.27 | d=−1.45, p=0.005 | d=−0.15, p=0.66 |
|
||||||
|
| lateral, CONTROL (+40) | d=+3.67, p=0.68 | d=+5.53, p=0.10 | d=−0.85, p=0.10 | d=+0.61, p=0.08 |
|
||||||
|
| turn rate, fixed 12 | d=+0.57, p=0.15 | d=+0.32, p=0.04 | d=+0.03, p=0.13 | d=−0.15, p=0.005 |
|
||||||
|
| turn rate, CONTROL | d=+0.84, p=0.06 | d=+0.41, p=0.005 | d=+0.14, p=0.005 | d=+0.06, p=0.005 |
|
||||||
|
| speed, fixed 12 | d=+0.09, p=0.91 | d=+0.02, p=0.85 | d=−0.05, p=0.14 | d=−0.01, p=0.76 |
|
||||||
|
| hit rate | d=+0.09, p=0.57 | d=−0.02, p=0.67 | d=+0.004, p=0.43 | d=−0.013, p=0.005 |
|
||||||
|
|
||||||
|
The null is **centred on the kinematic counterfactual**, so a *small* p means
|
||||||
|
"the observed difference is not fully explained by flight-time kinematics". Only the
|
||||||
|
raw miss in 450+ (and the fixed-12 turn rate) reach that, and in **both** cases the
|
||||||
|
sign is the *opposite* of "low power is dodged better": at matched conditions DrussGT
|
||||||
|
is marginally **further** from the line and turns **less** on the high-power (slower)
|
||||||
|
bullets, and the difference is present in the bullet-free control window as well.
|
||||||
|
|
||||||
|
*Effective sample size.* The two bands that carry the conclusion are band 450+
|
||||||
|
(12 960 LOW / 20 126 HIGH, 70 battles) and band 300-450 (5 272 / 10 696, 70 battles),
|
||||||
|
replicated in a second corpus (2 249 / 11 085, 35 battles). The bootstrap is clustered
|
||||||
|
by battle, so the CIs already carry the between-battle variance. Bands **0-100** and
|
||||||
|
**100-200** have n = 6/2 and 53/23 shots — far too few for any claim, and they are
|
||||||
|
labelled "too few for a contrast" in the report. The design has ample power to detect a
|
||||||
|
modest effect (the CIs on the fixed-window lateral displacement, a ~55 px quantity,
|
||||||
|
are ±1 px); it cannot rule out an effect smaller than ~2 % on any of these measures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The response latency, turn rate and speed (MEASURED)
|
||||||
|
|
||||||
|
* **(c) Response latency** (ticks from our fire until DrussGT's heading has turned
|
||||||
|
>15°): band 450+ **13.60 → 14.27 ticks**, delta +0.68 [+0.36, +0.99]. But the
|
||||||
|
measurement window is capped by the flight, which is itself 2.13 ticks longer for
|
||||||
|
HIGH power. As a **fraction of the flight window** it is 0.479 (LOW) vs 0.468
|
||||||
|
(HIGH) — the high-power shots are responded to *sooner relative to the bullet*.
|
||||||
|
In band 300-450 it is **−0.09** [−0.47, +0.30]. Directionally inconsistent ⇒ no
|
||||||
|
effect. `powtest`: +0.47 (450+), −0.29 (300-450).
|
||||||
|
* **(d) Turn rate** in the flight window: 1.49 vs 1.46 deg/tick in 450+ (−0.03
|
||||||
|
[−0.09, +0.02]); +0.13 [+0.05, +0.21] in 300-450 with the same sign in the control
|
||||||
|
window (+0.13). Surfers do slow to turn, but DrussGT's turn rate does not track our
|
||||||
|
power.
|
||||||
|
* **Speed** in the flight window: 5.46 vs 5.48 px/tick — flat, and the control window
|
||||||
|
agrees. DrussGT is not slowing down more against low-power bullets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Flight-window lengths (MEASURED) — the thing that would have to be true
|
||||||
|
|
||||||
|
`speed = 20 − 3p`, so a **lower** power is a **faster** bullet and a **shorter**
|
||||||
|
flight. Measured, band 450+:
|
||||||
|
|
||||||
|
| power | 0.50–0.75 | 0.75–1.00 | 1.00–1.50 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| flight ticks | 28.38 | 30.18 | 30.52 |
|
||||||
|
|
||||||
|
A genuine "dodge low power better" would have to overcome a **2.1-tick handicap** and
|
||||||
|
still produce a *larger* miss per unit time at low power. It does not: the per-tick
|
||||||
|
miss rate is 4.52 at LOW and 4.51 at HIGH. If anything the extra reaction time on the
|
||||||
|
slow, high-power bullets produces slightly *more* lateral drift (1.8 px/tick vs
|
||||||
|
1.7 px/tick between k=10 and k=30 in band 450+).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Outcome (MEASURED) — reproduces the known flat result
|
||||||
|
|
||||||
|
Hit rate from the real server events, per band:
|
||||||
|
|
||||||
|
| band | 0.50–0.75 | 1.00–1.50 | delta | 95 % CI |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 300-450 | 0.11 | 0.12 | +0.004 | [−0.01, +0.02] |
|
||||||
|
| 450+ | 0.10 | 0.09 | −0.013 | [−0.02, −0.00] |
|
||||||
|
|
||||||
|
Flat, in agreement with the previously measured live outcome (9.55 % / 10.86 % /
|
||||||
|
10.35 % by fired power, Fisher p = 0.37). This job adds the *mechanism*: the outcome is
|
||||||
|
flat because the movement is flat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Verdict
|
||||||
|
|
||||||
|
**MEASURED**
|
||||||
|
|
||||||
|
* DrussGT's miss-normalised, fixed-window and control-window movement metrics are
|
||||||
|
**flat across the power we fire, inside a range band**, on 54 939 shots and
|
||||||
|
replicated on 24 280 more.
|
||||||
|
* The only between-power difference that is large is the **raw miss distance at
|
||||||
|
arrival** (+8.2 px in band 450+ for a 2.13-tick longer flight), and that is
|
||||||
|
**exactly the flight-window kinematics**; normalising by the window removes it.
|
||||||
|
* The difference is present **5 ticks after the fire** and is **as large in the window
|
||||||
|
40 ticks later when the bullet is gone** — so it is a property of the low-energy /
|
||||||
|
high-energy *situation*, not of the shot.
|
||||||
|
* Hit rate is flat.
|
||||||
|
|
||||||
|
**INFERRED**
|
||||||
|
|
||||||
|
* The null results rule out a *dodge-quality* response to power of the size the visual
|
||||||
|
impression suggests. They cannot exclude an effect that is exactly cancelled by the
|
||||||
|
same energy confound in every metric — but that would require a coincidence, and the
|
||||||
|
control window argues against it.
|
||||||
|
* The user's visual impression ("more bullets fly at low power") is real but is a
|
||||||
|
*bullet-count* effect, not a dodge effect: firing at low power gives a shorter
|
||||||
|
reload interval (`10 + 2p` ticks) and a faster bullet, so more bullets are in the air
|
||||||
|
and more of them are seen. That increased visual *rate of activity* is not DrussGT
|
||||||
|
dodging differently.
|
||||||
|
* There is **no hidden DrussGT behaviour** to exploit at low power. If anything, the
|
||||||
|
(tiny, situational) residual points the other way: DrussGT is marginally *further*
|
||||||
|
off the aim line and turns *less* against the slower high-power bullets, which is
|
||||||
|
just the extra reaction time.
|
||||||
|
* Practical reading for the energy-slope policy: the low-power arm loses nothing in
|
||||||
|
terms of DrussGT's evasion; its hit rate is the same and its energy cost is lower.
|
||||||
|
The flat outcome measured earlier is not hiding a movement-side penalty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Reproducing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# corpora (live captures; not in the repo - ~490 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_drussgt_dodge_vs_power.py \
|
||||||
|
--tfil /tmp/tfil_ab2/out --powtest /tmp/powtest --reps 400 \
|
||||||
|
--json common_libs/tests/fixtures/dodge_vs_power_results.json \
|
||||||
|
| tee common_libs/tests/fixtures/dodge_vs_power_report.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`common_libs/tests/fixtures/dodge_vs_power_report.txt` is the verbatim captured
|
||||||
|
output the tables above were taken from;
|
||||||
|
`common_libs/tests/fixtures/dodge_vs_power_results.json` is the same numbers as JSON.
|
||||||
|
Runtime ≈ 2.5 minutes 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` (the analyzer also needs the `.rounds.json` sidecar,
|
||||||
|
which the shim writes automatically).
|
||||||
Reference in New Issue
Block a user