# Robocode shim spike — 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 is an **API-coverage / architecture spike**, not a working bot. No physics is implemented; the peer's world model is stubbed. --- ## 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 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. Remaining work to go from "compiles" to "runs a battle" Everything below is now the *inverse* problem: the API is complete, but the **engine semantics** behind it are not implemented. | # | Item | Status | Notes | |---|---|---|---| | 1 | Coordinate rotation at the boundary | **[MEASURED]** | Pure function; conversion `trRad = PI/2 - classicRad` already validated to 0.000–0.001° against recorded DrussGT motion (`tools/fixtures/DRUSSGT_FIXTURES.md`). Implemented in `TankRoyaleBridge`. Cheap. | | 2 | Async tick ↔ synchronous `execute()` bridge | **[MEASURED]** | Implemented in `ClassicPeer.provideTurn/awaitExecutedTurns`; proven by the 200-tick smoke run. Real integration still needs a Tank Royale Java websocket/JSON client to *produce* those ticks. | | 3 | `RobocodeFileOutputStream` engine dependency | **[MEASURED]** | `javap -c` shows it resolves `IThreadManagerBase` via `ContainerBase`. Outside the engine it throws `RobotException` and kills the bot thread. Reached only from `DrussGT.contain()`. Fix: register a no-op `IThreadManagerBase` in `ContainerBase`, or replace `contain`'s writer (can't — bot is unmodified), or accept that any bot exception is fatal. This bit us in testing. | | 4 | Classic motion model (`setAhead` distance vs TR speed) | **[MEASURED] + [ESTIMATE]** | DrussGT calls `getDistanceRemaining()` (ours) and `setAhead(d)`; Tank Royale wants a *target speed* + *turn rate*. The bridge must integrate a classic motion model (`ACCELERATION=1`, `DECELERATION=2`, `MAX_VELOCITY=8`, turn rate `10-0.75·|v|`) in front of TR, or approximate. DrussGT *also* has its own `MovePredictor` using classic accel/decel, so divergence is baked in regardless. [ESTIMATE: 1–3 days] | | 5 | Event synthesis & order | **[MEASURED]** | All classic event constructors are public and confirmed by javap. `ClassicPeer.dispatch` already routes ScannedRobot/HitByBullet/BulletHit/BulletHitBullet/BulletMissed/HitRobot/HitWall/RobotDeath/Win/Death/Status/SkippedTurn/Custom. Still to do: emit them from TR events and honour classic priority ordering. | | 6 | `getGunHeat()` / gun cooling | **[MEASURED]** | `DrussMoveGT` tracks enemy gun heat itself from `getGunCoolingRate()` (classic value 0.1). `ClassicPeer` returns a configurable value; the bridge must model *our* heat with the same decrement so `getGunHeat`/`getGunTurnRemaining` are consistent. | | 7 | `Bullet` identity / own-bullet tracking | **[ESTIMATE]** | DrussGT keeps `Bullet` objects from `setFireBullet` and matches them in `onBulletHit`/`HitByBulletEvent.getBullet()` (uses `equals`). The bridge must map TR bullet ids to stable classic `Bullet` instances. | | 8 | Firing translation | **[ESTIMATE]** | `setFire(power)`/`setFireBullet` → TR fire command; `setFireBullet` must return a `Bullet` synchronously. | | 9 | Rounds / counts | **[ESTIMATE]** | `getRoundNum`, `getNumRounds`, `getOthers`, `getNumSentries` map to TR round info; `DrussMoveGT` gates flattener learning on `getRoundNum() > 1 / > 4 / < 15`, so these must be real, not constants. | | 10 | Radar / scan cadence | **[ESTIMATE]** | Classic `ScannedRobotEvent` cadence is a swept radar; TR provides its own scan events. Must be synthesised with correct `bearing`/`distance` so `DrussMoveGT.onScannedRobot` sees true geometry. | | 11 | `onPaint` / AWT | **[MEASURED]** | `ClassicPeer.getGraphics()` returns `null`; DrussGT's `onPaint(Graphics2D)` (157 lines, debug only) can be skipped. `setColors`/`Color` are accepted and ignored. | | 12 | `getAllEvents()` on death | **[ESTIMATE]** | `DrussGT.contain`/`onDeath` replays events; peer already returns the turn's accumulated events. | | 13 | Class-loading topology | **[MEASURED]** | Must load DrussGT with a child `URLClassLoader` whose parent holds `robocode.jar`, so `IBasicRobot` is the same class. Implemented in `BotHost`. | | 14 | Physics fidelity | **[ESTIMATE]** | Even with a perfect bridge, TR's `maxTurn = 10 - 0.75·|speed|`, ±1 ramp and bullet model differ from classic. Exact trajectory match is impossible; aim for "same decisions, diverging trajectories". | There are **no remaining API unknowns**: the compiler resolved everything, and the smoke test exercised the full event → decision → intent path. --- ## 6. Repo layout & how to run ```text tools/robocode_shim/ README.md this report build.sh compile the shim (ClassicPeer/BotHost/Bridge/SmokeTest) compile_drussgt.sh ACCEPTANCE TEST: javac the real DrussGT sources run_smoke.sh load + drive the real jar, assert intents .gitignore out/ and *.jar never committed src/robocode_shim/ ClassicPeer.java IAdvancedRobotPeer impl + execute() tick bridge BotHost.java URLClassLoader + setPeer + run() thread TankRoyaleBridge.java angle conversion + documented tick contract (skeleton) SmokeTest.java the runtime proof out/ build output (gitignored) ``` ```bash export ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar export DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar ./build.sh ./compile_drussgt.sh ./run_smoke.sh ``` ## 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` and `DrussGT.jar` at `/tmp/drussgt/DrussGT.jar`; the scripts reference them via `ROBOCODE_JAR` / `DRUSSGT_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.