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.
10 KiB
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.
Why these exist — the fidelity gap they close
DRUSSGT_FIXTURES.md documents two gaps in the classic captures:
- Open loop. Replayed DrussGT never dodges our bullets, so our gun scores optimistically against it.
- 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 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.jsonsidecar and a raw.results.json. -
Commands (shield off unless stated):
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. theEnergyDomeshield is disabled (DRUSSGT_SHIELDunset).DRUSSGT_SHIELD=1was used only for the_shieldfile. The ModularBot binary is a frozen snapshot (md5d39dbdc326132cd371cd12f97bfb0a16) 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:
{"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:
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
- PERFECT INFORMATION. Observer captures report true positions every tick;
the live bot's
WorldStateis stale between radar scans. Optimistic vs live play. - 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:trueflag describes the capture, not the replay. - 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.
- 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.
- No round in these captures aborted or timed out; every round ended in a death (see winners table).