diff --git a/tools/robocode_shim/.gitignore b/tools/robocode_shim/.gitignore new file mode 100644 index 0000000..1029602 --- /dev/null +++ b/tools/robocode_shim/.gitignore @@ -0,0 +1,7 @@ +# Build output (compiled shim + compiled DrussGT classes) +out/ + +# Never commit third-party Robocode / DrussGT jars here. +# They live outside the repo (e.g. /tmp/robocode/install/libs/robocode.jar, +# /tmp/drussgt/DrussGT.jar) and are referenced via ROBOCODE_JAR/DRUSSGT_JAR. +*.jar diff --git a/tools/robocode_shim/README.md b/tools/robocode_shim/README.md new file mode 100644 index 0000000..e6ec8f2 --- /dev/null +++ b/tools/robocode_shim/README.md @@ -0,0 +1,404 @@ +# 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. diff --git a/tools/robocode_shim/build.sh b/tools/robocode_shim/build.sh new file mode 100755 index 0000000..214e411 --- /dev/null +++ b/tools/robocode_shim/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Build the Approach-(B) shim: the ClassicPeer driver + host + bridge. +# Requires JAVA_HOME/javac on PATH and the genuine Robocode 1.9.5.5 jar. +# +# ROBOCODE_JAR=/path/to/robocode.jar ./build.sh +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}" +OUT="${OUT:-$HERE/out}" + +if [[ ! -f "$ROBOCODE_JAR" ]]; then + echo "robocode.jar not found: $ROBOCODE_JAR" >&2 + echo "Set ROBOCODE_JAR to a Robocode 1.9.5.5 install's libs/robocode.jar" >&2 + exit 1 +fi + +mkdir -p "$OUT" +javac -nowarn -cp "$ROBOCODE_JAR" -d "$OUT" \ + "$HERE/src/robocode_shim/ClassicPeer.java" \ + "$HERE/src/robocode_shim/BotHost.java" \ + "$HERE/src/robocode_shim/TankRoyaleBridge.java" \ + "$HERE/src/robocode_shim/SmokeTest.java" +echo "shim built -> $OUT" diff --git a/tools/robocode_shim/compile_drussgt.sh b/tools/robocode_shim/compile_drussgt.sh new file mode 100755 index 0000000..719d267 --- /dev/null +++ b/tools/robocode_shim/compile_drussgt.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# ACCEPTANCE TEST: compile the REAL, unmodified DrussGT sources against the +# genuine Robocode 1.9.5.5 API (Approach B reuses robocode.jar instead of +# re-implementing robocode.*). +# +# ROBOCODE_JAR=... DRUSSGT_SRC=... DRUSSGT_JAR=... ./compile_drussgt.sh +# +# Why two source files are excluded: +# GFRange.java and Indice.java are *decompilation artefacts*. The same two +# classes are ALSO declared inline at the bottom of DrussGunDC.java, so a +# single javac invocation over all 22 files fails with +# "duplicate class: jk.mega.dGun.GFRange/Indice" even with no shim at all. +# The original DrussGT.jar already ships GFRange.class/Indice.class, so we +# drop the redundant standalone files and let the jar supply them (together +# with the 6 genuinely-unsourced classes: JKDCUtils, NormalDistribution, +# StoreScan, DCRobotState, DCWave, Scan). +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}" +DRUSSGT_SRC="${DRUSSGT_SRC:-/tmp/drussgt/src}" +DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}" +OUT="${OUT:-$HERE/out/drussgt}" + +for f in "$ROBOCODE_JAR" "$DRUSSGT_JAR"; do + [[ -f "$f" ]] || { echo "missing: $f" >&2; exit 1; } +done +[[ -d "$DRUSSGT_SRC" ]] || { echo "missing source dir: $DRUSSGT_SRC" >&2; exit 1; } + +mkdir -p "$OUT" +mapfile -t SRC < <(find "$DRUSSGT_SRC" -name '*.java' ! -name 'GFRange.java' ! -name 'Indice.java' | sort) + +echo "javac -nowarn -cp \"$ROBOCODE_JAR:$DRUSSGT_JAR\" -d \"$OUT\" <${#SRC[@]} files>" +javac -nowarn -cp "$ROBOCODE_JAR:$DRUSSGT_JAR" -d "$OUT" "${SRC[@]}" +echo "OK: ${#SRC[@]}/22 source files compiled, $(find "$OUT" -name '*.class' | wc -l) classes emitted" +echo "(2/22 excluded as duplicate-class decompilation artefacts; supplied by DrussGT.jar)" diff --git a/tools/robocode_shim/run_smoke.sh b/tools/robocode_shim/run_smoke.sh new file mode 100755 index 0000000..cd7686e --- /dev/null +++ b/tools/robocode_shim/run_smoke.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Runtime proof for Approach (B): load the real DrussGT jar, drive it through +# ClassicPeer for 200 synthetic ticks and assert it emits movement intents. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}" +DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}" +OUT="${OUT:-$HERE/out}" + +WORK="$(mktemp -d /tmp/shim_smoke.XXXXXX)" +java -cp "$OUT:$ROBOCODE_JAR" robocode_shim.SmokeTest "$DRUSSGT_JAR" "$WORK" diff --git a/tools/robocode_shim/src/robocode_shim/BotHost.java b/tools/robocode_shim/src/robocode_shim/BotHost.java new file mode 100644 index 0000000..0cd0c3d --- /dev/null +++ b/tools/robocode_shim/src/robocode_shim/BotHost.java @@ -0,0 +1,95 @@ +package robocode_shim; + +import robocode.Event; +import robocode.robotinterfaces.IBasicRobot; +import robocode.robotinterfaces.IAdvancedRobot; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collections; +import java.util.List; + +/** + * APPROACH (B): load the REAL, unmodified DrussGT jar with a classloader, hand + * it our {@link ClassicPeer} via {@code _RobotBase.setPeer()}, and run its own + * {@code run()} loop on a worker thread. No robocode.* class is re-implemented. + * + *
+ *   URLClassLoader cl = new URLClassLoader(new URL[]{ drussgtJar }, robocodeParent );
+ *   Class<?> c = cl.loadClass("jk.mega.DrussGT");
+ *   IBasicRobot bot = (IBasicRobot) c.getDeclaredConstructor().newInstance();
+ *   bot.setPeer(peer);            // the seam
+ *   peer.attachRobot(bot);
+ *   new Thread(bot.getRobotRunnable()).start();
+ * 
+ * + * The classloader's parent must be the classloader that loaded robocode.jar, so + * that DrussGT's {@code IBasicRobot} is the same type we call setPeer on. + */ +public class BotHost { + + public final ClassicPeer peer; + public final IBasicRobot robot; + public final URLClassLoader loader; + private final Thread thread; + private volatile Throwable botError; + + public BotHost(File drussgtJar, File dataDir) throws Exception { + this.peer = new ClassicPeer(dataDir); + this.loader = new URLClassLoader( + new URL[]{drussgtJar.toURI().toURL()}, + BotHost.class.getClassLoader()); + Class cls = loader.loadClass("jk.mega.DrussGT"); + this.robot = (IBasicRobot) cls.getDeclaredConstructor().newInstance(); + robot.setPeer(peer); + robot.setOut(new PrintStream(new File(dataDir, "bot.out"))); + peer.attachRobot(robot); + this.thread = new Thread(() -> { + try { + robot.getRobotRunnable().run(); + } catch (Throwable t) { + botError = t; + } finally { + peer.markDead(); + } + }, "DrussGT-run"); + this.thread.setDaemon(true); + } + + /** Optional: force the shield subsystem off so movement runs its pure surfer path. */ + public void disableShield() throws Exception { + java.lang.reflect.Field f = loader.loadClass("jk.mega.DrussGT").getDeclaredField("shieldEnabled"); + f.setAccessible(true); + f.setBoolean(null, false); + } + + /** Optional: call a named no-arg method via the IBasicEvents interface. */ + public void invokeBasicEvent(String method, Class argType, Event event) throws Exception { + Method m = robot.getClass().getMethod(method, argType); + m.invoke(robot, event); + } + + public void start() { + thread.start(); + } + + /** Feed one synthetic turn and block until the bot consumed it. */ + /** @return false if the bot thread died during this tick. */ + public boolean tick(List events) { + int n = peer.getExecutedTurns() + 1; + peer.provideTurn(events == null ? Collections.emptyList() : events); + peer.awaitExecutedTurns(n); + return peer.getExecutedTurns() >= n && !peer.isDead(); + } + + public void stop() { + peer.shutdown(); + thread.interrupt(); + } + + public Thread getThread() { return thread; } + public Throwable getBotError() { return botError; } +} diff --git a/tools/robocode_shim/src/robocode_shim/ClassicPeer.java b/tools/robocode_shim/src/robocode_shim/ClassicPeer.java new file mode 100644 index 0000000..9d925f1 --- /dev/null +++ b/tools/robocode_shim/src/robocode_shim/ClassicPeer.java @@ -0,0 +1,290 @@ +package robocode_shim; + +import robocode.Bullet; +import robocode.Condition; +import robocode.Event; +import robocode.robotinterfaces.IBasicEvents; +import robocode.robotinterfaces.IBasicRobot; +import robocode.robotinterfaces.IAdvancedEvents; +import robocode.robotinterfaces.peer.IAdvancedRobotPeer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * APPROACH (B) SEAM. + * + * The genuine robocode.jar {@code AdvancedRobot}/{@code Robot} classes are pure + * delegates: every movement/fire setter forwards to + * {@code _RobotBase.peer}, an interface field of type + * {@link robocode.robotinterfaces.peer.IBasicRobotPeer} (narrowed to + * {@link IAdvancedRobotPeer} for advanced setters). {@code _RobotBase.setPeer()} + * is public+final, and {@code Robot.getRobotRunnable()} simply returns + * {@code this}. + * + * Therefore we do NOT need to re-implement the robocode.* API (Approach A). + * We only implement this peer interface, feed it world state, let the bot's own + * {@code run()} thread block in {@link #execute()}, and read back the intents. + * + * The getters here are a stubbed world model: this is an API/architecture + * spike, NOT physics. See TankRoyaleBridge for the translation layer that would + * fill these fields from a Tank Royale websocket tick. + */ +public class ClassicPeer implements IAdvancedRobotPeer { + + // ---- world state (would be pushed from Tank Royale each tick) ---- + public volatile String name = "DrussGT"; + public volatile long time = 0; + public volatile double energy = 100.0; + public volatile double x = 400.0, y = 300.0; + public volatile double velocity = 0.0; + public volatile double bodyHeading = 0.0; + public volatile double gunHeading = 0.0; + public volatile double radarHeading = 0.0; + public volatile double gunHeat = 3.0; + public volatile double battleFieldWidth = 800.0; + public volatile double battleFieldHeight = 600.0; + public volatile int others = 1; + public volatile int numSentries = 0; + public volatile int numRounds = 10; + public volatile int roundNum = 0; + public volatile int sentryBorderSize = 100; + public volatile double gunCoolingRate = 0.1; + public volatile double distanceRemaining = 0.0; + public volatile double bodyTurnRemaining = 0.0; + public volatile double gunTurnRemaining = 0.0; + public volatile double radarTurnRemaining = 0.0; + + // ---- captured intents (the engine would normally read/execute these) ---- + public volatile double lastMove = 0.0; + public volatile double lastTurnBody = 0.0; + public volatile double lastTurnGun = 0.0; + public volatile double lastTurnRadar = 0.0; + public volatile double lastFirePower = Double.NaN; + public volatile double maxVelocity = Double.POSITIVE_INFINITY; + public volatile double maxTurnRate = Double.POSITIVE_INFINITY; + public volatile boolean stopRequested = false; + + public volatile boolean adjustGunForBodyTurn = false; + public volatile boolean adjustRadarForGunTurn = false; + public volatile boolean adjustRadarForBodyTurn = false; + + // ---- colors / debug (accepted and ignored) ---- + public volatile Color bodyColor, gunColor, radarColor, bulletColor, scanColor; + public final List debugProperties = new ArrayList<>(); + + // ---- tick bridge ---- + private final Object lock = new Object(); + private volatile boolean turnReady = false; + private volatile boolean shutdown = false; + private volatile boolean dead = false; + private int executedTurns = 0; + private final List pendingEvents = new ArrayList<>(); + private final List allEvents = new ArrayList<>(); + private IBasicRobot robot; // set after setPeer, used to dispatch events on the bot thread + private final File dataDir; + + public ClassicPeer(File dataDir) { + this.dataDir = dataDir; + if (!dataDir.exists()) { + dataDir.mkdirs(); + } + } + + public void attachRobot(IBasicRobot robot) { + this.robot = robot; + } + + // ================= world getters ================= + @Override public String getName() { return name; } + @Override public long getTime() { return time; } + @Override public double getEnergy() { return energy; } + @Override public double getX() { return x; } + @Override public double getY() { return y; } + @Override public double getVelocity() { return velocity; } + @Override public double getBodyHeading() { return bodyHeading; } + @Override public double getGunHeading() { return gunHeading; } + @Override public double getRadarHeading() { return radarHeading; } + @Override public double getGunHeat() { return gunHeat; } + @Override public double getBattleFieldWidth() { return battleFieldWidth; } + @Override public double getBattleFieldHeight() { return battleFieldHeight; } + @Override public int getOthers() { return others; } + @Override public int getNumSentries() { return numSentries; } + @Override public int getNumRounds() { return numRounds; } + @Override public int getRoundNum() { return roundNum; } + @Override public int getSentryBorderSize() { return sentryBorderSize; } + @Override public double getGunCoolingRate() { return gunCoolingRate; } + @Override public double getDistanceRemaining() { return distanceRemaining; } + @Override public double getBodyTurnRemaining() { return bodyTurnRemaining; } + @Override public double getGunTurnRemaining() { return gunTurnRemaining; } + @Override public double getRadarTurnRemaining() { return radarTurnRemaining; } + + // ================= movement / action intents ================= + @Override public void move(double distance) { lastMove = distance; } + @Override public void turnBody(double radians) { lastTurnBody = radians; } + @Override public void turnGun(double radians) { lastTurnGun = radians; } + @Override public void turnRadar(double radians) { lastTurnRadar = radians; } + @Override public void setMove(double distance) { lastMove = distance; } + @Override public void setTurnBody(double radians) { lastTurnBody = radians; } + @Override public void setTurnGun(double radians) { lastTurnGun = radians; } + @Override public void setTurnRadar(double radians) { lastTurnRadar = radians; } + @Override public void setMaxTurnRate(double rate) { maxTurnRate = rate; } + @Override public void setMaxVelocity(double velocity) { maxVelocity = velocity; } + + @Override public Bullet fire(double power) { return makeBullet(power); } + @Override public Bullet setFire(double power) { return makeBullet(power); } + + private Bullet makeBullet(double power) { + lastFirePower = power; + // Bullet(x, y, headingRadians, power, ownerName, victimName, isActive, bulletId) + return new Bullet(x, y, gunHeading, power, name, null, true, executedTurns); + } + + @Override public void stop(boolean overwrite) { stopRequested = true; } + @Override public void resume() { stopRequested = false; } + @Override public void setStop(boolean overwrite) { stopRequested = true; } + @Override public void setResume() { stopRequested = false; } + + @Override public void setAdjustGunForBodyTurn(boolean v) { adjustGunForBodyTurn = v; } + @Override public void setAdjustRadarForGunTurn(boolean v) { adjustRadarForGunTurn = v; } + @Override public void setAdjustRadarForBodyTurn(boolean v) { adjustRadarForBodyTurn = v; } + @Override public boolean isAdjustGunForBodyTurn() { return adjustGunForBodyTurn; } + @Override public boolean isAdjustRadarForGunTurn() { return adjustRadarForGunTurn; } + @Override public boolean isAdjustRadarForBodyTurn() { return adjustRadarForBodyTurn; } + + @Override public void execute() { + synchronized (lock) { + while (!turnReady && !shutdown) { + try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } + } + turnReady = false; + } + List snapshot; + synchronized (lock) { + snapshot = new ArrayList<>(pendingEvents); + pendingEvents.clear(); + } + dispatch(snapshot); + synchronized (lock) { + executedTurns++; + lock.notifyAll(); + } + } + + private void dispatch(List events) { + if (robot == null) return; + IBasicEvents basic = robot.getBasicEventListener(); + IAdvancedEvents adv = null; + if (robot instanceof robocode.robotinterfaces.IAdvancedRobot) { + adv = ((robocode.robotinterfaces.IAdvancedRobot) robot).getAdvancedEventListener(); + } + for (Event e : events) { + if (e instanceof robocode.ScannedRobotEvent) basic.onScannedRobot((robocode.ScannedRobotEvent) e); + else if (e instanceof robocode.HitByBulletEvent) basic.onHitByBullet((robocode.HitByBulletEvent) e); + else if (e instanceof robocode.BulletHitEvent) basic.onBulletHit((robocode.BulletHitEvent) e); + else if (e instanceof robocode.BulletHitBulletEvent) basic.onBulletHitBullet((robocode.BulletHitBulletEvent) e); + else if (e instanceof robocode.BulletMissedEvent) basic.onBulletMissed((robocode.BulletMissedEvent) e); + else if (e instanceof robocode.HitRobotEvent) basic.onHitRobot((robocode.HitRobotEvent) e); + else if (e instanceof robocode.HitWallEvent) basic.onHitWall((robocode.HitWallEvent) e); + else if (e instanceof robocode.RobotDeathEvent) basic.onRobotDeath((robocode.RobotDeathEvent) e); + else if (e instanceof robocode.WinEvent) basic.onWin((robocode.WinEvent) e); + else if (e instanceof robocode.DeathEvent) basic.onDeath((robocode.DeathEvent) e); + else if (e instanceof robocode.StatusEvent) basic.onStatus((robocode.StatusEvent) e); + else if (adv != null && e instanceof robocode.SkippedTurnEvent) adv.onSkippedTurn((robocode.SkippedTurnEvent) e); + else if (adv != null && e instanceof robocode.CustomEvent) adv.onCustomEvent((robocode.CustomEvent) e); + } + } + + // ================= tick bridge control (driver side) ================= + /** Driver: publish world state + this turn's events, then unblock the bot's execute(). */ + public void provideTurn(List events) { + synchronized (lock) { + pendingEvents.addAll(events); + allEvents.addAll(events); + turnReady = true; + lock.notifyAll(); + } + } + + /** Driver: wait until the bot has finished consuming turn {@code n}. */ + public void awaitExecutedTurns(int n) { + synchronized (lock) { + while (executedTurns < n && !shutdown && !dead) { + try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } + } + } + } + + /** Called by the host when the bot's run() thread terminates. */ + public void markDead() { + synchronized (lock) { dead = true; lock.notifyAll(); } + } + + public boolean isDead() { return dead; } + + public int getExecutedTurns() { + synchronized (lock) { return executedTurns; } + } + + public void shutdown() { + synchronized (lock) { + shutdown = true; + turnReady = true; + lock.notifyAll(); + } + } + + // ================= misc peer surface ================= + @Override public void setBodyColor(Color c) { bodyColor = c; } + @Override public void setGunColor(Color c) { gunColor = c; } + @Override public void setRadarColor(Color c) { radarColor = c; } + @Override public void setBulletColor(Color c) { bulletColor = c; } + @Override public void setScanColor(Color c) { scanColor = c; } + @Override public void getCall() { } + @Override public void setCall() { } + @Override public Graphics2D getGraphics() { return null; } + @Override public void setDebugProperty(String key, String value) { debugProperties.add(key + "=" + value); } + @Override public void rescan() { } + + @Override public void waitFor(Condition condition) { } + @Override public void setInterruptible(boolean v) { } + @Override public void setEventPriority(String eventClass, int priority) { } + @Override public int getEventPriority(String eventClass) { return 0; } + @Override public void addCustomEvent(Condition condition) { } + @Override public void removeCustomEvent(Condition condition) { } + @Override public void clearAllEvents() { + synchronized (lock) { allEvents.clear(); pendingEvents.clear(); } + } + + @Override public List getAllEvents() { synchronized (lock) { return new ArrayList<>(allEvents); } } + @Override public List getStatusEvents() { return filter(robocode.StatusEvent.class); } + @Override public List getBulletMissedEvents() { return filter(robocode.BulletMissedEvent.class); } + @Override public List getBulletHitBulletEvents() { return filter(robocode.BulletHitBulletEvent.class); } + @Override public List getBulletHitEvents() { return filter(robocode.BulletHitEvent.class); } + @Override public List getHitByBulletEvents() { return filter(robocode.HitByBulletEvent.class); } + @Override public List getHitRobotEvents() { return filter(robocode.HitRobotEvent.class); } + @Override public List getHitWallEvents() { return filter(robocode.HitWallEvent.class); } + @Override public List getRobotDeathEvents() { return filter(robocode.RobotDeathEvent.class); } + @Override public List getScannedRobotEvents() { return filter(robocode.ScannedRobotEvent.class); } + + @SuppressWarnings("unchecked") + private List filter(Class type) { + List out = new ArrayList<>(); + synchronized (lock) { + for (Event e : allEvents) if (type.isInstance(e)) out.add((T) e); + } + return out; + } + + @Override public File getDataDirectory() { return dataDir; } + + @Override public File getDataFile(String filename) { return new File(dataDir, filename); } + + @Override public long getDataQuotaAvailable() { return 1024L * 1024L; } + + public List peekAllEvents() { return Collections.unmodifiableList(allEvents); } +} diff --git a/tools/robocode_shim/src/robocode_shim/SmokeTest.java b/tools/robocode_shim/src/robocode_shim/SmokeTest.java new file mode 100644 index 0000000..8a9530b --- /dev/null +++ b/tools/robocode_shim/src/robocode_shim/SmokeTest.java @@ -0,0 +1,90 @@ +package robocode_shim; + +import robocode.ScannedRobotEvent; + +import java.io.File; +import java.util.Collections; + +/** + * Runtime proof for APPROACH (B): instantiate the real DrussGT from its jar, + * setPeer(), run its own run() loop, feed it synthetic ScannedRobotEvents and + * observe that it emits movement/fire intents through ClassicPeer. + * + * This is an architecture spike: there is no physics here and the peer's world + * state does not integrate the intents. If the bot starts, survives its + * constructor (DrussMoveGT/MeleeRadar/DrussGunDC/EnergyDomeWorker) and produces + * non-zero setAhead/setTurnRight intents, the seam is proven operational. + */ +public class SmokeTest { + + public static void main(String[] args) throws Exception { + File jar = new File(args.length > 0 ? args[0] : "/tmp/drussgt/DrussGT.jar"); + File work = new File(args.length > 1 ? args[1] : "/tmp/shim_smoke"); + if (!work.exists()) work.mkdirs(); + + System.out.println("DrussGT jar: " + jar + " (" + jar.length() + " bytes)"); + + BotHost host = new BotHost(jar, work); + host.disableShield(); // run the pure surfer path (skip EnergyDome shield) + host.start(); + + int ticks = 200; + int movementIntentTicks = 0; + double firstMove = Double.NaN, firstTurn = Double.NaN; + int fireRequests = 0; + boolean botSurvived = true; + + for (int t = 0; t < ticks; t++) { + ClassicPeer p = host.peer; + p.time = t; + p.x = 400; + p.y = 300; + p.energy = 100 - (t * 0.001); + p.bodyHeading = 0.0; + p.gunHeading = 0.0; + p.radarHeading = 0.0; + p.gunHeat = Math.max(0, 3.0 - t * 0.1); + p.distanceRemaining = 0; + + // A moving, orbiting enemy: enough feature variation that the + // KD-tree in EnemyMoves does not see degenerate duplicate points. + double theta = Math.toRadians(t * 7.0); + double bearing = theta; // bot heading is 0 + double distance = 300.0 + 80.0 * Math.sin(theta / 2.3); + double enemyHeading = theta + Math.PI / 2; // perpendicular (surfer) + double enemyEnergy = 100.0 - 0.1 * (t % 10); + ScannedRobotEvent scan = new ScannedRobotEvent( + "Enemy", enemyEnergy, bearing, distance, enemyHeading, 8.0); + if (!host.tick(Collections.singletonList(scan))) { botSurvived = false; break; } + + if (p.lastMove != 0 || p.lastTurnBody != 0) { + if (movementIntentTicks == 0) { firstMove = p.lastMove; firstTurn = p.lastTurnBody; } + movementIntentTicks++; + } + if (!Double.isNaN(p.lastFirePower)) { + fireRequests++; + p.lastFirePower = Double.NaN; + } + } + + System.out.println("ticks executed : " + host.peer.getExecutedTurns()); + System.out.println("bot thread survived : " + botSurvived); + if (host.getBotError() != null) { + System.out.println("bot thread error : " + host.getBotError()); + } + System.out.println("ticks with a move intent : " + movementIntentTicks); + System.out.println("first (setAhead,setTurnRR): (" + firstMove + ", " + firstTurn + ")"); + System.out.println("fire requests : " + fireRequests); + System.out.println("last captured intents : move=" + host.peer.lastMove + + " turnBody=" + host.peer.lastTurnBody + + " turnGun=" + host.peer.lastTurnGun + + " turnRadar=" + host.peer.lastTurnRadar); + System.out.println("bot thread alive : " + host.getThread().isAlive()); + + host.stop(); + boolean ok = movementIntentTicks > 0 && botSurvived; + System.out.println(ok ? "SMOKE TEST: OK (real DrussGT drove through the peer seam)" + : "SMOKE TEST: no movement intents observed"); + System.exit(ok ? 0 : 1); + } +} diff --git a/tools/robocode_shim/src/robocode_shim/TankRoyaleBridge.java b/tools/robocode_shim/src/robocode_shim/TankRoyaleBridge.java new file mode 100644 index 0000000..e7628e0 --- /dev/null +++ b/tools/robocode_shim/src/robocode_shim/TankRoyaleBridge.java @@ -0,0 +1,85 @@ +package robocode_shim; + +/** + * Translation layer between a Tank Royale websocket tick and the classic + * Robocode peer. THIS IS A SKELETON: only the pure angle conversion is + * implemented and tested; the websocket plumbing and the event synthesis are + * the remaining work (see README). + * + *

