# 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 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 `.json` identity file and a `.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.