There are no genuinely competitive adversaries for Tank Royale, and the in-repo ones were broken until recently. Classic Robocode 1.9.5.5 is obtainable (SourceForge, 20.4 MB) and its programmatic control API (robocode.control.RobocodeEngine + BattleAdaptor.onTurnEnded) can run real battles headless and expose per-turn robot state. So a legacy leader bot's MOVEMENT can be captured and used as a gun-testing fixture with no port. Captured unmodified DrussGT 3.1.4159 vs spinbot/ramfire/crazy/corners and a mirror match: 28,797 ticks, plus two trivial-bot contrasts. Conversion to the Tank Royale convention is validated to 0.000-0.001 deg by recomputing the direction implied by (heading, speed) and comparing it against the recorded per-tick displacement -- i.e. the data is proven to be genuine recorded motion rather than a mangled export. (A first attempt treated the snapshot API's headings as degrees; they are radians, ~95 deg off.) The statistics confirm it is really a wave surfer: perpendicular to the opponent 65-96% of ticks, radial ~0.001, 41-72% of ticks at full speed, reversing on 42-46% of ticks, holding range at a 283-526 px median. The straight-line contrast is radial-dominant (0.75) with ZERO reversals. Discovery: DrussGT detects predictable guns and switches to a bullet-shield stand-still mode, so captures against sample.Walls/TrackFire had to be rejected as non-movement. CAVEATS, recorded in DRUSSGT_FIXTURES.md: these are open-loop (replayed DrussGT never dodges OUR bullets) and perfect-information (the observer gives true positions every tick, unlike our stale live WorldState). Both make our guns look better than in live play, so use them for RELATIVE gun ranking, not absolute hit rates. Jars stay out of git; capture tooling is reproducible via capture.sh.
9.5 KiB
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.
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
BattleAdaptorreceivesonTurnEnded(TurnEndedEvent)every turn. event.getTurnSnapshot().getRobots()yieldsIRobotSnapshot[]withgetX(),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 indrussgt_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)
- 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.
- PERFECT INFORMATION. Classic Robocode's battle observer reports true
positions every turn. Our live bot's
WorldStateis 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. - 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. - Rounds are concatenated.
tickis continuous, so positions jump at round boundaries. Split on the boundaries indrussgt_meta/*.rounds.json(or on impossible displacements) before computing anything displacement-based. - Signed velocity.
es/ssmay be negative. Reconstruct motion asx += es*cos(radians(eh)); y += es*sin(radians(eh))— do not useabs(es)withehunless you also flipehby 180° for negative ticks.
Reproduce with tools/robocode_fixture_capture/capture.sh; validate/analyse with
tools/robocode_fixture_capture/analyze.py.