17c99f542f
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.
181 lines
9.9 KiB
Markdown
181 lines
9.9 KiB
Markdown
# 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`](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:
|
||
|
||
```java
|
||
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`.
|