Files
SirRoboGarage/tools/fixtures/DRUSSGT_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

9.9 KiB
Raw Blame History

Classic-Robocode leader movement fixtures — DrussGT

Captured real, unmodified DrussGT 3.1.4159 movement from classic Robocode 1.9.5.5 and converted it to the project's shared JSONL fixture format.

These are movement-pattern fixtures for gun testing, not runnable opponents.

See also: 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

Artifact Source Result
Classic Robocode 1.9.5.5 (full install, 20,431,686 bytes) https://sourceforge.net/projects/robocode/files/robocode/1.9.5.5/robocode-1.9.5.5-setup.jar/download HTTP 200
robocode-1.9.5.5.zip same SourceForge dir HTTP 404 (does not exist)
net.sf.robocode:* 1.9.5.5 (api/core/battle/host/samples/repository/ui/sound/roborumble) https://repo1.maven.org/maven2/net/sf/robocode/ HTTP 200 (available; not needed once the full installer was fetched)
DrussGT 3.1.4159 jar (159,289 bytes, md5 5cd6015dcc6d6da8a7e6aeecb1fec211) http://robocode-archive.strangeautomata.com/robots/jk.mega.DrussGT_3.1.4159.jar HTTP 200

The Robocode "setup jar" is a plain ZIP of the full installation (libs/, robots/, battles/, javadoc). Extracting it is enough to run the programmatic control API headlessly. On Java 21 the engine needs -Djava.security.manager=allow and four --add-opens flags (see tools/robocode_fixture_capture/README.md); Java 17/11 also work.

The DrussGT jar and classic Robocode are third-party redistributables and are kept OUT of the repository — they live only under /tmp/robocode/. tools/robocode_fixture_capture/capture.sh re-downloads them on demand.

Capture mechanism

tools/robocode_fixture_capture/Capture.java uses the classic Robocode control API — no GUI:

  • robocode.control.RobocodeEngine + BattleSpecification (800×600, 20 rounds, gun-cooling 0.1, inactivity 450) run the battle headlessly.
  • A BattleAdaptor receives onTurnEnded(TurnEndedEvent) every turn.
  • event.getTurnSnapshot().getRobots() yields IRobotSnapshot[] with getX(), getY(), getBodyHeading(), getVelocity(), getEnergy() for every robot, every turn — true positions, no fog of war.
  • The fallback "spy robot" approach was not needed; the observer API worked.
  • Robots captured: e* = jk.mega.DrussGT, s* = the opponent that fought it.

Coordinate conversion (exact)

Tank Royale and classic Robocode share the same positional axes (origin bottom-left, +x right, +y up), so x/y are copied unchanged. Only the angle system differs:

  • classic: 0° = North (+y), positive rotation clockwise
  • Tank Royale: 0° = East (+x), positive rotation counter-clockwise

The control-snapshot API returns headings in radians (per the official javadoc: "Returns the body heading of the robot in radians"), in the classic convention. Conversion used:

double classicDeg = Math.toDegrees(rawBodyHeadingRadians);   // 0=north, clockwise
double tankDeg    = (90.0 - classicDeg) % 360.0;             // 0=east,  counter-clockwise
if (tankDeg < 0) tankDeg += 360.0;
// x, y: unchanged

Equivalently, in radians: tankRad = PI/2 - classicRad.

Validation: for every consecutive tick within a round, the measured displacement direction of DrussGT was compared with the direction implied by the converted heading + signed velocity. Mean angular error is 0.000°–0.001° on every fixture — the conversion is exact, and it doubles as proof the fixtures are real recorded motion rather than fabricated data.

Fields

Following the mandated shared format exactly:

{"meta":{"adversary":"...","source":"classic-robocode","perfect_info":true,"arena":{"w":800,"h":600},"note":"..."}}
{"tick":0,"ex":...,"ey":...,"eh":...,"es":...,"ee":...,"sx":...,"sy":...,"sh":...,"ss":...,"se":...}
  • eh/sh: Tank Royale heading in degrees (0=east, CCW).
  • es/ss: signed velocity in px/tick (getVelocity(); negative = moving backwards relative to body heading).
  • ee/se: energy (0–100).
  • tick: continuous global counter across all rounds in the file; positions teleport at round boundaries. Round boundaries are listed in drussgt_meta/<file>.jsonl.rounds.json (startTick, count).

Files

File ticks rounds bytes enemy (e*) shooter (s*)
drussgt_vs_spinbot.jsonl 5002 20 741,720 DrussGT sample.SpinBot
drussgt_vs_ramfire.jsonl 3240 20 481,176 DrussGT sample.RamFire (rammer)
drussgt_vs_crazy.jsonl 9025 20 1,335,519 DrussGT sample.Crazy
drussgt_vs_corners.jsonl 4975 20 732,806 DrussGT sample.Corners (wall-hugger)
drussgt_vs_drussgt.jsonl 6555 2 965,834 DrussGT DrussGT (mirror)
contrast_straightline.jsonl 1451 1 213,049 trivial.StraightLine sample.SittingDuck
contrast_stationary_sittingduck.jsonl 1451 1 213,117 sample.SittingDuck sample.SittingDuck

