8e2be6a4c6
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.
583 lines
29 KiB
Markdown
583 lines
29 KiB
Markdown
# 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.
|