feat(fixtures): closed-loop DrussGT captures from real Tank Royale battles
The classic captures were OPEN-LOOP: replayed DrussGT never dodged OUR
bullets. These come from real TR battles through the working Java bridge, so
the recording contains genuine reactions to ModularBot's live fire. The
open-loop caveat is gone (perfect-information remains).
PRIMARY RESULT - the boss beats us badly. DrussGT 1447 - ModularBot 300 over
15 rounds, ModularBot winning only round 5 (DrussGT died at tick 1893). Rounds
are long, not truncated: mean 1335 ticks, ModularBot got off 1134 shots.
ModularBot 1134 shots / 60 hits = 5.3% real hit rate
DrussGT 1400 shots / 169 hits = 12.1% real hit rate
So DrussGT's gun is ~2.3x more accurate than our entire rack, on top of far
better movement. That is the number to move.
Also captured: shield-on variant (DrussGT 939-287, 9/10 - ModularBot takes
round 1 to the known shield warm-up), and vs SpinBot 1175-0, Crazy 1080-1,
Corners 1659-0. 20,026 + 12,629 + 10,824 + 11,507 + 2,575 ticks.
Movement statistics match the classic set within ~0.04 on the perpendicular
and radial fractions, so this is the same wave surfer in TR physics:
TR vs modularbot: perp 0.967, radial 0.001, 52.8% at full speed,
reversing 46.3%, median range 464 px.
CLOSED LOOP PROVEN, not asserted. ModularBot's fire is a heat-limited near
metronome (median interval 14 ticks), which gives a usable exogenous clock:
- event-locked |delta heading| oscillates 0.96 -> 2.69 deg about a 1.47 deg
mean with the fire period, almost every lag outside the 95% band of a
400-iteration phase-shuffled null;
- cross-correlation of |delta heading| against the fire impulse peaks at
r = +0.111, lag 12 ticks, permutation p = 0.005 (null peak mean +0.016);
- OWN-FIRE CONTROL is flat, so the oscillation is enemy-driven rather than
an internal cadence;
- range response is weak (~3 px over 30 ticks, near noise) and is therefore
NOT claimed, and per-bullet dodging is not claimed either because the
bullet detector (shield) is off.
CAVEATS: still perfect-information (observer gives true positions every tick,
unlike the live bot's stale between-scan WorldState) so these remain optimistic
vs live play; and they are open-loop AT REPLAY TIME - 'closed_loop' describes
the capture, not a later replay. TR conversion residual is ~1.5 deg mean
because the TR server moves along the pre-turn heading, vs 0.000 deg for the
classic captures.
Adds analyze_closed_loop.py (PSTH event-locking, phase-shuffle permutation
null, cross-correlation, own-fire control) and per-round result sidecars.
This commit is contained in:
@@ -444,7 +444,13 @@ identity file and a `<DIR>.sh` launcher that runs `robocode_shim.DrussGTBridge`)
|
||||
`TrBattleCapture` is the observer: it runs the battle through the Tank Royale
|
||||
battle-runner API and dumps every bot's per-turn state in the *same* JSONL
|
||||
fixture format as `tools/robocode_fixture_capture/Capture.java`, so
|
||||
`analyze.py` runs on both unchanged.
|
||||
`analyze.py` runs on both unchanged. It writes `"source":"tr-bridge"`,
|
||||
`"closed_loop":true` in the meta, a `.rounds.json` sidecar and a
|
||||
`.results.json` per-round score sidecar. With `--events-out FILE` it also dumps
|
||||
bullet fire / hit / death events, which
|
||||
`tools/robocode_shim/analyze_closed_loop.py` uses to measure how strongly
|
||||
DrussGT's movement is phase-locked to the adversary's live fire (see
|
||||
`tools/fixtures/TR_BRIDGE_FIXTURES.md`).
|
||||
|
||||
### 5.8 Verification — it is really DrussGT, not a stub
|
||||
|
||||
@@ -543,6 +549,8 @@ tools/robocode_shim/
|
||||
DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
|
||||
SmokeTest.java the standalone runtime proof
|
||||
TrBattleCapture.java observer: battle -> classic JSONL fixture format
|
||||
analyze_closed_loop.py PSTH / cross-correlation loop-reactivity analysis
|
||||
evidence/ raw bullet-event sidecars for that analysis
|
||||
out/ build output (gitignored)
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Closed-loop reactivity analysis for the Tank-Royale bridge fixtures.
|
||||
|
||||
Given a fixture JSONL (per-tick DrussGT + adversary state, produced by
|
||||
``TrBattleCapture``) and the matching raw-events JSONL (bullet fire / hit /
|
||||
death events), this measures whether **DrussGT's movement responds to the
|
||||
adversary's live bullets** at capture time -- the property the classic-replay
|
||||
fixtures cannot have.
|
||||
|
||||
Why the raw events: the fixture alone cannot distinguish an enemy *fire* from
|
||||
enemy *damage* (both are energy drops), and it has no bullet positions. The
|
||||
events sidecar gives the exact fire tick, muzzle position, direction and power
|
||||
of every shot, so we can event-lock DrussGT's response and compute predicted
|
||||
bullet-arrival times.
|
||||
|
||||
Outputs
|
||||
-------
|
||||
* fire cadence, hits / misses / bullet-bullet, per-round death victim (who won
|
||||
the round) and DrussGT hits taken;
|
||||
* event-locked PSTH (peri-stimulus time histogram) of DrussGT's response
|
||||
(``|dHeading|``, reversal rate, radial-velocity fraction) as a function of
|
||||
ticks since an adversary fire, with a permutation null band (fire ticks
|
||||
circularly shifted inside each round);
|
||||
* cross-correlation of the fire impulse train with the response, peak lag.
|
||||
|
||||
Note: fixture roles are fixed by ``TrBattleCapture``: e* = DrussGT (subject),
|
||||
s* = adversary. The muzzle-position match is used only to *validate* that owner
|
||||
id mapping independently.
|
||||
|
||||
Usage:
|
||||
python3 tools/robocode_shim/analyze_closed_loop.py FIXTURE.jsonl EVENTS.jsonl
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import statistics as st
|
||||
import sys
|
||||
|
||||
|
||||
def wrap180(a):
|
||||
return ((a + 180.0) % 360.0) - 180.0
|
||||
|
||||
|
||||
def load_fixture(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 = []
|
||||
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 = []
|
||||
return rows, rounds
|
||||
|
||||
|
||||
def load_events(path):
|
||||
return [json.loads(l) for l in open(path) if l.strip()]
|
||||
|
||||
|
||||
def build_rounds(rows, rounds):
|
||||
if not rounds:
|
||||
out, cur = [], [rows[0]]
|
||||
for r in rows[1:]:
|
||||
if math.hypot(r["ex"] - cur[-1]["ex"], r["ey"] - cur[-1]["ey"]) > 20:
|
||||
out.append(cur)
|
||||
cur = []
|
||||
cur.append(r)
|
||||
out.append(cur)
|
||||
return [{"rows": c, "start": c[0]["tick"], "round": i + 1}
|
||||
for i, c in enumerate(out)]
|
||||
by_tick = {r["tick"]: r for r in rows}
|
||||
out = []
|
||||
for rn in rounds:
|
||||
s = rn["startTick"]
|
||||
out.append({"round": rn["round"], "start": s,
|
||||
"rows": [by_tick[t] for t in range(s, s + rn["count"]) if t in by_tick]})
|
||||
return out
|
||||
|
||||
|
||||
def response_series(rlist):
|
||||
res = []
|
||||
for rd in rlist:
|
||||
R = rd["rows"]
|
||||
n = len(R)
|
||||
hc = [float("nan")] * n
|
||||
rev = [float("nan")] * n
|
||||
rad = [float("nan")] * n
|
||||
for i in range(1, n):
|
||||
hc[i] = abs(wrap180(R[i]["eh"] - R[i - 1]["eh"]))
|
||||
v0, v1 = R[i - 1]["es"], R[i]["es"]
|
||||
rev[i] = 1.0 if (abs(v0) > 0.5 and v0 * v1 < 0) else 0.0
|
||||
theta = math.degrees(math.atan2(R[i]["sy"] - R[i]["ey"],
|
||||
R[i]["sx"] - R[i]["ex"]))
|
||||
mu = R[i]["eh"] if v1 >= 0 else R[i]["eh"] + 180.0
|
||||
rad[i] = abs(math.cos(math.radians(mu - theta)))
|
||||
res.append({"round": rd["round"], "start": rd["start"], "n": n,
|
||||
"hc": hc, "rev": rev, "rad": rad})
|
||||
return res
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
fixture, events_path = sys.argv[1], sys.argv[2]
|
||||
rows, rounds = load_fixture(fixture)
|
||||
events = load_events(events_path)
|
||||
rlist = build_rounds(rows, rounds)
|
||||
arrays = response_series(rlist)
|
||||
n_by_round = {a["round"]: a["n"] for a in arrays}
|
||||
|
||||
# ---- validate owner -> role mapping from muzzle positions ----
|
||||
idx = {r["tick"]: i for i, r in enumerate(rows)}
|
||||
start_of = {rn["round"]: rn["startTick"] for rn in rounds}
|
||||
role_by_owner = {}
|
||||
for e in events:
|
||||
if e["type"] != "fire" or "x" not in e:
|
||||
continue
|
||||
start = start_of.get(e["round"])
|
||||
if start is None:
|
||||
continue
|
||||
gi = start + (e["tick"] - 1)
|
||||
if gi not in idx:
|
||||
continue
|
||||
r = rows[idx[gi]]
|
||||
de = math.hypot(r["ex"] - e["x"], r["ey"] - e["y"])
|
||||
ds = math.hypot(r["sx"] - e["x"], r["sy"] - e["y"])
|
||||
role_by_owner.setdefault(e["owner"], []).append("e" if de <= ds else "s")
|
||||
owner_role = {o: max(set(v), key=v.count) for o, v in role_by_owner.items()}
|
||||
|
||||
subj_owner = next((o for o, ro in owner_role.items() if ro == "e"), None)
|
||||
adv_owner = next((o for o, ro in owner_role.items() if ro == "s"), None)
|
||||
|
||||
print(f"fixture : {fixture}")
|
||||
print(f"events : {events_path}")
|
||||
print(f"ticks : {len(rows)} rounds: {len(rlist)}")
|
||||
for o, v in role_by_owner.items():
|
||||
print(f"owner {o}: muzzle-position role votes e*={v.count('e')} s*={v.count('s')} "
|
||||
f"-> {owner_role[o]}*")
|
||||
print(f"subject (DrussGT, e*) = owner {subj_owner}; adversary (s*) = owner {adv_owner}")
|
||||
|
||||
fires_adv = [e for e in events if e["type"] == "fire" and e["owner"] == adv_owner]
|
||||
fires_subj = [e for e in events if e["type"] == "fire" and e["owner"] == subj_owner]
|
||||
hits_adv = [e for e in events if e["type"] == "hit" and e["owner"] == adv_owner]
|
||||
hits_subj = [e for e in events if e["type"] == "hit" and e["owner"] == subj_owner]
|
||||
bhb = [e for e in events if e["type"] == "hitbullet"]
|
||||
print(f"fires : adversary={len(fires_adv)} subject(DrussGT)={len(fires_subj)}")
|
||||
print(f"hits : adversary->DrussGT={len(hits_adv)} DrussGT->adversary={len(hits_subj)}"
|
||||
f" bullet-bullet={len(bhb)}")
|
||||
if fires_adv:
|
||||
print(f"adversary hit rate = {len(hits_adv)/len(fires_adv):.3f}")
|
||||
|
||||
fire_ticks_by_round = {rd["round"]: [] for rd in rlist}
|
||||
for e in fires_adv:
|
||||
fire_ticks_by_round[e["round"]].append(e["tick"])
|
||||
|
||||
print("\n=== per-round (ticks, adversary fires, DrussGT hits taken, round death) ===")
|
||||
per_round_hits = {}
|
||||
for e in hits_adv:
|
||||
per_round_hits[e["round"]] = per_round_hits.get(e["round"], 0) + 1
|
||||
death_by_round = {}
|
||||
for e in events:
|
||||
if e["type"] == "death":
|
||||
death_by_round.setdefault(e["round"], []).append(e["victim"])
|
||||
for rd in rlist:
|
||||
rn = rd["round"]
|
||||
dth = death_by_round.get(rn)
|
||||
if dth is None:
|
||||
winner = "no death (timeout?)"
|
||||
else:
|
||||
# victimId == adversary owner -> adversary died -> DrussGT won
|
||||
winner = (f"DrussGT (adversary victimId={dth[0]})" if dth[0] == adv_owner
|
||||
else f"adversary (DrussGT victimId={dth[0]})")
|
||||
print(f" round {rn:2d}: ticks={n_by_round[rn]:5d} advFires={len(fire_ticks_by_round[rn]):4d} "
|
||||
f"DrussGT hits taken={per_round_hits.get(rn,0):2d} winner={winner}")
|
||||
|
||||
deltas = []
|
||||
for rn, fts in fire_ticks_by_round.items():
|
||||
fts = sorted(fts)
|
||||
deltas += [b - a for a, b in zip(fts, fts[1:])]
|
||||
if deltas:
|
||||
s = sorted(deltas)
|
||||
print(f"\nadversary fire interval (ticks): median={st.median(deltas):.1f} "
|
||||
f"mean={st.mean(deltas):.1f} p10={s[len(s)//10]} p90={s[len(s)*9//10]} "
|
||||
f"(min={s[0]}, max={s[-1]})")
|
||||
|
||||
# ---- event-locked PSTH around adversary fire tick ----
|
||||
tau_lo, tau_hi = -5, 80
|
||||
fire_by_round = {rd["round"]: [rd["start"] + (t - 1)
|
||||
for t in sorted(fire_ticks_by_round[rd["round"]])]
|
||||
for rd in rlist}
|
||||
|
||||
def psth_for(key, fires_map):
|
||||
acc = {t: [] for t in range(tau_lo, tau_hi + 1)}
|
||||
for arr in arrays:
|
||||
lo, n = arr["start"], arr["n"]
|
||||
for f in fires_map[arr["round"]]:
|
||||
for tau in range(tau_lo, tau_hi + 1):
|
||||
gi = f + tau
|
||||
if lo <= gi < lo + n:
|
||||
v = arr[key][gi - lo]
|
||||
if v == v:
|
||||
acc[tau].append(v)
|
||||
return {t: (st.mean(acc[t]) if acc[t] else float("nan")) for t in acc}
|
||||
|
||||
random.seed(1234)
|
||||
for key, label in [("hc", "|dHeading| deg"), ("rev", "reversal rate"), ("rad", "|cos| radial frac")]:
|
||||
real = psth_for(key, fire_by_round)
|
||||
nulls = {t: [] for t in range(tau_lo, tau_hi + 1)}
|
||||
for _ in range(150):
|
||||
shifted = {}
|
||||
for arr in arrays:
|
||||
lo, n = arr["start"], arr["n"]
|
||||
base = fire_by_round[arr["round"]]
|
||||
k = random.randint(0, max(1, n - 1))
|
||||
shifted[arr["round"]] = [lo + ((x - lo + k) % n) for x in base]
|
||||
acc = psth_for(key, shifted)
|
||||
for t in nulls:
|
||||
if acc[t] == acc[t]:
|
||||
nulls[t].append(acc[t])
|
||||
print(f"\n=== event-locked PSTH: {label} vs ticks-since-adversary-fire ===")
|
||||
print(" tau : real null95-low null95-high flag")
|
||||
for t in range(tau_lo, tau_hi + 1):
|
||||
band = sorted(nulls[t])
|
||||
lo_b = band[int(0.025 * len(band))] if band else float("nan")
|
||||
hi_b = band[int(0.975 * len(band))] if band else float("nan")
|
||||
flag = ""
|
||||
if real[t] == real[t] and band and (real[t] < lo_b or real[t] > hi_b):
|
||||
flag = "OUTSIDE-NULL"
|
||||
if t <= 45:
|
||||
print(f" {t:4d}: {real[t]:8.4f} {lo_b:9.4f} {hi_b:9.4f} {flag}")
|
||||
|
||||
# ---- control: is DrussGT's turning also locked to its OWN fire cadence? ----
|
||||
own_fire_by_round = {rd["round"]: [] for rd in rlist}
|
||||
_start = {rd["round"]: rd["start"] for rd in rlist}
|
||||
for e in fires_subj:
|
||||
own_fire_by_round[e["round"]].append(_start[e["round"]] + (e["tick"] - 1))
|
||||
for rn in own_fire_by_round:
|
||||
own_fire_by_round[rn].sort()
|
||||
print("\n=== CONTROL PSTH (|dHeading|) locked to DrussGT's OWN fire times ===")
|
||||
print(" tau : real null95-low null95-high flag")
|
||||
real_own = psth_for("hc", own_fire_by_round)
|
||||
for t in range(0, 41):
|
||||
band = []
|
||||
for _ in range(80):
|
||||
shifted = {}
|
||||
for arr in arrays:
|
||||
lo, n = arr["start"], arr["n"]
|
||||
k = random.randint(0, max(1, n - 1))
|
||||
shifted[arr["round"]] = [lo + ((x - lo + k) % n) for x in own_fire_by_round[arr["round"]]]
|
||||
band.append(psth_for("hc", shifted)[t])
|
||||
band = sorted(v for v in band if v == v)
|
||||
lo_b = band[int(0.025 * len(band))] if band else float("nan")
|
||||
hi_b = band[int(0.975 * len(band))] if band else float("nan")
|
||||
flag = "OUTSIDE-NULL" if (real_own[t] == real_own[t] and band and
|
||||
(real_own[t] < lo_b or real_own[t] > hi_b)) else ""
|
||||
print(f" {t:4d}: {real_own[t]:8.4f} {lo_b:9.4f} {hi_b:9.4f} {flag}")
|
||||
|
||||
# ---- range-keeping vs ticks since adversary fire ----
|
||||
print("\n=== range response: mean range (px) vs ticks-since-adversary-fire ===")
|
||||
for t in range(0, 41, 5):
|
||||
vals = []
|
||||
for rd in rlist:
|
||||
R = rd["rows"]
|
||||
lo = rd["start"]
|
||||
for f in fire_by_round[rd["round"]]:
|
||||
gi = f + t
|
||||
if lo <= gi < lo + len(R):
|
||||
r = R[gi - lo]
|
||||
vals.append(math.hypot(r["ex"] - r["sx"], r["ey"] - r["sy"]))
|
||||
print(f" tau={t:3d}: mean range = {st.mean(vals):7.1f} (n={len(vals)})")
|
||||
|
||||
# ---- cross-correlation: fire impulse train vs response ----
|
||||
print("\n=== cross-correlation of adversary-fire impulse with response ===")
|
||||
# Sparse computation: fires are rare (k ~ 1000 out of ~20000 ticks), so the
|
||||
# only permutation-dependent term is sum_xy at each lag.
|
||||
for key, label in [("hc", "|dHeading|"), ("rev", "reversal")]:
|
||||
resp = [arr[key] for arr in arrays]
|
||||
fire_rel = []
|
||||
for arr in arrays:
|
||||
lo, n = arr["start"], arr["n"]
|
||||
fire_rel.append([gi - lo for gi in fire_by_round[arr["round"]] if lo <= gi < lo + n])
|
||||
|
||||
denom = {}
|
||||
for lag in range(0, 41):
|
||||
npar = 0
|
||||
sy = sy2 = 0.0
|
||||
for r in resp:
|
||||
n = len(r)
|
||||
start = 1 if lag == 0 else 0 # r[0] is NaN (round start)
|
||||
for j in range(start + lag, n):
|
||||
v = r[j]
|
||||
if v == v:
|
||||
npar += 1
|
||||
sy += v
|
||||
sy2 += v * v
|
||||
denom[lag] = (npar, sy, sy2)
|
||||
|
||||
def sparse_corr(fr_lists):
|
||||
out = {}
|
||||
for lag in range(0, 41):
|
||||
npar, sy, sy2 = denom[lag]
|
||||
if npar < 10:
|
||||
out[lag] = float("nan")
|
||||
continue
|
||||
k = 0
|
||||
sxy = 0.0
|
||||
for r, fl in zip(resp, fr_lists):
|
||||
n = len(r)
|
||||
start = 1 if lag == 0 else 0
|
||||
for fi in fl:
|
||||
if start <= fi < n - lag:
|
||||
v = r[fi + lag]
|
||||
if v == v:
|
||||
k += 1
|
||||
sxy += v
|
||||
mux = k / npar
|
||||
muy = sy / npar
|
||||
cov = sxy / npar - mux * muy
|
||||
varx = k / npar - mux * mux
|
||||
vary = sy2 / npar - muy * muy
|
||||
out[lag] = (cov / math.sqrt(varx * vary)
|
||||
if varx > 0 and vary > 0 else float("nan"))
|
||||
return out
|
||||
|
||||
real = sparse_corr(fire_rel)
|
||||
best = max((v, k) for k, v in real.items() if v == v)
|
||||
print(f" {label}: peak r={best[0]:+.4f} at lag={best[1]} ticks; "
|
||||
f"r(0)={real[0]:+.4f} r(10)={real[10]:+.4f} r(20)={real[20]:+.4f} "
|
||||
f"r(30)={real[30]:+.4f}")
|
||||
|
||||
null_peaks = []
|
||||
for _ in range(200):
|
||||
shifted = []
|
||||
for arr, fl in zip(arrays, fire_rel):
|
||||
n = arr["n"]
|
||||
kk = random.randint(0, max(1, n - 1))
|
||||
shifted.append([(x + kk) % n for x in fl])
|
||||
null_peaks.append(max(v for v in sparse_corr(shifted).values() if v == v))
|
||||
p = (1 + sum(1 for v in null_peaks if v >= best[0])) / (1 + len(null_peaks))
|
||||
print(f" permutation p(peak >= real peak) = {p:.4f} "
|
||||
f"(null peak mean={st.mean(null_peaks):+.4f}, max={max(null_peaks):+.4f})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,16 @@ export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}"
|
||||
BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
||||
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null
|
||||
|
||||
EVENTS_ARGS=()
|
||||
if [[ -n "${TR_EVENTS_OUT:-}" ]]; then
|
||||
EVENTS_ARGS=(--events-out "$TR_EVENTS_OUT")
|
||||
fi
|
||||
|
||||
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
|
||||
--rounds "$ROUNDS" --out "$OUT_JSONL" \
|
||||
--bot "$BOTDIR" --bot "$ADVERSARY" \
|
||||
--subject DrussGT --adversary "$(basename "$ADVERSARY")"
|
||||
--subject DrussGT --adversary "$(basename "$ADVERSARY")" \
|
||||
"${EVENTS_ARGS[@]}"
|
||||
|
||||
echo
|
||||
echo "=== movement statistics (TR) ==="
|
||||
|
||||
@@ -42,18 +42,29 @@ public final class TrBattleCapture {
|
||||
private static int globalTick = 0;
|
||||
private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
|
||||
private static final List<double[]> pendingRows = new ArrayList<>();
|
||||
private static final List<String> roundResultLines = new ArrayList<>();
|
||||
|
||||
// battle evidence counters (subject = the bot under test)
|
||||
private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0;
|
||||
private static int scansSeen = 0, hitsTaken = 0;
|
||||
|
||||
// optional raw-event sidecar (fire/hit/death events), used to measure
|
||||
// closed-loop reactivity of the subject to the adversary's bullets
|
||||
private static PrintWriter eventsWriter = null;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int rounds = 5;
|
||||
String outFile = "/tmp/tr_drussgt.jsonl";
|
||||
String adversary = "unknown";
|
||||
String note = "Tank Royale battle captured via TrBattleCapture (observer tick stream, perfect info)";
|
||||
String note = "Tank Royale battle via the robocode_shim bridge: the real, unmodified DrussGT fights the "
|
||||
+ "adversary in Tank Royale physics. Unlike the classic-robocode captures, the recorded trajectory "
|
||||
+ "was produced while DrussGT was reacting to THIS adversary's live bullets (closed loop at capture "
|
||||
+ "time). It is still a recorded trajectory, so replaying it against a different gun is open-loop; "
|
||||
+ "and it is still perfect-information (observer gives true positions every tick, unlike the live "
|
||||
+ "bot's stale between-scan WorldState).";
|
||||
List<String> botDirs = new ArrayList<>();
|
||||
String subject = "DrussGT";
|
||||
String eventsOut = null;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
@@ -63,9 +74,14 @@ public final class TrBattleCapture {
|
||||
case "--subject" -> subject = args[++i];
|
||||
case "--adversary" -> adversary = args[++i];
|
||||
case "--note" -> note = args[++i];
|
||||
case "--events-out" -> eventsOut = args[++i];
|
||||
default -> { System.err.println("Unknown arg: " + args[i]); System.exit(2); }
|
||||
}
|
||||
}
|
||||
if (eventsOut != null) {
|
||||
new File(eventsOut).getAbsoluteFile().getParentFile().mkdirs();
|
||||
eventsWriter = new PrintWriter(new BufferedWriter(new FileWriter(eventsOut)));
|
||||
}
|
||||
if (botDirs.isEmpty()) {
|
||||
System.err.println("--bot <dir> required (repeatable)");
|
||||
System.exit(2);
|
||||
@@ -88,9 +104,24 @@ public final class TrBattleCapture {
|
||||
System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern);
|
||||
try (var handle = runner.startBattleAsync(setup, bots)) {
|
||||
handle.getOnTickEvent().on(owner, event -> onTick(event, subjectPattern));
|
||||
handle.getOnRoundEnded().on(owner, event -> {
|
||||
StringBuilder sb = new StringBuilder("[capture] ROUND " + event.getRoundNumber() + " (turn "
|
||||
+ event.getTurnNumber() + "): ");
|
||||
for (var r : event.getResults()) {
|
||||
sb.append(String.format(Locale.ROOT, "%s rank=%d score=%d | ",
|
||||
r.getName(), r.getRank(), r.getTotalScore()));
|
||||
}
|
||||
String line = sb.toString();
|
||||
System.out.println(line);
|
||||
roundResultLines.add(line);
|
||||
});
|
||||
BattleResults results = handle.awaitResults();
|
||||
|
||||
writeOutput(outFile, adversary, note, rounds);
|
||||
if (eventsWriter != null) {
|
||||
eventsWriter.close();
|
||||
System.out.println("[capture] events -> " + eventsOut);
|
||||
}
|
||||
System.out.println("[capture] rows=" + globalTick + " rounds=" + roundTicks + " -> " + outFile);
|
||||
System.out.println("[capture] subject event counts: scans=" + scansSeen
|
||||
+ " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits
|
||||
@@ -146,9 +177,61 @@ public final class TrBattleCapture {
|
||||
&& sb.getScannedByBotId() == subjectId) {
|
||||
scansSeen++;
|
||||
}
|
||||
logEvent(event.getRoundNumber(), event.getTurnNumber(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Raw event sidecar: bullet fires / hits / deaths for both bots. */
|
||||
private static void logEvent(int round, int turn, dev.robocode.tankroyale.client.model.Event e) {
|
||||
if (eventsWriter == null) {
|
||||
return;
|
||||
}
|
||||
Long owner = null, victim = null, bullet = null;
|
||||
Double power = null, x = null, y = null, dir = null, damage = null;
|
||||
String type;
|
||||
if (e instanceof dev.robocode.tankroyale.client.model.BulletFiredEvent bf) {
|
||||
type = "fire";
|
||||
bullet = (long) bf.getBullet().getBulletId();
|
||||
owner = (long) bf.getBullet().getOwnerId();
|
||||
power = bf.getBullet().getPower();
|
||||
x = bf.getBullet().getX();
|
||||
y = bf.getBullet().getY();
|
||||
dir = bf.getBullet().getDirection();
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBotEvent bh) {
|
||||
type = "hit";
|
||||
bullet = (long) bh.getBullet().getBulletId();
|
||||
owner = (long) bh.getBullet().getOwnerId();
|
||||
victim = (long) bh.getVictimId();
|
||||
damage = bh.getDamage();
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBulletEvent bb) {
|
||||
type = "hitbullet";
|
||||
bullet = (long) bb.getBullet().getBulletId();
|
||||
owner = (long) bb.getBullet().getOwnerId();
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitWallEvent bw) {
|
||||
type = "hitwall";
|
||||
bullet = (long) bw.getBullet().getBulletId();
|
||||
owner = (long) bw.getBullet().getOwnerId();
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BotDeathEvent bd) {
|
||||
type = "death";
|
||||
victim = (long) bd.getVictimId();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"round\":").append(round).append(",\"tick\":").append(turn)
|
||||
.append(",\"type\":\"").append(type).append('\"');
|
||||
if (owner != null) sb.append(",\"owner\":").append(owner);
|
||||
if (victim != null) sb.append(",\"victim\":").append(victim);
|
||||
if (bullet != null) sb.append(",\"bullet\":").append(bullet);
|
||||
if (power != null) sb.append(",\"power\":").append(fmt(power));
|
||||
if (x != null) sb.append(",\"x\":").append(fmt(x));
|
||||
if (y != null) sb.append(",\"y\":").append(fmt(y));
|
||||
if (dir != null) sb.append(",\"dir\":").append(fmt(dir));
|
||||
if (damage != null) sb.append(",\"damage\":").append(fmt(damage));
|
||||
sb.append('}');
|
||||
eventsWriter.println(sb);
|
||||
}
|
||||
|
||||
private static void writeOutput(String outFile, String adversary, String note, int rounds) throws Exception {
|
||||
File f = new File(outFile);
|
||||
if (f.getParentFile() != null) {
|
||||
@@ -156,7 +239,7 @@ public final class TrBattleCapture {
|
||||
}
|
||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
|
||||
w.println("{\"meta\":{\"adversary\":\"" + esc(adversary)
|
||||
+ "\",\"source\":\"tank-royale\",\"perfect_info\":true,"
|
||||
+ "\",\"source\":\"tr-bridge\",\"closed_loop\":true,\"perfect_info\":true,"
|
||||
+ "\"arena\":{\"w\":800,\"h\":600},\"note\":\"" + esc(note) + "\"}}");
|
||||
for (double[] r : pendingRows) {
|
||||
w.println("{\"tick\":" + (long) r[0]
|
||||
@@ -178,6 +261,14 @@ public final class TrBattleCapture {
|
||||
}
|
||||
rs.println("]}");
|
||||
}
|
||||
try (PrintWriter rr = new PrintWriter(new BufferedWriter(new FileWriter(outFile + ".results.json")))) {
|
||||
rr.println("{\"roundResults\":[");
|
||||
for (int i = 0; i < roundResultLines.size(); i++) {
|
||||
rr.print(" \"" + esc(roundResultLines.get(i)) + "\"");
|
||||
rr.println(i + 1 < roundResultLines.size() ? "," : "");
|
||||
}
|
||||
rr.println("]}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String fmt(double v) {
|
||||
|
||||
Reference in New Issue
Block a user