#!/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}")