Both engines share the same positional axes (origin bottom-left, +x right, + * +y up), so {@code x}/{@code y} are copied unchanged. Only the angle system + * differs: + *

    + *
  • classic Robocode: {@code 0deg = North (+y)}, clockwise
  • + *
  • Tank Royale: {@code 0deg = East (+x)}, counter-clockwise
  • + *
+ * Hence {@code trRad = PI/2 - classicRad} (and vice-versa). This is the exact + * conversion already validated in {@code tools/fixtures/DRUSSGT_FIXTURES.md} + * against recorded DrussGT motion. + */ +public final class TankRoyaleBridge { + + private TankRoyaleBridge() { } + + /** classic degrees (0 = North, clockwise) -> Tank Royale radians (0 = East, CCW). */ + public static double classicDegToTankRad(double classicDeg) { + return Math.toRadians(normalizeDeg(90.0 - classicDeg)); + } + + /** classic radians (0 = North, clockwise) -> Tank Royale radians (0 = East, CCW). */ + public static double classicRadToTankRad(double classicRad) { + return (Math.PI / 2.0) - classicRad; + } + + /** Tank Royale radians (0 = East, CCW) -> classic radians (0 = North, clockwise). */ + public static double tankRadToClassicRad(double tankRad) { + return (Math.PI / 2.0) - tankRad; + } + + /** Tank Royale radians (0 = East, CCW) -> classic degrees (0 = North, clockwise). */ + public static double tankRadToClassicDeg(double tankRad) { + return normalizeDeg(90.0 - Math.toDegrees(tankRad)); + } + + /** Classic signed velocity px/tick is directly reusable: Tank Royale uses the same sign. */ + public static double classicVelocity(double tankVelocity) { + return tankVelocity; + } + + private static double normalizeDeg(double d) { + double r = d % 360.0; + return r < 0 ? r + 360.0 : r; + } + + /* + * ================== ONE-TICK CONTRACT (not yet wired) ================== + * + * Tank Royale delivers one websocket "tick" (turn) carrying, per bot: + * energy, x, y, direction, gunDirection, radarDirection, radarSpeed, + * speed, turnRate, gunTurnRate, radarTurnRate (all TR conventions) + * + * and, when applicable, the events for that turn: + * ScannedRobotEvent, HitBotEvent, HitWallEvent, BulletFiredEvent, + * HitByBulletEvent, BulletHitBotEvent, BulletHitBulletEvent, ... + * + * The bridge must: + * 1. Convert every TR angle with classicRadToTankRad()/tankRadToClassicRad() + * and copy x/y/energy/speed verbatim. + * 2. Write those values into ClassicPeer's volatile world fields. + * 3. Synthesise the classic event objects (public constructors confirmed + * by javap, e.g. ScannedRobotEvent(name, energy, bearing, distance, + * heading, velocity) -- NOT the javadoc-intuitive order) and pass them + * to peer.provideTurn(events). + * 4. After the bot's execute() unblocks, read the peer intents + * (lastMove/lastTurnBody/lastTurnGun/lastTurnRadar/lastFirePower), + * convert the classic angular intent back to TR, and map + * classic setAhead(distance)/setTurnBody(rad) onto TR's + * (speed, turnRate) per tick. + * + * That last step is the hard part and is deliberately NOT implemented here: + * classic Robocode integrates a *distance* target (getDistanceRemaining), + * while Tank Royale expects a *target speed* and *rotation rate*. A faithful + * bridge needs a per-tick "classic motion model" shim in front of the real + * physics (see README "Remaining work"). + */ +}