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:
2026-09-21 01:18:13 +02:00
parent 7f706e5b14
commit 17c99f542f
22 changed files with 63333 additions and 4 deletions
+9 -1
View File
@@ -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)
```
+356
View File
@@ -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
+7 -1
View File
@@ -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) {