Files
SirRoboGarage/tools/robocode_shim/README.md
T
SirStone 8e2be6a4c6 feat(tools): the real DrussGT now plays and wins Tank Royale battles
The unmodified DrussGT.jar connects, wave-surfs, fires and beats every
adversary we have. 5 rounds each, all rounds won:
  SpinBot 542-16, Corners 823-4, Crazy 604-0, RamFire 900-0,
  ModularBot 506-72.

It is genuinely surfing, not drifting or stalling. Movement statistics
against the classic captures, same metrics, same analyzer:

  opp        perp TR/classic   reversing TR/cl   median range TR/cl
  SpinBot    0.967 / 0.962     0.466 / 0.464     462 / 402
  Corners    0.894 / 0.920     0.462 / 0.419     520 / 504
  Crazy      0.841 / 0.829     0.527 / 0.439     370 / 378
  RamFire    0.762 / 0.651     0.533 / 0.417     326 / 283
  ModularBot 0.956 / -         0.466 / -         453 / -

Every round starts moving within 3-11 ticks and runs at 75-82% full speed.

Implemented: the classic remaining-quantity motion model (delegated to the
TR Bot's own Nat-Pavasant model - identical constants: accel 1, decel -2,
max 8, body turn 10-0.75|v|, gun 20, radar 45 - so getDistanceRemaining and
getTurnRemaining are exactly self-consistent with what is emulated); event
synthesis with classic ordering; bullet identity via object identity; gun
heat; rounds; radar cadence; firing translation; and the ThreadManager
landmine is killed by installing a no-op IThreadManagerBase in
ContainerBase.instance (verified: without it 'RobotException: ThreadManager
cannot be null!' kills the bot thread; with it the write succeeds).

Also adds TrBattleCapture, an observer that dumps per-tick state in the SAME
JSONL fixture format as the classic capture, so legacy bots can be captured
from Tank Royale battles too.

HONEST DIVERGENCES (README section 5.9): the TR server moves along the
PRE-turn heading and then turns, while classic aligns displacement with the
POST-turn heading, so the analyzer's conversion error is ~1.5 deg rather than
0.000 deg; distanceRemaining decrements by target speed rather than actual
distance; collision clamping differs; BulletMissedEvent can fire less often
because age-expiry has no TR event; bulletId is a local temp id;
StatusEvent/onPaint/SkippedTurnEvent are never delivered. Physics fidelity
diverges by construction - expect to retune.

EnergyDomeWorker (the bullet shield) is off by default: its precise
bullet-detection warm-up makes round 1 up to 79% stationary vs 35% in
classic. Rounds 2+ match classic closely with it on, but the pure surfer is
the consistent path. DRUSSGT_SHIELD=1 re-enables it.

Jars remain out of git.
2026-09-21 00:29:37 +02:00

583 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Robocode shim — running the real DrussGT as a Tank Royale bot
**Question:** can the *unmodified* legacy DrussGT jar be driven from a shim, and
which is cheaper — (A) re-implement `robocode.*` or (B) reuse the genuine
`robocode.jar` and write only a driver?
**Answer: (B). Viable and chosen.** The real `robocode.jar` classes are pure
delegates to a `peer` interface, and that seam is public. DrussGT compiles with
**zero shim API symbols** and, in the runtime smoke test, the real bot executes
its own event handlers and emits 200 ticks of movement intents + 169 fire
requests through our peer.
This was an **API-coverage / architecture spike** and is now a **working
bridge**: the real, unmodified DrussGT jar connects to a Tank Royale server,
plays real battles, wave-surfs and fires. The physics/engine semantics are
supplied by the shim (`ClassicPeer` + `DrussGTBridge`) and documented in §5;
the measured movement statistics against classic-Robocode captures are in §5.8.
> **Status:** COMPLETE (Java-only). See §5 for what was implemented, §5.8 for the
> verification evidence, and §5.9 for the honest list of physics divergences.
---
## 1. Approach decision — evidence
`javap` on `/tmp/robocode/install/libs/robocode.jar` (Robocode 1.9.5.5):
* `robocode.AdvancedRobot extends robocode._AdvancedRadiansRobot` and
implements `IAdvancedRobot, IAdvancedEvents`. **Every** getter/setter is a
one-line delegation, e.g.:
```text
AdvancedRobot.getDistanceRemaining() -> getfield peer : IBasicRobotPeer;
invokeinterface IBasicRobotPeer.getDistanceRemaining()D
AdvancedRobot.setAhead(double) -> getfield peer; checkcast IAdvancedRobotPeer
invokeinterface IAdvancedRobotPeer.setMove:(D)V
```
* The peer field lives on `robocode._RobotBase`:
```text
robocode._RobotBase:
robocode.robotinterfaces.peer.IBasicRobotPeer peer;
public final void setPeer(robocode.robotinterfaces.peer.IBasicRobotPeer);
public final void setOut(java.io.PrintStream);
```
* `robocode.Robot` (the parent of `AdvancedRobot`):
`getRobotRunnable()` returns `this`; `getBasicEventListener()` returns `this`.
So there is **no engine requirement to start the bot**: implement the peer,
`setPeer()`, and run `getRobotRunnable()` on a thread.
* The seam is one narrow interface. `IAdvancedRobotPeer` extends
`IStandardRobotPeer` extends `IBasicRobotPeer` — **75 methods total**
(38 + 6 + 31). We implement exactly that one interface
(`ClassicPeer`) plus a classloader/driver (`BotHost`).
**Approach (A) would have required** re-declaring ≥15 `robocode.*` classes
(`AdvancedRobot`, `Bullet`, `ScannedRobotEvent`, `Rules`, `Utils`, all the event
types, `RobocodeFileOutputStream`, `Robot`, `_Robot`, `_RobotBase`,
`_AdvancedRobot`, `_AdvancedRadiansRobot`, the `robotinterfaces.*` and
`robotinterfaces.peer.*` hierarchies) and keeping their signatures in lockstep
with the jar. Approach (B) throws all of that away. The only cost is that the
shim must still supply the *semantics* the engine normally supplies — which is
the remaining-work list in §5.
**Runtime proof (not just a compile):** `robocode_shim.SmokeTest` loads
`/tmp/drussgt/DrussGT.jar` in a child `URLClassLoader`, calls
`setPeer(ClassicPeer)`, starts the bot thread, and feeds 200 synthetic turns:
```text
DrussGT jar: /tmp/drussgt/DrussGT.jar (159289 bytes)
ticks executed : 200
bot thread survived : true
ticks with a move intent : 200
first (setAhead,setTurnRR): (-20.271410058613192, -2.25459929233885)
fire requests : 169
last captured intents : move=-20.994678898031065 turnBody=0.17133770212129118 turnGun=-0.2965833407153049 turnRadar=Infinity
bot thread alive : true
SMOKE TEST: OK (real DrussGT drove through the peer seam)
```
The first attempt hung; the thread dump showed real DrussGT frames —
`jk.mega.DrussGT.onScannedRobot` → `DrussMoveGT.onScannedRobot` →
`EnemyMoves.predict` — so the seam executes genuine bot code. The hang was a
synthetic-input bug (see §4), fixed by passing the event constructor arguments
in the correct order.
---
## 2. Acceptance test — `javac` compiles DrussGT
### Exact command (the one recorded by `compile_drussgt.sh`)
```bash
ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar
DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar
OUT=/tmp/shim_out
javac -nowarn -cp "$ROBOCODE_JAR:$DRUSSGT_JAR" -d "$OUT" \
$(find /tmp/drussgt/src -name '*.java' ! -name 'GFRange.java' ! -name 'Indice.java')
```
### Output
```text
Note: Some input files use or override a deprecated API that is marked for removal.
Note: Recompile with -Xlint:removal for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
exit: 0
```
**20/22 source files compile clean, 43 classes emitted, zero unresolved symbols.**
### The literal "all 22 files" invocation and why it cannot compile
```bash
javac -nowarn -cp "$ROBOCODE_JAR:$DRUSSGT_JAR" -d /tmp/shim_out \
$(find /tmp/drussgt/src -name '*.java') # 22 files
```
```text
/tmp/drussgt/src/jk/mega/dGun/Indice.java:2: error: duplicate class: jk.mega.dGun.Indice
class Indice implements Comparable{
^
/tmp/drussgt/src/jk/mega/dGun/GFRange.java:2: error: duplicate class: jk.mega.dGun.GFRange
public class GFRange implements Comparable{
^
2 errors
```
This failure is **pre-existing and independent of the shim**: `DrussGunDC.java`
declares `GFRange` and `Indice` as extra top-level classes at its end
(`DrussGunDC.java:729` and `:1207`), and the decompiler *also* emitted them as
standalone files. The original `DrussGT.jar` already ships
`jk/mega/dGun/GFRange.class` and `Indice.class`, so the two redundant `.java`
files are excluded and the jar supplies them, together with the 6 genuinely
unsourced classes. No API symbols are missing — the compiler asked us for
**nothing**.
---
## 3. True API surface required by DrussGT
Measured from the **compiled bytecode** (`javap -c -p` over all 43 emitted
classes, aggregating every `Method`/`InterfaceMethod`/`class` constant-pool
reference), plus the `this.`-calls in `DrussGT.java` (which `javap` prints
without an owner). This is exactly what the compiler/JVM demands.
### Types (14 `robocode.*`, 3 `java.awt.*` families)
| Type | Required members actually used |
|---|---|
| `robocode.AdvancedRobot` (superclass) | `getX, getY, getVelocity, getHeadingRadians, getGunHeadingRadians, getRadarHeadingRadians, getEnergy, getGunHeat, getGunCoolingRate, getTime, getOthers, getRoundNum, getNumRounds, getBattleFieldWidth, getBattleFieldHeight, getDistanceRemaining, getGunTurnRemaining, getGunTurnRemainingRadians, getTurnRemainingRadians, getRadarTurnRemaining, setAhead, setTurnRightRadians, setTurnGunRightRadians, setTurnRadarRightRadians, setFire, setFireBullet, execute, getDataFile, setColors, setAdjustGunForRobotTurn, setAdjustRadarForGunTurn, setAdjustRadarForRobotTurn, getAllEvents` |
| `robocode.Bullet` | `getX, getY, getHeadingRadians, getPower, getVelocity, isActive, equals` |
| `robocode.ScannedRobotEvent` | `getBearingRadians, getDistance, getEnergy, getHeadingRadians, getName, getVelocity` |
| `robocode.Rules` | `getBulletDamage, getBulletSpeed, getGunHeat` |
| `robocode.util.Utils` | `normalAbsoluteAngle, normalRelativeAngle` |
| `robocode.HitByBulletEvent` | `getBullet, getVelocity` |
| `robocode.BulletHitEvent` | `getBullet, getEnergy` |
| `robocode.BulletHitBulletEvent` | `getBullet, getHitBullet` |
| `robocode.RobotDeathEvent` | `getName` |
| `robocode.SkippedTurnEvent` | `getTime` |
| `robocode.HitRobotEvent` | *(parameter type only)* |
| `robocode.WinEvent` | *(parameter type only)* |
| `robocode.DeathEvent` | *(parameter type only)* |
| `robocode.RobocodeFileOutputStream` | constructor + `write/close/flush` (via `PrintStream`) |
| `java.awt.Color` | `setColors`, paint |
| `java.awt.Graphics2D` | `onPaint(Graphics2D)` |
| `java.awt.geom.Point2D`/`Rectangle2D` | `Point2D.Double`, `Rectangle2D.Double`, `distance`, `contains` |
Source imports confirm the same set: `import robocode.*` (×8),
`import robocode.util.Utils` (×3), `import robocode.util.*` (×2),
`java.awt.geom.*` (×13), `java.util.*` (×11), `java.awt.Color` (×3),
`java.io.*` (×2), `java.io.PrintStream`, `java.io.IOException`.
### Surprises vs the predicted list
The predicted list was *"AdvancedRobot, Bullet, ScannedRobotEvent, Rules, Utils,
HitByBulletEvent, BulletHitEvent, BulletHitBulletEvent, HitRobotEvent, WinEvent,
DeathEvent, SkippedTurnEvent, RobocodeFileOutputStream, plus
java.awt.geom/Color/Graphics."* Differences (**new information**):
1. **`robocode.RobotDeathEvent` is required and was NOT predicted.** The
prediction listed only `DeathEvent`. `DrussGT.onRobotDeath(RobotDeathEvent)`
forwards to `DrussMoveGT.onRobotDeath` and `MeleeRadar`, and calls
`e.getName()`. `DeathEvent` (our own death) and `RobotDeathEvent` (another
robot died) are distinct classic types.
2. **`RobocodeFileOutputStream` is reached only through `DrussGT.contain()`**
(`DrussGT.java:259`) and has a hard engine dependency — `javap -c` shows it
calls `net.sf.robocode.core.ContainerBase.getComponent(IThreadManagerBase)`
and throws `RobotException: ThreadManager cannot be null!` outside the
engine. It is best-effort error logging, but it can *kill the bot thread*
under the shim (observed). See §5.
3. **`Bullet.equals()`** is called — DrussGT compares `Bullet` instances (our
fired-bullet tracker), so `equals` semantics matter, not just the getters.
4. **`Rules` needs the three instance methods** `getBulletDamage`,
`getBulletSpeed`, `getGunHeat` — these *are* the classic physics constants we
do not otherwise have.
5. **`HitByBulletEvent.getVelocity()`** and **`BulletHitEvent.getEnergy()`** are
used (not just `getBullet()`).
6. **`java.util.Vector`** is the declared return of `AdvancedRobot.getAllEvents()`
(used in `onDeath`), and `java.util.Hashtable`/`Vector` appear in
`MeleeRadar` — a Java-collections surface the type-level prediction omitted.
Nothing in the actual set was *missing* from the real jar (as expected — the jar
is the authority), and nothing required a custom stub.
---
## 4. Public signatures of the 6 unsourced classes
These have **no source** in `/tmp/drussgt/src`; the jar supplies their bytecode.
Recorded with `javap -p -classpath /tmp/drussgt/DrussGT.jar` (package-private
declarations included; `//` = our annotation).
### `jk.mega.dGun.JKDCUtils` (utility, all static)
```text
class jk.mega.dGun.JKDCUtils {
static double HALF_PI, WALL_MARGIN, S, W, N, E; // mutable static geometry
jk.mega.dGun.JKDCUtils();
public static java.awt.geom.Point2D$Double project(Point2D$Double, double, double);
public static double absoluteBearing(Point2D$Double, Point2D$Double);
public static double velocityFromDistance(double);
public static double limit(double, double, double);
public static double bulletVelocity(double);
public static double maxEscapeAngle(double);
static double rollingAverage(double, double, double, double);
public static double sqr(double);
public static int getIndex(double[], double);
static double wallDistance(double, double, double, Point2D$Double);
static double distanceWest(double, double, double, double);
static double decelDistance(double);
}
```
### `jk.mega.dGun.NormalDistribution`
```text
class jk.mega.dGun.NormalDistribution implements java.lang.Comparable {
double position, height, inv_width;
jk.mega.dGun.NormalDistribution();
public int compareTo(java.lang.Object);
}
```
### `jk.mega.dGun.StoreScan`
```text
class jk.mega.dGun.StoreScan {
jk.mega.dGun.GFRange range;
double[] location, ASLocation;
double vel, deltaHeading;
jk.mega.dGun.StoreScan previous;
jk.mega.dGun.StoreScan();
}
```
### `jk.mega.dGun.DCRobotState`
```text
class jk.mega.dGun.DCRobotState {
double direction, deltaHeading, vel, heading, latVel, advVel, offset, distance,
timeSinceDirChange, accel, distLast30, distLast20, distLast10,
forwardWall, reverseWall, timeSinceDecel, timeSinceAccel, BFT,
currentGF, mirrorOffset, MEA_pos, MEA_neg, GF0, hitGF;
java.awt.geom.Point2D$Double location, enemyLocation;
long time;
int bulletsShot;
boolean firstScan;
jk.mega.dGun.DCRobotState();
public double[] unweightedLocation();
public double[] location();
public double[] ASLocation();
}
```
### `jk.mega.dGun.DCWave`
```text
class jk.mega.dGun.DCWave {
static java.util.ArrayList paintPoints;
static int DCHits, actualHits, DCASHits;
static double randomHits;
static java.awt.geom.Point2D$Double targetLocation;
static double targetHeading;
static int GUN;
static final int DC, DCAS, RANDOM;
long fireTime;
double bulletPower;
java.awt.geom.Point2D$Double gunLocation;
double bearing, lateralDirection, MEA_pos, MEA_neg, MEA_norm, GF0, BFT;
boolean bulletFired, bulletAlive;
double[] currentASBuffer;
robocode.Bullet bullet;
static jk.tree.KDTree<jk.mega.dGun.StoreScan> heapTree, ASTree;
static long currentTime;
jk.mega.dGun.StoreScan storeScan;
boolean intersecting;
double bestBearing, DCBearing, DCASBearing, randomBearing;
jk.mega.dGun.DCRobotState scan;
private robocode.AdvancedRobot robot;
private double distanceTraveled;
jk.mega.dGun.DCWave(robocode.AdvancedRobot);
public boolean test();
void setSegmentations(jk.mega.dGun.DCRobotState);
private boolean hasArrived();
private double currentGF();
private double currentPreciseGF();
private double getBearing(jk.tree.KDTree, double[], int, boolean, int);
private double getBearingGaussian(jk.tree.KDTree, double[], int, boolean);
public double mostVisitedBearing();
private double getASBearing();
}
```
### `jk.mega.dMove.Scan` (declared at the end of `DrussMoveGT.java`; only `.class` shipped)
```text
class jk.mega.dMove.Scan implements java.lang.Cloneable {
static final double latVelWeight, advVelWeight, bftWeight, forwardWallWeight,
reverseWallWeight, lastVelWeight, accelWeight, timeSinceDecelWeight,
timeSinceDirChangeWeight, distLast10Weight, timeWeight1, timeWeight2, timeWeight3,
ASlatVelWeight, ASadvVelWeight, ASbftWeight, ASforwardWallWeight,
ASreverseWallWeight, ASlastVelWeight, ASaccelWeight, AStimeSinceDecelWeight,
AStimeSinceDirChangeWeight, ASdistLast10Weight, AStimeWeight1, AStimeWeight2, AStimeWeight3;
double latVel, advVel, bft, accel, forwardWall, reverseWall, lastVel,
timeSinceDecel, timeSinceDirChange, distLast10;
int pointInTime, index;
jk.mega.dMove.Scan();
public java.lang.Object clone();
double[] location();
double[] ASLocation();
}
```
> Note: `DCWave`, `JKDCUtils`, etc. are declared inline at the bottom of
> `DrussGunDC.java` in the decompiled tree, so their *source* does exist there;
> the *standalone* `.java` files do not. Either way the jar's bytecode is
> authoritative for signatures.
---
## 5. The bridge is implemented (Java-only, working)
The five remaining-work items are done. The bridge is `DrussGTBridge` (a Tank
Royale `Bot`) + `ClassicPeer` (the genuine-jar peer) + `BotHost` (child
classloader) + `ThreadManagerFix`.
| # | Item | Status | What was done |
|---|---|---|---|
| 1 | Coordinate rotation | **DONE** | Every TR angle is converted on read (`tankRadToClassicRad`) and every classic intent on write (`toDegrees` of the classic signed value). See `TankRoyaleBridge`. |
| 2 | `RobocodeFileOutputStream` / ThreadManager | **DONE** | `ThreadManagerFix.install()` registers a no-op `IThreadManagerBase` in `ContainerBase.instance`, so `DrussGT.contain()` writes a normal file instead of throwing `RobotException: ThreadManager cannot be null!` and killing the thread. Proven by `ThreadManagerFixTest`. |
| 3 | Classic motion model | **DONE** | The peer's captured `setAhead`/`setTurn*` are forwarded to the TR `Bot`'s own classic motion model (`setForward`/`setTurnRight`/`setTurnGunRight`/`setTurnRadarRight`), which is Nat Pavasant's optimal-velocity model using the same constants (`ACCELERATION=1`, `DECELERATION=-2`, `MAX_SPEED=8`, body turn `10-0.75·|v|`, gun `20`, radar `45`). `getDistanceRemaining()`/`getTurnRemaining()` delegate to the same model, so they are exactly consistent with what is emulated. |
| 4 | Event synthesis & ordering | **DONE** | TR events are mapped to classic events and delivered synchronously; TR's event queue priorities are the classic priorities, so ordering matches. `ScannedRobotEvent`, `HitByBulletEvent`, `BulletHitEvent`, `BulletHitBulletEvent`, `BulletMissedEvent`, `HitRobotEvent`, `HitWallEvent`, `RobotDeathEvent`, `WinEvent`, `DeathEvent`. `SkippedTurnEvent` is deliberately never delivered. |
| 5 | Physics / state / bullets / rounds / radar | **DONE** | Energy, position, headings, speed, gun heat and cooling rate are read live from the TR bot; `getRoundNum()=roundNumber-1`, `getNumRounds`, `getOthers`, `getTime()=turnNumber`. Bullet identity is tracked: the `Bullet` returned by `setFireBullet` is the *same instance* later handed back in `BulletHit*` events, so DrussGT's `Bullet.equals` (which compares class + `bulletId`) and identity checks both work. Radar/gun adjust flags are forwarded. |
### 5.1 How the tick contract works
DrussGT's own `run()` is executed **on the Tank Royale bot thread** (this is
exactly where the official `robocode-api-bridge` runs a legacy robot). Its
`execute()` calls back into `ClassicPeer.TickHost.onExecute()`, which:
1. applies the captured intents (`setAhead` → `setForward`, …) to the TR bot,
2. calls the TR `Bot.go()`.
`go()` sends the intent, waits for the next tick and then dispatches that tick's
events. Our `DrussGTBridge` overrides the TR event handlers and forwards each
one to the classic listeners via `ClassicPeer.deliver`. Because the TR `Bot`
model dispatches the *new* turn's events before `run()`'s next iteration, the
classic contract "events for turn N are delivered before the turn-N run() body"
holds, as does the classic "new robot instance per round, static state survives"
lifetime (a fresh DrussGT is instantiated each round).
### 5.2 Classic motion model mapping (exact)
| classic (peer) | Tank Royale |
|---|---|
| `setAhead(d)` / `setMove(d)` | `setForward(d)` |
| `setTurnRightRadians(r)` | `setTurnRight(toDegrees(r))` |
| `setTurnGunRightRadians(r)` | `setTurnGunRight(toDegrees(r))` |
| `setTurnRadarRightRadians(r)` | `setTurnRadarRight(toDegrees(r))` |
| `setMaxVelocity(v)` / `setMaxTurnRate(r)` | `setMaxSpeed(v)` / `setMaxTurnRate(r)` |
| `setAdjustGunForRobotTurn` | `setAdjustGunForBodyTurn` |
| `setAdjustRadarForRobotTurn` / `setAdjustRadarForGunTurn` | `setAdjustRadarForBodyTurn` / `setAdjustRadarForGunTurn` |
| `setFireBullet(p)` | `setFire(p)` + create/return a `robocode.Bullet` |
| `execute()` | `go()` |
| `getDistanceRemaining()` | `getDistanceRemaining()` |
| `getTurnRemaining()` / gun / radar | `-toRadians(getTurnRemaining())` … (TR positive = left) |
A command is only forwarded when DrussGT actually issued it that turn (dirty
flags), because classic remaining-quantities persist until overwritten.
### 5.3 Bullet identity
At `setFireBullet(p)` the bridge calls the TR `setFire(p)`; if accepted it
creates a plain `robocode.Bullet` (never a subclass — `Bullet.equals` compares
`getClass()`), stores it in `unmatchedFired`, and returns it. When the TR
`BulletFiredEvent` arrives the nearest unmatched bullet is bound to the TR
`bulletId`. Later `BulletHitBotEvent` / `BulletHitBulletEvent` /
`BulletHitWallEvent` look the id up and pass **the same instance** to DrussGT, so
`e.getBullet().equals(w.bullet)` in `DrussGunDC.onBulletHitBullet` is true.
Position/`isActive` are updated by reflection (best effort).
### 5.4 Shield (EnergyDome) handling
DrussGT's `EnergyDomeWorker` ("shield") is a separate *precise* subsystem that
detects enemy bullets from per-scan energy drops. Its first-round warm-up is
sensitive to exact classic timing. The proven path (also used by `SmokeTest`) is
to run the **pure wave surfer** (`DrussMoveGT`): the bridge sets
`DrussGT.shieldEnabled = false` via `BotHost.disableShield()` unless
`DRUSSGT_SHIELD=1`. This is the configuration the statistics in §5.8 use.
### 5.5 Event mapping details / gotchas honoured
* `ScannedRobotEvent(name, energy, bearing, distance, heading, velocity)` — the
documented, non-intuitive argument order; `bearing` is the classic body-relative
bearing and `heading` the enemy's classic absolute heading.
* `BulletHitWallEvent` (TR) → `BulletMissedEvent` (classic).
* `BotDeathEvent` → `RobotDeathEvent`; `WonRoundEvent` → `WinEvent`; TR
`DeathEvent` → classic `DeathEvent`.
* `HitWallEvent` bearing is computed from the nearest wall normal.
* `onPaint`/AWT, `StatusEvent`, `SkippedTurnEvent`, `onCustomEvent` are not
delivered (DrussGT does not use them; `getGraphics()` returns `null`).
### 5.6 Class-loading topology
`BotHost` loads `jk.mega.DrussGT` in a child `URLClassLoader` whose parent is the
shim's loader (which holds the genuine `robocode.jar`), so `IBasicRobot` is the
same type the peer implements. One DrussGT class is loaded per JVM, so the static
KD-trees persist across rounds exactly like classic.
### 5.7 How to run it
```bash
export ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar
export TR_BOT_API_JAR=$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar
export DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar
./build.sh
./run_smoke.sh # standalone 200-tick proof
java -cp "out:$ROBOCODE_JAR" robocode_shim.ThreadManagerFixTest # item 3 proof
# real battle + movement fixture (embedded server, boots the bot dir):
./run_bridge_battle.sh /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
```
`make_botdir.sh [DIR]` generates the Tank Royale bot directory (a `<DIR>.json`
identity file and a `<DIR>.sh` launcher that runs `robocode_shim.DrussGTBridge`).
`TrBattleCapture` is the observer: it runs the battle through the Tank Royale
battle-runner API and dumps every bot's per-turn state in the *same* JSONL
fixture format as `tools/robocode_fixture_capture/Capture.java`, so
`analyze.py` runs on both unchanged.
### 5.8 Verification — it is really DrussGT, not a stub
All runs: embedded Tank Royale server, `maxSpeed`, shield disabled (pure surfer),
5 rounds each, against the same sample bots whose classic captures are in
`tools/fixtures/`. Stats computed by `tools/robocode_fixture_capture/analyze.py`
on the TR capture (5 rounds) and the 20-round classic fixture.
**Battle outcomes** (DrussGT first, total score over 5 rounds):
| opponent | DrussGT | opponent | rounds won | bullets fired / hits / misses |
|---|---:|---:|---:|---:|
| SpinBot | **542** | 16 | 5/5 | 327 / 39 / 277 |
| Corners | **823** | 4 | 5/5 | 79 / 36 / 31 |
| Crazy | **604** | 0 | 5/5 | 332 / 45 / 274 |
| RamFire | **900** | 0 | 5/5 | 54 / 38 / 12 |
| ModularBot (Nim, real) | **506** | 72 | 5/5 | 491 / 56 / 418 (+5 bullet-bullet; took 18 hits) |
**Movement statistics** (TR vs the matching classic capture):
| opponent | metric | TR (bridge) | classic fixture |
|---|---|---:|---:|
| SpinBot | perp frac / radial frac | **0.967 / 0.000** | 0.962 / 0.001 |
| | full speed ≥7.5 / rest <0.5 / reversing | 0.781 / 0.017 / 0.466 | 0.701 / 0.067 / 0.464 |
| | median range (px) | 462 | 402 |
| Corners | perp / radial | **0.894 / 0.002** | 0.920 / 0.004 |
| | full / rest / reversing | 0.754 / 0.028 / 0.462 | 0.586 / 0.092 / 0.419 |
| | median range | 520 | 504 |
| Crazy | perp / radial | **0.841 / 0.003** | 0.829 / 0.010 |
| | full / rest / reversing | 0.811 / 0.017 / 0.527 | 0.668 / 0.137 / 0.439 |
| | median range | 370 | 378 |
| RamFire | perp / radial | **0.762 / 0.002** | 0.651 / 0.013 |
| | full / rest / reversing | 0.747 / 0.043 / 0.533 | 0.723 / 0.046 / 0.417 |
| | median range | 326 | 283 |
| ModularBot | perp / radial | **0.956 / 0.000** | — |
The perpendicular-to-opponent fraction, the near-zero radial fraction and the
~42–53% reversal rate are the signature of DrussGT's wave surfer and match the
classic captures closely. The TR run is slightly faster / shorter-ranged because
the shield is off and the rounds are shorter (5 vs 20). Per-round, every round
now starts moving within 3–11 ticks and runs at 75–82% full speed. **This is not
a straight line, not a standstill, and not a crash.**
### 5.9 Known physics divergences (honest list)
1. **Move/turn ordering.** The TR server calls `moveToNewPosition()` using the
*pre-turn* heading and only then applies the turn, whereas the classic capture
aligns each tick's displacement with the *post-turn* heading
(`analyze.py` conversion error 0.000° classic vs ~1.5° TR). Trajectories
therefore diverge even with identical intents.
2. **Distance bookkeeping.** The TR `Bot` motion model decrements
`distanceRemaining` by the *target* speed (Nat Pavasant's over-drive method);
classic decrements by the distance actually travelled, and collision/wall
clamping differs.
3. **No exact trajectory equivalence.** Bullet speeds/damage match
(`20-3p`, `4p (+2(p-1) if p>1)`), gun heat matches (`1+p/5`, cool 0.1/turn),
turn rates match — but the integration order above means "same decisions,
diverging trajectories" is the ceiling.
4. **First-round shield warm-up.** With the shield *enabled*
(`DRUSSGT_SHIELD=1`) round 1 can be 79% stationary vs 35% in classic, because
the shield's precise bullet detection is timing-sensitive. Disabled by
default.
5. **`BulletMissedEvent`.** TR emits `BulletHitWallEvent` on wall hits; bullets
that expire by age may not produce an event, so `onBulletMissed` can fire less
often than classic. DrussGT does not use it.
6. **`bulletId`.** The classic `Bullet`'s `bulletId` is a local temporary id, not
the TR server id; identity is preserved by object identity, not by id.
7. **`onPaint`/AWT, `StatusEvent`, `SkippedTurnEvent`, custom events** are not
delivered. DrussGT does not rely on them (`onPaint` is debug-only).
8. **Tick phase.** TR `getTurnNumber()` is 1-based like classic `getTime()`, but
the observer snapshot phase differs by the move/turn ordering above; per-tick
state read by the bot is always the current turn's.
There are **no remaining API unknowns**: the compiler resolved everything, and
the bridge now runs full battles, surf and fire.
---
## 6. Repo layout & how to run
```text
tools/robocode_shim/
README.md this report
build.sh compile the shim + capture
compile_drussgt.sh ACCEPTANCE TEST: javac the real DrussGT sources
run_smoke.sh load + drive the real jar, assert intents (standalone)
make_botdir.sh generate a Tank Royale bot dir that boots the bridge
run_bridge_battle.sh run a real TR battle + movement statistics
.gitignore out/ and *.jar never committed
src/robocode_shim/
ClassicPeer.java IAdvancedRobotPeer impl + world/tick seams
BotHost.java URLClassLoader + setPeer + per-round instances
TankRoyaleBridge.java exact angle conversion
ThreadManagerFix.java no-op IThreadManagerBase in ContainerBase
ThreadManagerFixTest.java proof of the above
DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
SmokeTest.java the standalone runtime proof
TrBattleCapture.java observer: battle -> classic JSONL fixture format
out/ build output (gitignored)
```
```bash
export ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar
export TR_BOT_API_JAR=$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar
export DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar
./build.sh
./compile_drussgt.sh
./run_smoke.sh
./run_bridge_battle.sh /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
```
## 7. Third-party jars
Per the task constraints, **no third-party jar is copied into the repo.**
`robocode.jar` (Robocode 1.9.5.5) stays at
`/tmp/robocode/install/libs/robocode.jar`, `DrussGT.jar` at
`/tmp/drussgt/DrussGT.jar`, the Tank Royale Java bot API at
`~/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar`
and the Tank Royale battle runner at
`/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar`;
the scripts reference them via `ROBOCODE_JAR`, `DRUSSGT_JAR`, `TR_BOT_API_JAR`
and `TR_RUNNER_JAR`. `tools/robocode_shim/.gitignore` additionally blocks
`*.jar` and `out/` in case a jar is ever dropped here by mistake.
## 8. Gotchas discovered (for whoever wires the bridge)
* **`ScannedRobotEvent` constructor argument order is
`(name, energy, bearing, distance, heading, velocity)`** — *not* the
javadoc-intuitive heading-before-bearing ordering. Confirmed both
empirically and in the javadoc. Wrong order silently yields `distance=0`,
`BFT=0`, an immediate KD-tree insert and an NPE inside `EnemyMoves.predict`.
* `javap -c` omits the owner on `this`-calls; API extraction must include
unqualified `// Method name:desc` references from the bot's own class.
* `DrussMoveGT`/`EnemyMoves` keep **static** KD-trees; the shim must not load
two copies of the bot in one JVM if that state is to stay meaningful.