drussgt_meta/ holds the round-boundary sidecars; it is deliberately outside the *.jsonl namespace.

Statistics — evidence this is real DrussGT movement

All numbers measured from the fixtures above.

| fixture | mean speed | %ticks speed≥7.5 | %ticks speed<0.5 | %neg. velocity | mean |Δheading| | %|Δh|>5° | median dist | perp frac | radial frac | mean|sin| | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | drussgt_vs_drussgt | 5.26 | 41.1% | 6.6% | 45.7% | 1.29° | 8.2% | 526 | 0.951 | 0.001 | 0.972 | | drussgt_vs_spinbot | 6.59 | 70.1% | 6.7% | 46.4% | 1.81° | 9.4% | 402 | 0.962 | 0.001 | 0.971 | | drussgt_vs_ramfire | 6.65 | 72.3% | 4.6% | 41.7% | 3.00° | 14.9% | 283 | 0.651 | 0.013 | 0.898 | | drussgt_vs_crazy | 6.16 | 66.8% | 13.7% | 43.9% | 2.01° | 8.3% | 378 | 0.829 | 0.010 | 0.934 | | drussgt_vs_corners | 6.01 | 58.6% | 9.2% | 41.9% | 2.09° | 12.5% | 504 | 0.920 | 0.004 | 0.964 | | contrast straight-line | 6.28 | 75.3% | 18.4% | 0.0% | 1.66° | 16.6%* | 239 | 0.086 | 0.752 | 0.373 | | contrast stationary | 0.00 | 0.0% | 100% | 0.0% | 0.00° | 0.0% | 210 | n/a | n/a | n/a |

perp frac = fraction of moving ticks whose movement is within 30° of perpendicular to the enemy→shooter line (|sin δ| > 0.866); radial frac = within 30° of that line (|cos δ| > 0.866).

* the straight-line bot only ever turns 10°/tick in place at a wall, giving the bimodal heading-change distribution below — it never arcs.

Speed distribution (fraction of all ticks per px/tick bin):

bin 0 0.5-1 1-2 2-3 3-4 4-5 5-6 6-7 7-7.5 7.5-8
DrussGT vs SpinBot .067 .006 .024 .033 .024 .042 .030 .052 .020 .701
straight-line (contrast) .184 .000 .009 .009 .009 .009 .009 .009 .009 .753
stationary (contrast) 1.000 0 0 0 0 0 0 0 0 0

Heading-change-per-tick distribution (fraction, degrees):

bin 0-0.5 0.5-1 1-2 2-3 3-5 5-7 7-10 10-11
DrussGT vs SpinBot .439 .113 .122 .057 .175 .035 .044 .015
DrussGT vs Crazy .389 .096 .117 .075 .240 .033 .041 .010
straight-line (contrast) .833 0 0 .001 0 0 0 .166
stationary (contrast) 1.000 0 0 0 0 0 0 0

Interpretation: DrussGT spends most of its time at full speed, frequently reverses (≈42–46% of ticks have negative velocity — stop-and-go surfing), and its movement is strongly perpendicular to the opponent (mean |sin| ≈ 0.90–0.97, radial fraction ≈ 0.001–0.013) while actively controlling range (median distance 283–526 px, varying by opponent). The contrast bots are stationary or move radially at constant heading with no reversals. The difference is in the numbers, not asserted.

Caveats (read before using these as an opponent)

  1. OPEN-LOOP. This is recorded trajectory data replayed into a gun test. Replayed DrussGT does not react to bullets fired by our bot, so our gun's score against these fixtures is optimistic. This is a movement-pattern benchmark, not an opponent. It tests whether our aiming math can lead and predict leader-class motion; it does not test whether we can out-duel DrussGT.
  2. PERFECT INFORMATION. Classic Robocode's battle observer reports true positions every turn. Our live bot's WorldState is stale between radar scans. Fixtures from this source are therefore optimistic relative to live play in two ways (open-loop + no sensor latency/fog). A gun that performs well here may perform worse live.
  3. Bullet-shielding is excluded. Against predictable guns (sample.Walls, sample.TrackFire) DrussGT's EnergyDome shield engages and it stands still — a real DrussGT behaviour but not movement. Those captures were rejected; only opponents that make DrussGT surf (SpinBot/RamFire/Crazy/Corners and a DrussGT mirror) are included. DrussGT's source was not modified — the shipped jar is used as-is.
  4. Rounds are concatenated. tick is continuous, so positions jump at round boundaries. Split on the boundaries in drussgt_meta/*.rounds.json (or on impossible displacements) before computing anything displacement-based.
  5. Signed velocity. es/ss may be negative. Reconstruct motion as x += es*cos(radians(eh)); y += es*sin(radians(eh)) — do not use abs(es) with eh unless you also flip eh by 180° for negative ticks.

Reproduce with tools/robocode_fixture_capture/capture.sh; validate/analyse with tools/robocode_fixture_capture/analyze.py.