Files
SirRoboGarage/tools/fixtures/TR_BRIDGE_FIXTURES.md
T
SirStone 17c99f542f 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.
2026-09-21 01:18:13 +02:00

10 KiB
Raw Blame History

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:

  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 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):

    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:

{"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

  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).