# 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/.jsonl.rounds.json`; per-round running scores are in `drussgt_meta/.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).