4f18c8ce07
There are no genuinely competitive adversaries for Tank Royale, and the in-repo ones were broken until recently. Classic Robocode 1.9.5.5 is obtainable (SourceForge, 20.4 MB) and its programmatic control API (robocode.control.RobocodeEngine + BattleAdaptor.onTurnEnded) can run real battles headless and expose per-turn robot state. So a legacy leader bot's MOVEMENT can be captured and used as a gun-testing fixture with no port. Captured unmodified DrussGT 3.1.4159 vs spinbot/ramfire/crazy/corners and a mirror match: 28,797 ticks, plus two trivial-bot contrasts. Conversion to the Tank Royale convention is validated to 0.000-0.001 deg by recomputing the direction implied by (heading, speed) and comparing it against the recorded per-tick displacement -- i.e. the data is proven to be genuine recorded motion rather than a mangled export. (A first attempt treated the snapshot API's headings as degrees; they are radians, ~95 deg off.) The statistics confirm it is really a wave surfer: perpendicular to the opponent 65-96% of ticks, radial ~0.001, 41-72% of ticks at full speed, reversing on 42-46% of ticks, holding range at a 283-526 px median. The straight-line contrast is radial-dominant (0.75) with ZERO reversals. Discovery: DrussGT detects predictable guns and switches to a bullet-shield stand-still mode, so captures against sample.Walls/TrackFire had to be rejected as non-movement. CAVEATS, recorded in DRUSSGT_FIXTURES.md: these are open-loop (replayed DrussGT never dodges OUR bullets) and perfect-information (the observer gives true positions every tick, unlike our stale live WorldState). Both make our guns look better than in live play, so use them for RELATIVE gun ranking, not absolute hit rates. Jars stay out of git; capture tooling is reproducible via capture.sh.
153 lines
6.7 KiB
Python
153 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Analyze classic-Robocode fixture JSONL + validate the heading conversion.
|
|
|
|
Conversion claimed: tank_heading = (90 - classic_heading) mod 360
|
|
Since classic Robocode x/y axes match Tank Royale (origin bottom-left, x right,
|
|
y up), position deltas between ticks should align with the reported heading.
|
|
"""
|
|
import json, sys, math, statistics as st
|
|
|
|
def load(path):
|
|
rows = []
|
|
for line in open(path):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
o = json.loads(line)
|
|
if "tick" in o:
|
|
rows.append(o)
|
|
rounds = []
|
|
import os
|
|
for cand in (path + ".rounds.json",
|
|
os.path.join(os.path.dirname(path), "drussgt_meta",
|
|
os.path.basename(path) + ".rounds.json")):
|
|
try:
|
|
rounds = json.load(open(cand))["rounds"]
|
|
break
|
|
except Exception:
|
|
rounds = None
|
|
return rows, rounds
|
|
|
|
def wrap180(a):
|
|
return ((a + 180.0) % 360.0) - 180.0
|
|
|
|
def unit(deg):
|
|
r = math.radians(deg)
|
|
return math.cos(r), math.sin(r)
|
|
|
|
def analyze(path):
|
|
rows, rounds = load(path)
|
|
# boundary ticks (first tick of a round)
|
|
starts = set()
|
|
if rounds:
|
|
for r in rounds:
|
|
starts.add(r["startTick"])
|
|
else:
|
|
# fallback heuristic: big jump
|
|
for i in range(1, len(rows)):
|
|
dx = rows[i]["ex"] - rows[i-1]["ex"]
|
|
dy = rows[i]["ey"] - rows[i-1]["ey"]
|
|
if math.hypot(dx, dy) > 20:
|
|
starts.add(rows[i]["tick"])
|
|
|
|
n = len(rows)
|
|
speeds = [abs(r["es"]) for r in rows]
|
|
signed = [r["es"] for r in rows]
|
|
heading_changes = []
|
|
dists = []
|
|
perp_sin = []
|
|
radial_cos = []
|
|
disp_err = [] # angular error between displacement dir and reported movement dir
|
|
disp_err_next = []
|
|
moving_ticks = 0
|
|
|
|
for i, r in enumerate(rows):
|
|
d = math.hypot(r["ex"] - r["sx"], r["ey"] - r["sy"])
|
|
dists.append(d)
|
|
if i == 0 or rows[i]["tick"] in starts:
|
|
continue
|
|
p = rows[i-1]
|
|
dh = wrap180(r["eh"] - p["eh"])
|
|
heading_changes.append(abs(dh))
|
|
dx = r["ex"] - p["ex"]
|
|
dy = r["ey"] - p["ey"]
|
|
step = math.hypot(dx, dy)
|
|
v = r["es"]
|
|
if abs(v) < 0.5 or step < 0.2:
|
|
continue
|
|
moving_ticks += 1
|
|
# movement direction implied by heading + velocity sign (tank convention)
|
|
mu = r["eh"] if v >= 0 else r["eh"] + 180.0
|
|
# radial direction from enemy to shooter
|
|
theta = math.degrees(math.atan2(r["sy"] - r["ey"], r["sx"] - r["ex"]))
|
|
delta = math.radians(mu - theta)
|
|
perp_sin.append(abs(math.sin(delta)))
|
|
radial_cos.append(abs(math.cos(delta)))
|
|
# conversion validation: displacement direction vs movement dir
|
|
phi = math.degrees(math.atan2(dy, dx))
|
|
disp_err.append(abs(wrap180(phi - mu)))
|
|
phi_prev = math.degrees(math.atan2(dy, dx))
|
|
mu_prev = p["eh"] if p["es"] >= 0 else p["eh"] + 180.0
|
|
disp_err_next.append(abs(wrap180(phi_prev - mu_prev)))
|
|
|
|
def pct(x, p):
|
|
if not x:
|
|
return float("nan")
|
|
s = sorted(x)
|
|
k = min(len(s)-1, int(round((p/100.0)*(len(s)-1))))
|
|
return s[k]
|
|
|
|
def hist(x, edges):
|
|
tot = len(x)
|
|
out = []
|
|
for lo, hi in zip(edges[:-1], edges[1:]):
|
|
c = sum(1 for v in x if lo <= v < hi)
|
|
out.append((f"[{lo},{hi})", c/tot if tot else 0.0))
|
|
return out
|
|
|
|
res = {
|
|
"path": path, "n": n,
|
|
"speed_hist": hist(speeds, [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 7.5, 8.01]),
|
|
"speed_mean": st.mean(speeds), "speed_median": st.median(speeds),
|
|
"speed_std": (st.pstdev(speeds) if n > 1 else 0.0),
|
|
"speed_p90": pct(speeds, 90),
|
|
"frac_rest": sum(1 for v in speeds if v < 0.5)/n,
|
|
"frac_full": sum(1 for v in speeds if v >= 7.5)/n,
|
|
"frac_neg": sum(1 for v in signed if v < -0.5)/n,
|
|
"hc_mean": st.mean(heading_changes) if heading_changes else float("nan"),
|
|
"hc_median": st.median(heading_changes) if heading_changes else float("nan"),
|
|
"hc_p90": pct(heading_changes, 90), "hc_p99": pct(heading_changes, 99),
|
|
"hc_max": max(heading_changes) if heading_changes else float("nan"),
|
|
"frac_hc_lt1": (sum(1 for v in heading_changes if v < 1.0)/len(heading_changes)) if heading_changes else float("nan"),
|
|
"frac_hc_gt5": (sum(1 for v in heading_changes if v > 5.0)/len(heading_changes)) if heading_changes else float("nan"),
|
|
"hc_hist": hist(heading_changes, [0, 0.5, 1, 2, 3, 5, 7, 10, 11, 180.1]),
|
|
"dist_mean": st.mean(dists), "dist_median": st.median(dists),
|
|
"dist_p10": pct(dists, 10), "dist_p90": pct(dists, 90),
|
|
"moving_ticks": moving_ticks,
|
|
"perp_frac": (sum(1 for v in perp_sin if v > 0.866)/len(perp_sin)) if perp_sin else float("nan"),
|
|
"radial_frac": (sum(1 for v in radial_cos if v > 0.866)/len(radial_cos)) if radial_cos else float("nan"),
|
|
"mean_abs_sin": st.mean(perp_sin) if perp_sin else float("nan"),
|
|
"mean_abs_cos": st.mean(radial_cos) if radial_cos else float("nan"),
|
|
"conv_err_same_tick_mean": st.mean(disp_err) if disp_err else float("nan"),
|
|
"conv_err_prev_tick_mean": st.mean(disp_err_next) if disp_err_next else float("nan"),
|
|
"conv_err_same_tick_median": st.median(disp_err) if disp_err else float("nan"),
|
|
}
|
|
return res
|
|
|
|
if __name__ == "__main__":
|
|
for path in sys.argv[1:]:
|
|
r = analyze(path)
|
|
print("="*100)
|
|
print(r["path"], "ticks=", r["n"], "movingTicks=", r["moving_ticks"])
|
|
print(f" conversion check: mean |angle(displacement) - movementdir| = {r['conv_err_same_tick_mean']:.3f} deg "
|
|
f"(median {r['conv_err_same_tick_median']:.3f}); using prev-tick heading {r['conv_err_prev_tick_mean']:.3f}")
|
|
print(f" SPEED: mean={r['speed_mean']:.3f} median={r['speed_median']:.3f} std={r['speed_std']:.3f} p90={r['speed_p90']:.2f} "
|
|
f"rest(<0.5)={r['frac_rest']:.3f} full(>=7.5)={r['frac_full']:.3f} neg={r['frac_neg']:.3f}")
|
|
print(" speed hist:", " ".join(f"{k}:{v:.3f}" for k,v in r["speed_hist"]))
|
|
print(f" HEADING CHANGE: mean={r['hc_mean']:.3f} median={r['hc_median']:.3f} p90={r['hc_p90']:.2f} p99={r['hc_p99']:.2f} max={r['hc_max']:.2f} "
|
|
f"|d|<1={r['frac_hc_lt1']:.3f} |d|>5={r['frac_hc_gt5']:.3f}")
|
|
print(" heading-change hist:", " ".join(f"{k}:{v:.3f}" for k,v in r["hc_hist"]))
|
|
print(f" DISTANCE: mean={r['dist_mean']:.1f} median={r['dist_median']:.1f} p10={r['dist_p10']:.1f} p90={r['dist_p90']:.1f}")
|
|
print(f" LATERAL/RADIAL: perpFrac={r['perp_frac']:.3f} radialFrac={r['radial_frac']:.3f} "
|
|
f"mean|sin|={r['mean_abs_sin']:.3f} mean|cos|={r['mean_abs_cos']:.3f}")
|