diff --git a/tools/robocode_shim/README.md b/tools/robocode_shim/README.md index e6ec8f2..f3663bc 100644 --- a/tools/robocode_shim/README.md +++ b/tools/robocode_shim/README.md @@ -1,4 +1,4 @@ -# Robocode shim spike — running the real DrussGT as a Tank Royale bot +# 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 @@ -10,8 +10,14 @@ delegates to a `peer` interface, and that seam is public. DrussGT compiles with 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. +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. --- @@ -330,30 +336,190 @@ class jk.mega.dMove.Scan implements java.lang.Cloneable { --- -## 5. Remaining work to go from "compiles" to "runs a battle" +## 5. The bridge is implemented (Java-only, working) -Everything below is now the *inverse* problem: the API is complete, but the -**engine semantics** behind it are not implemented. +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 | Notes | +| # | Item | Status | What was done | |---|---|---|---| -| 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". | +| 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 `
Two modes: + *
It is a {@link Bot} (the TR API's classic-movement subclass) so that the + * peer's classic intents can be forwarded one-to-one onto the TR Bot API's own + * classic motion model: + * + *
+ * classic (peer) -> Tank Royale Bot API + * setAhead(d) setForward(d) + * setTurnRightRadians(r) setTurnRight(toDegrees(r)) + * setTurnGunRightRadians setTurnGunRight(toDegrees(r)) + * setTurnRadarRightRadians setTurnRadarRight(toDegrees(r)) + * setFireBullet(p) setFire(p) + bullet-identity tracking + * execute() go() + *+ * + *
DrussGT's own {@code run()} is executed on the Tank Royale bot thread
+ * (exactly where the official robocode-api-bridge runs a legacy robot), and its
+ * {@code execute()} calls {@code go()} through {@link ClassicPeer.TickHost}.
+ * TR events are synthesised into classic events and delivered synchronously, so
+ * classic event ordering (TR's event queue priorities) and the classic "events
+ * are delivered before the next turn's run() body" contract both hold.
+ */
+public final class DrussGTBridge extends Bot
+ implements ClassicPeer.WorldModel, ClassicPeer.TickHost {
+
+ // ---- DrussGT host ----
+ private BotHost host;
+ private ClassicPeer peer;
+ private IBasicRobot robot;
+ private boolean firstRound = true;
+
+ // ---- bullet identity ----
+ private static final class BulletReflection {
+ static final Field X, Y, ACTIVE;
+ static {
+ Field x = null, y = null, active = null;
+ try {
+ x = robocode.Bullet.class.getDeclaredField("x");
+ x.setAccessible(true);
+ y = robocode.Bullet.class.getDeclaredField("y");
+ y.setAccessible(true);
+ active = robocode.Bullet.class.getDeclaredField("isActive");
+ active.setAccessible(true);
+ } catch (Exception e) {
+ // leave null; position/active updates are best-effort
+ }
+ X = x; Y = y; ACTIVE = active;
+ }
+
+ static void setXY(robocode.Bullet b, double x, double y) {
+ try { if (X != null) X.setDouble(b, x); if (Y != null) Y.setDouble(b, y); } catch (Exception ignored) { }
+ }
+
+ static void setActive(robocode.Bullet b, boolean active) {
+ try { if (ACTIVE != null) ACTIVE.setBoolean(b, active); } catch (Exception ignored) { }
+ }
+ }
+
+ private final List The genuine {@code robocode.RobocodeFileOutputStream} resolves an
+ * {@link IThreadManagerBase} through {@code ContainerBase.getComponent(...)} and
+ * throws {@code RobotException: ThreadManager cannot be null!} when no engine is
+ * present. DrussGT reaches it from its own {@code contain()} error logger, so
+ * any exception inside a DrussGT event handler used to kill the bot thread.
+ *
+ * {@code ContainerBase.instance} is a public static field and
+ * {@code getBaseComponent} is the single abstract hook, so installing a no-op
+ * thread manager is enough: {@code RobocodeFileOutputStream} then simply writes
+ * a normal file (the same behaviour the real engine gives a robot writing to its
+ * data directory). Nothing else in robocode.jar consults this instance in our
+ * standalone process.
+ */
+public final class ThreadManagerFix {
+
+ private ThreadManagerFix() { }
+
+ /** The no-op thread manager: an ordinary file stream, always a "safe" thread. */
+ private static final IThreadManagerBase FILE_THREAD_MANAGER = new IThreadManagerBase() {
+ @Override public boolean isSafeThread() {
+ return true;
+ }
+
+ @Override public FileOutputStream createRobotFileStream(String fileName, boolean append)
+ throws IOException {
+ return new FileOutputStream(fileName, append);
+ }
+ };
+
+ private static volatile boolean installed = false;
+
+ public static void install() {
+ if (installed) {
+ return;
+ }
+ ContainerBase.instance = new ContainerBase() {
+ @Override
+ @SuppressWarnings("unchecked")
+ protected The movement subject (written as {@code e*}) is identified by name
+ * substring; the other bot is written as {@code s*}. Because this uses the
+ * observer tick stream, the positions are perfect information, exactly like the
+ * classic capture.
+ *
+ *
+ * java -cp out:runner.jar robocode_shim.TrBattleCapture \
+ * --rounds 10 --out /tmp/tr_drussgt.jsonl \
+ * --bot /path/DrussGT --bot /path/Target --subject DrussGT
+ *
+ */
+public final class TrBattleCapture {
+
+ private static int globalTick = 0;
+ private static final Map