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
+7
View File
@@ -5,6 +5,13 @@ Captured **real, unmodified DrussGT 3.1.4159** movement from classic Robocode
These are **movement-pattern fixtures for gun testing**, not runnable opponents. These are **movement-pattern fixtures for gun testing**, not runnable opponents.
> **See also:** [`TR_BRIDGE_FIXTURES.md`](TR_BRIDGE_FIXTURES.md) — Tank Royale
> bridge captures (`source:"tr-bridge"`) where the recorded DrussGT was
> reacting to *our* bot's real bullets at capture time. Same JSONL format, but
> `closed_loop:true`. The classic files below remain the higher-fidelity
> *trajectory-conversion* reference (validated to 0.000°); the TR files are the
> better gun benchmark. Both are perfect-information.
## How the data was obtained ## How the data was obtained
| Artifact | Source | Result | | Artifact | Source | Result |
+200
View File
@@ -0,0 +1,200 @@
# Tank-Royale bridge fixtures — DrussGT vs our bots (closed loop at capture time)
Fixtures captured from **real Tank Royale battles** in which the *real,
unmodified* classic DrussGT 3.1.4159 plays through the Java bridge in
`tools/robocode_shim/`, against opponents whose shots it actually had to dodge.
They complement the classic-Robocode set in
[`DRUSSGT_FIXTURES.md`](DRUSSGT_FIXTURES.md).
## Why these exist — the fidelity gap they close
`DRUSSGT_FIXTURES.md` documents two gaps in the classic captures:
1. **Open loop.** Replayed DrussGT never dodges *our* bullets, so our gun
scores optimistically against it.
2. **Perfect information.** The observer reports true positions every tick,
unlike the live bot's stale between-scan `WorldState`.
These TR-bridge fixtures close gap **1, at capture time**: the recorded
trajectory is one in which DrussGT was actively reacting to **our bot's real
bullets** (ModularBot's gun) in Tank Royale physics. The coupling is measured in
[Closed-loop evidence](#closed-loop-evidence) below.
Gap **2 remains.** These are still observer captures: true positions every tick,
no fog of war, no sensor latency. They are therefore **optimistic relative to
live play**. And because a fixture is a *recording*, replaying it against a
*different* gun is still open-loop — the closed loop exists only in the battle
that produced the file, not at replay time.
## Provenance / capture mechanism
* Bot host: `tools/robocode_shim/` (`DrussGTBridge` + `ClassicPeer`). See the
shim README §5 for the physics/API details and §5.9 for the known divergences.
* Observer: `tools/robocode_shim/src/robocode_shim/TrBattleCapture.java`, which
runs the Tank Royale battle-runner API (embedded server) and writes the shared
JSONL format, plus a `.rounds.json` sidecar and a raw `.results.json`.
* Commands (shield **off** unless stated):
```bash
tools/robocode_shim/run_bridge_battle.sh \
/tmp/capture_bots/ModularBot 15 /tmp/cap/tr_drussgt_vs_modularbot.jsonl
# TR_EVENTS_OUT=... adds the bullet-event sidecar used for the loop analysis
```
* DrussGT runs as the **pure wave surfer** (`DrussMoveGT`), i.e. the
`EnergyDome` shield is disabled (`DRUSSGT_SHIELD` unset). `DRUSSGT_SHIELD=1`
was used only for the `_shield` file. The ModularBot binary is a frozen
snapshot (md5 `d39dbdc326132cd371cd12f97bfb0a16`) copied to `/tmp`; no Nim build
was invoked during capture.
## Format
Same field layout as the classic fixtures
(`ex/ey/eh/es/ee`, `sx/sy/sh/ss/se`, continuous global `tick`), but the `meta`
line is distinct:
```json
{"meta":{"adversary":"ModularBot","source":"tr-bridge","closed_loop":true,
"perfect_info":true,"arena":{"w":800,"h":600},"note":"..."}}
```
`source` is `"tr-bridge"` (classic files say `"classic-robocode"`),
`closed_loop` is `true`, and the `note` states the capture-time loop and the
remaining open-loop/perfect-info caveats. `e*` is always DrussGT; `s*` is the
adversary. Round boundaries are in `drussgt_meta/<file>.jsonl.rounds.json`;
per-round running scores are in `drussgt_meta/<file>.jsonl.results.json`.
## Files
| File | rounds | ticks | bytes | adversary | shield |
|---|---:|---:|---:|---|---|
| `tr_drussgt_vs_modularbot.jsonl` | 15 | 20,026 | 2,976,439 | **ModularBot (our bot)** | off |
| `tr_drussgt_vs_modularbot_shield.jsonl` | 10 | 12,629 | 1,872,874 | ModularBot | **on** |
| `tr_drussgt_vs_spinbot.jsonl` | 10 | 10,824 | 1,597,273 | sample SpinBot | off |
| `tr_drussgt_vs_crazy.jsonl` | 10 | 11,507 | 1,701,186 | sample Crazy | off |
| `tr_drussgt_vs_corners.jsonl` | 10 | 2,575 | 379,680 | sample Corners | off |
The primary benchmark is **`tr_drussgt_vs_modularbot.jsonl`** (shield off):
20,026 ticks over 15 rounds, mean 1,335 ticks/round (min 845, max 1,893). The
rounds are *not* short — DrussGT does not trivially rush ModularBot — so
ModularBot generated a real shot stream: **1,134 shots** in that run.
## Movement statistics vs the classic captures
Produced by `tools/robocode_fixture_capture/analyze.py` (values are fractions,
median range in px). Classic rows are copied from `DRUSSGT_FIXTURES.md`.
| fixture | mean speed | full ≥7.5 | rest <0.5 | neg. vel | mean \|Δh\| | \|Δh\|>5° | median dist | perp frac | radial frac | mean\|sin\| |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| **TR** modularbot | 5.922 | 0.528 | 0.037 | 0.463 | 1.629 | 0.109 | 463.9 | **0.967** | **0.001** | 0.971 |
| **TR** modularbot (shield) | 5.826 | 0.533 | 0.070 | 0.466 | 1.574 | 0.097 | 450.0 | 0.959 | 0.001 | 0.967 |
| **TR** spinbot | 6.981 | 0.749 | 0.019 | 0.487 | 1.596 | 0.091 | 443.2 | 0.982 | 0.001 | 0.979 |
| **TR** crazy | 7.040 | 0.767 | 0.019 | 0.413 | 2.035 | 0.094 | 423.1 | 0.883 | 0.005 | 0.946 |
| **TR** corners | 6.954 | 0.770 | 0.037 | 0.489 | 2.158 | 0.111 | 524.5 | 0.883 | 0.026 | 0.941 |
| classic spinbot | 6.59 | 0.701 | 0.067 | 0.464 | 1.81 | 0.094 | 402 | 0.962 | 0.001 | 0.971 |
| classic crazy | 6.16 | 0.668 | 0.137 | 0.439 | 2.01 | 0.083 | 378 | 0.829 | 0.010 | 0.934 |
| classic corners | 6.01 | 0.586 | 0.092 | 0.419 | 2.09 | 0.125 | 504 | 0.920 | 0.004 | 0.964 |
| classic drussgt (mirror) | 5.26 | 0.411 | 0.066 | 0.457 | 1.29 | 0.082 | 526 | 0.951 | 0.001 | 0.972 |
The wave-surfer signature — strongly **perpendicular** to the opponent
(perp frac 0.88–0.98), near-zero **radial** fraction, ~41–49% **reversals**,
near-full speed — is present in the TR captures and matches the classic set
closely (perp/radial within ~0.04 of the matching classic opponent). The TR runs
are slightly faster and shorter-ranged, as before, because the rounds are
shorter and the shield is off. This is surfing, not a straight line or a stall.
## Battle outcomes
**DrussGT vs ModularBot, 15 rounds, shield off:** DrussGT 1447 – 300,
14 rounds won (ModularBot won round 5). Per-round ticks and running scores:
| round | ticks | DrussGT score | ModularBot score | winner |
|---:|---:|---:|---:|---|
| 1 | 1252 | 71 | 8 | DrussGT |
| 2 | 1416 | 152 | 20 | DrussGT |
| 3 | 845 | 286 | 36 | DrussGT |
| 4 | 1446 | 385 | 68 | DrussGT |
| 5 | 1893 | 399 | 156 | **ModularBot** |
| 6 | 1080 | 499 | 156 | DrussGT |
| 7 | 1460 | 606 | 188 | DrussGT |
| 8 | 1213 | 708 | 200 | DrussGT |
| 9 | 1759 | 808 | 228 | DrussGT |
| 10 | 1380 | 910 | 236 | DrussGT |
| 11 | 1127 | 1026 | 252 | DrussGT |
| 12 | 1258 | 1144 | 272 | DrussGT |
| 13 | 1330 | 1240 | 280 | DrussGT |
| 14 | 1474 | 1336 | 292 | DrussGT |
| 15 | 1093 | 1447 | 300 | DrussGT |
Scores are the server's **cumulative** running totals at round end.
DrussGT: 1400 bullets / 169 hits / 60 hits taken. ModularBot: 1,134 bullets /
60 hits / 169 hits taken; 29 bullet-bullet.
**Adversaries (shield off, 10 rounds each):** SpinBot 1175–0, Crazy 1080–1,
Corners 1659–0; all 10 rounds won in each. **ModularBot shield on, 10 rounds:**
DrussGT 939–287, 9 rounds won (ModularBot won round 1).
## Closed-loop evidence
Claim under test: *at capture time DrussGT's movement is coupled to ModularBot's
live fire*, not an independent open-loop trajectory. Measured with
`tools/robocode_shim/analyze_closed_loop.py` on
`tr_drussgt_vs_modularbot.jsonl` + `tools/robocode_shim/evidence/tr_drussgt_vs_modularbot.events.json`:
```bash
python3 tools/robocode_shim/analyze_closed_loop.py \
tools/fixtures/tr_drussgt_vs_modularbot.jsonl \
tools/robocode_shim/evidence/tr_drussgt_vs_modularbot.events.json
```
What the run establishes (numbers from that exact run):
* **ModularBot fires on an exogenous, heat-limited clock.** Fire interval
median 14 ticks (p10 14, p90 20). It is a near-metronome, so it is not
following DrussGT tick-by-tick.
* **DrussGT's turning is strongly phase-locked to ModularBot's fire ticks.**
Event-locking `|Δheading|` to the adversary's fire times gives an oscillating
response (dips ≈0.96–1.35°, peaks ≈2.1–2.7° about a ≈1.47° mean) with the
fire period (~13–14 ticks). With 15 rounds, 1,134 fires, almost every lag is
outside the 95% band of a null that **circularly shifts each round's fire
times** (permutation null). The peaks are the surf re-committing on each new
enemy wave (fire detection); the lulls are the committed run between waves.
* **The lock is enemy-specific, not an internal clock.** The same analysis
locked to DrussGT's **own** fire times is essentially flat (only lag 0 is
marginally outside the null). So the oscillation is driven by the *incoming*
fire, not by DrussGT's own gun cadence.
* **Cross-correlation `|Δheading|` vs fire impulse:** peak r = **+0.111** at
lag **12** ticks, permutation p = **0.005** (null peak mean +0.016, max
+0.028). Reversal rate: peak r = **+0.025** at lag 29, p = 0.035.
* **Range keeping responds weakly.** Mean range rises monotonically from
478.3 px at the fire to 481.7 px 30 ticks later — a ~3 px drift, near the
noise floor, so it is not claimed as strong evidence.
**Honest reading.** The PSTH and cross-correlation are significant against a
phase-shuffled null and are absent in the own-fire control, so DrussGT's
movement *is* temporally coupled to ModularBot's firing at capture time. The
effect size is modest (peak r ≈ 0.11) — expected, because DrussGT surfs almost
all the time regardless, and the lock is modulated by the periodic wave train.
What is **not** shown is that each individual bullet is dodged on approach: the
shield (which detects individual bullets) is off here, so the coupling is the
wave-level reaction to the enemy **energy drop / fire**, which is exactly how
this surfer decides to move. No claim is made beyond that.
## Caveats
1. **PERFECT INFORMATION.** Observer captures report true positions every tick;
the live bot's `WorldState` is stale between radar scans. Optimistic vs live
play.
2. **OPEN LOOP AT REPLAY TIME.** These are recordings. DrussGT reacts to
ModularBot's bullets in the file that was captured, not to whatever gun later
replays the trajectory. The `closed_loop:true` flag describes the capture,
not the replay.
3. **Physics divergences** from classic Robocode remain (move/turn ordering,
distance bookkeeping, no exact trajectory equivalence). See the shim README
§5.9. The classic captures validate to 0.000°; TR converts at ~1.5° mean
(median ~0.05°) because of the engine's pre-turn move ordering.
4. The shield-on file is included for completeness; its round 1 is the known
shield warm-up case (shorter, more stationary) — the shield-off files are the
primary set.
5. No round in these captures aborted or timed out; every round ended in a
death (see winners table).
@@ -0,0 +1,12 @@
{"roundResults":[
"[capture] ROUND 1 (turn 223): DrussGT rank=1 score=164 | Corners rank=2 score=0 | ",
"[capture] ROUND 2 (turn 218): DrussGT rank=1 score=328 | Corners rank=2 score=0 | ",
"[capture] ROUND 3 (turn 353): DrussGT rank=1 score=491 | Corners rank=2 score=0 | ",
"[capture] ROUND 4 (turn 247): DrussGT rank=1 score=658 | Corners rank=2 score=0 | ",
"[capture] ROUND 5 (turn 200): DrussGT rank=1 score=824 | Corners rank=2 score=0 | ",
"[capture] ROUND 6 (turn 256): DrussGT rank=1 score=991 | Corners rank=2 score=0 | ",
"[capture] ROUND 7 (turn 250): DrussGT rank=1 score=1160 | Corners rank=2 score=0 | ",
"[capture] ROUND 8 (turn 247): DrussGT rank=1 score=1327 | Corners rank=2 score=0 | ",
"[capture] ROUND 9 (turn 327): DrussGT rank=1 score=1493 | Corners rank=2 score=0 | ",
"[capture] ROUND 10 (turn 254): DrussGT rank=1 score=1659 | Corners rank=2 score=0 | "
]}
@@ -0,0 +1 @@
{"rounds":[{"round":1,"startTick":0,"count":223},{"round":2,"startTick":223,"count":218},{"round":3,"startTick":441,"count":353},{"round":4,"startTick":794,"count":247},{"round":5,"startTick":1041,"count":200},{"round":6,"startTick":1241,"count":256},{"round":7,"startTick":1497,"count":250},{"round":8,"startTick":1747,"count":247},{"round":9,"startTick":1994,"count":327},{"round":10,"startTick":2321,"count":254}]}
@@ -0,0 +1,12 @@
{"roundResults":[
"[capture] ROUND 1 (turn 1453): DrussGT rank=1 score=95 | Crazy rank=2 score=1 | ",
"[capture] ROUND 2 (turn 666): DrussGT rank=1 score=249 | Crazy rank=2 score=1 | ",
"[capture] ROUND 3 (turn 1356): DrussGT rank=1 score=355 | Crazy rank=2 score=1 | ",
"[capture] ROUND 4 (turn 771): DrussGT rank=1 score=470 | Crazy rank=2 score=1 | ",
"[capture] ROUND 5 (turn 1369): DrussGT rank=1 score=561 | Crazy rank=2 score=1 | ",
"[capture] ROUND 6 (turn 1182): DrussGT rank=1 score=662 | Crazy rank=2 score=1 | ",
"[capture] ROUND 7 (turn 813): DrussGT rank=1 score=790 | Crazy rank=2 score=1 | ",
"[capture] ROUND 8 (turn 1400): DrussGT rank=1 score=899 | Crazy rank=2 score=1 | ",
"[capture] ROUND 9 (turn 857): DrussGT rank=1 score=987 | Crazy rank=2 score=1 | ",
"[capture] ROUND 10 (turn 1640): DrussGT rank=1 score=1080 | Crazy rank=2 score=1 | "
]}
@@ -0,0 +1 @@
{"rounds":[{"round":1,"startTick":0,"count":1453},{"round":2,"startTick":1453,"count":666},{"round":3,"startTick":2119,"count":1356},{"round":4,"startTick":3475,"count":771},{"round":5,"startTick":4246,"count":1369},{"round":6,"startTick":5615,"count":1182},{"round":7,"startTick":6797,"count":813},{"round":8,"startTick":7610,"count":1400},{"round":9,"startTick":9010,"count":857},{"round":10,"startTick":9867,"count":1640}]}
@@ -0,0 +1,17 @@
{"roundResults":[
"[capture] ROUND 1 (turn 1252): DrussGT rank=1 score=71 | ModularBot rank=2 score=8 | ",
"[capture] ROUND 2 (turn 1416): DrussGT rank=1 score=152 | ModularBot rank=2 score=20 | ",
"[capture] ROUND 3 (turn 845): DrussGT rank=1 score=286 | ModularBot rank=2 score=36 | ",
"[capture] ROUND 4 (turn 1446): DrussGT rank=1 score=385 | ModularBot rank=2 score=68 | ",
"[capture] ROUND 5 (turn 1893): DrussGT rank=1 score=399 | ModularBot rank=2 score=156 | ",
"[capture] ROUND 6 (turn 1080): DrussGT rank=1 score=499 | ModularBot rank=2 score=156 | ",
"[capture] ROUND 7 (turn 1460): DrussGT rank=1 score=606 | ModularBot rank=2 score=188 | ",
"[capture] ROUND 8 (turn 1213): DrussGT rank=1 score=708 | ModularBot rank=2 score=200 | ",
"[capture] ROUND 9 (turn 1759): DrussGT rank=1 score=808 | ModularBot rank=2 score=228 | ",
"[capture] ROUND 10 (turn 1380): DrussGT rank=1 score=910 | ModularBot rank=2 score=236 | ",
"[capture] ROUND 11 (turn 1127): DrussGT rank=1 score=1026 | ModularBot rank=2 score=252 | ",
"[capture] ROUND 12 (turn 1258): DrussGT rank=1 score=1144 | ModularBot rank=2 score=272 | ",
"[capture] ROUND 13 (turn 1330): DrussGT rank=1 score=1240 | ModularBot rank=2 score=280 | ",
"[capture] ROUND 14 (turn 1474): DrussGT rank=1 score=1336 | ModularBot rank=2 score=292 | ",
"[capture] ROUND 15 (turn 1093): DrussGT rank=1 score=1447 | ModularBot rank=2 score=300 | "
]}
@@ -0,0 +1 @@
{"rounds":[{"round":1,"startTick":0,"count":1252},{"round":2,"startTick":1252,"count":1416},{"round":3,"startTick":2668,"count":845},{"round":4,"startTick":3513,"count":1446},{"round":5,"startTick":4959,"count":1893},{"round":6,"startTick":6852,"count":1080},{"round":7,"startTick":7932,"count":1460},{"round":8,"startTick":9392,"count":1213},{"round":9,"startTick":10605,"count":1759},{"round":10,"startTick":12364,"count":1380},{"round":11,"startTick":13744,"count":1127},{"round":12,"startTick":14871,"count":1258},{"round":13,"startTick":16129,"count":1330},{"round":14,"startTick":17459,"count":1474},{"round":15,"startTick":18933,"count":1093}]}
@@ -0,0 +1,12 @@
{"roundResults":[
"[capture] ROUND 1 (turn 593): ModularBot rank=1 score=139 | DrussGT rank=2 score=31 | ",
"[capture] ROUND 2 (turn 1081): ModularBot rank=1 score=151 | DrussGT rank=2 score=144 | ",
"[capture] ROUND 3 (turn 1237): DrussGT rank=1 score=246 | ModularBot rank=2 score=167 | ",
"[capture] ROUND 4 (turn 1296): DrussGT rank=1 score=366 | ModularBot rank=2 score=195 | ",
"[capture] ROUND 5 (turn 1550): DrussGT rank=1 score=452 | ModularBot rank=2 score=207 | ",
"[capture] ROUND 6 (turn 1284): DrussGT rank=1 score=535 | ModularBot rank=2 score=211 | ",
"[capture] ROUND 7 (turn 1364): DrussGT rank=1 score=618 | ModularBot rank=2 score=223 | ",
"[capture] ROUND 8 (turn 1546): DrussGT rank=1 score=726 | ModularBot rank=2 score=255 | ",
"[capture] ROUND 9 (turn 1286): DrussGT rank=1 score=837 | ModularBot rank=2 score=275 | ",
"[capture] ROUND 10 (turn 1392): DrussGT rank=1 score=939 | ModularBot rank=2 score=287 | "
]}
@@ -0,0 +1 @@
{"rounds":[{"round":1,"startTick":0,"count":593},{"round":2,"startTick":593,"count":1081},{"round":3,"startTick":1674,"count":1237},{"round":4,"startTick":2911,"count":1296},{"round":5,"startTick":4207,"count":1550},{"round":6,"startTick":5757,"count":1284},{"round":7,"startTick":7041,"count":1364},{"round":8,"startTick":8405,"count":1546},{"round":9,"startTick":9951,"count":1286},{"round":10,"startTick":11237,"count":1392}]}
@@ -0,0 +1,12 @@
{"roundResults":[
"[capture] ROUND 1 (turn 1334): DrussGT rank=1 score=101 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 2 (turn 1454): DrussGT rank=1 score=184 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 3 (turn 758): DrussGT rank=1 score=284 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 4 (turn 1035): DrussGT rank=1 score=417 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 5 (turn 691): DrussGT rank=1 score=530 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 6 (turn 1013): DrussGT rank=1 score=674 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 7 (turn 1512): DrussGT rank=1 score=779 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 8 (turn 1215): DrussGT rank=1 score=905 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 9 (turn 1119): DrussGT rank=1 score=1031 | Spin Bot rank=2 score=0 | ",
"[capture] ROUND 10 (turn 693): DrussGT rank=1 score=1175 | Spin Bot rank=2 score=0 | "
]}
@@ -0,0 +1 @@
{"rounds":[{"round":1,"startTick":0,"count":1334},{"round":2,"startTick":1334,"count":1454},{"round":3,"startTick":2788,"count":758},{"round":4,"startTick":3546,"count":1035},{"round":5,"startTick":4581,"count":691},{"round":6,"startTick":5272,"count":1013},{"round":7,"startTick":6285,"count":1512},{"round":8,"startTick":7797,"count":1215},{"round":9,"startTick":9012,"count":1119},{"round":10,"startTick":10131,"count":693}]}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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 `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 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 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 ### 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 DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
SmokeTest.java the standalone runtime proof SmokeTest.java the standalone runtime proof
TrBattleCapture.java observer: battle -> classic JSONL fixture format 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) 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}" BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null "$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 \ java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
--rounds "$ROUNDS" --out "$OUT_JSONL" \ --rounds "$ROUNDS" --out "$OUT_JSONL" \
--bot "$BOTDIR" --bot "$ADVERSARY" \ --bot "$BOTDIR" --bot "$ADVERSARY" \
--subject DrussGT --adversary "$(basename "$ADVERSARY")" --subject DrussGT --adversary "$(basename "$ADVERSARY")" \
"${EVENTS_ARGS[@]}"
echo echo
echo "=== movement statistics (TR) ===" echo "=== movement statistics (TR) ==="
@@ -42,18 +42,29 @@ public final class TrBattleCapture {
private static int globalTick = 0; private static int globalTick = 0;
private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>(); private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
private static final List<double[]> pendingRows = new ArrayList<>(); private static final List<double[]> pendingRows = new ArrayList<>();
private static final List<String> roundResultLines = new ArrayList<>();
// battle evidence counters (subject = the bot under test) // battle evidence counters (subject = the bot under test)
private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0; private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0;
private static int scansSeen = 0, hitsTaken = 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 { public static void main(String[] args) throws Exception {
int rounds = 5; int rounds = 5;
String outFile = "/tmp/tr_drussgt.jsonl"; String outFile = "/tmp/tr_drussgt.jsonl";
String adversary = "unknown"; 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<>(); List<String> botDirs = new ArrayList<>();
String subject = "DrussGT"; String subject = "DrussGT";
String eventsOut = null;
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
switch (args[i]) { switch (args[i]) {
@@ -63,9 +74,14 @@ public final class TrBattleCapture {
case "--subject" -> subject = args[++i]; case "--subject" -> subject = args[++i];
case "--adversary" -> adversary = args[++i]; case "--adversary" -> adversary = args[++i];
case "--note" -> note = args[++i]; case "--note" -> note = args[++i];
case "--events-out" -> eventsOut = args[++i];
default -> { System.err.println("Unknown arg: " + args[i]); System.exit(2); } 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()) { if (botDirs.isEmpty()) {
System.err.println("--bot <dir> required (repeatable)"); System.err.println("--bot <dir> required (repeatable)");
System.exit(2); System.exit(2);
@@ -88,9 +104,24 @@ public final class TrBattleCapture {
System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern); System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern);
try (var handle = runner.startBattleAsync(setup, bots)) { try (var handle = runner.startBattleAsync(setup, bots)) {
handle.getOnTickEvent().on(owner, event -> onTick(event, subjectPattern)); 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(); BattleResults results = handle.awaitResults();
writeOutput(outFile, adversary, note, rounds); 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] rows=" + globalTick + " rounds=" + roundTicks + " -> " + outFile);
System.out.println("[capture] subject event counts: scans=" + scansSeen System.out.println("[capture] subject event counts: scans=" + scansSeen
+ " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits + " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits
@@ -146,9 +177,61 @@ public final class TrBattleCapture {
&& sb.getScannedByBotId() == subjectId) { && sb.getScannedByBotId() == subjectId) {
scansSeen++; 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 { private static void writeOutput(String outFile, String adversary, String note, int rounds) throws Exception {
File f = new File(outFile); File f = new File(outFile);
if (f.getParentFile() != null) { if (f.getParentFile() != null) {
@@ -156,7 +239,7 @@ public final class TrBattleCapture {
} }
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) { try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
w.println("{\"meta\":{\"adversary\":\"" + esc(adversary) 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) + "\"}}"); + "\"arena\":{\"w\":800,\"h\":600},\"note\":\"" + esc(note) + "\"}}");
for (double[] r : pendingRows) { for (double[] r : pendingRows) {
w.println("{\"tick\":" + (long) r[0] w.println("{\"tick\":" + (long) r[0]
@@ -178,6 +261,14 @@ public final class TrBattleCapture {
} }
rs.println("]}"); 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) { private static String fmt(double v) {