feat(tools): drive the real DrussGT jar outside the Robocode engine

The question was whether we can fight a genuine legacy leader bot in Tank
Royale. Answer: the API side is now PROVEN, not estimated.

KEY FINDING: the classic robocode.* API is a thin delegation layer over a
public seam. javap -c shows AdvancedRobot forwarding every call to
_RobotBase.peer (IBasicRobotPeer/IAdvancedRobotPeer), and _RobotBase.setPeer
is public final. So we do NOT need to reimplement the API: we reuse the
genuine robocode.jar and implement only the 75-method peer interface.

Consequences:
- DrussGT's 22 sources compile against the real API with ZERO unresolved
  symbols. (The literal 22-file javac fails only on two PRE-EXISTING
  duplicate classes - GFRange and Indice are declared both inline in
  DrussGunDC.java and as standalone files - and the 6 classes that ship
  without source. The jar supplies all of them, so a shim never cares.)
- Runtime smoke test PASSES: the unmodified DrussGT.jar is loaded through a
  child URLClassLoader and driven for 200 synthetic ticks, emitting movement
  intents every tick and 169 fire requests, with its thread surviving.

This is the cheap path to the 'final boss', and it also unlocks the whole
roborumble archive rather than one bot.

Traps found by measurement:
- ScannedRobotEvent's constructor order is (name, energy, bearing, distance,
  heading, velocity) - NOT heading-before-bearing. The wrong order silently
  yields distance=0, an immediate KD-tree insert and an NPE in
  EnemyMoves.predict; it hung the first smoke run.
- RobocodeFileOutputStream has a hard engine dependency (resolves
  IThreadManagerBase via ContainerBase) and throws 'ThreadManager cannot be
  null!' outside the engine, killing the bot thread. Reached from DrussGT's
  own contain() error logging, so it must be stubbed.
- robocode.RobotDeathEvent is required and was NOT in the predicted API list.
- Bullet.equals() is genuinely called for bullet identity, not just getters.

REMAINING WORK (README section 5): coordinate rotation DONE, execute()->tick
bridge DONE and proven, ThreadManager fix scoped. The main open item is the
classic motion model (setAhead distance semantics vs TR speed), estimated
1-3 days, plus event synthesis/ordering, gun heat, bullet identity, round
and radar cadence, and firing translation. NO API UNKNOWNS REMAIN.
Physics fidelity will still diverge from classic - expect to retune.

Jars stay out of git (blocked by tools/robocode_shim/.gitignore); the
genuine robocode.jar and DrussGT.jar are referenced from /tmp via
ROBOCODE_JAR / DRUSSGT_JAR.
This commit is contained in:
2026-09-20 23:52:51 +02:00
parent 76ae6170f8
commit e0bfa9b5e1
9 changed files with 1043 additions and 0 deletions
+7
View File
@@ -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
+404
View File
@@ -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<jk.mega.dGun.StoreScan> heapTree, ASTree;
static long currentTime;
jk.mega.dGun.StoreScan storeScan;
boolean intersecting;
double bestBearing, DCBearing, DCASBearing, randomBearing;
jk.mega.dGun.DCRobotState scan;
private robocode.AdvancedRobot robot;
private double distanceTraveled;
jk.mega.dGun.DCWave(robocode.AdvancedRobot);
public boolean test();
void setSegmentations(jk.mega.dGun.DCRobotState);
private boolean hasArrived();
private double currentGF();
private double currentPreciseGF();
private double getBearing(jk.tree.KDTree, double[], int, boolean, int);
private double getBearingGaussian(jk.tree.KDTree, double[], int, boolean);
public double mostVisitedBearing();
private double getASBearing();
}
```
### `jk.mega.dMove.Scan` (declared at the end of `DrussMoveGT.java`; only `.class` shipped)
```text
class jk.mega.dMove.Scan implements java.lang.Cloneable {
static final double latVelWeight, advVelWeight, bftWeight, forwardWallWeight,
reverseWallWeight, lastVelWeight, accelWeight, timeSinceDecelWeight,
timeSinceDirChangeWeight, distLast10Weight, timeWeight1, timeWeight2, timeWeight3,
ASlatVelWeight, ASadvVelWeight, ASbftWeight, ASforwardWallWeight,
ASreverseWallWeight, ASlastVelWeight, ASaccelWeight, AStimeSinceDecelWeight,
AStimeSinceDirChangeWeight, ASdistLast10Weight, AStimeWeight1, AStimeWeight2, AStimeWeight3;
double latVel, advVel, bft, accel, forwardWall, reverseWall, lastVel,
timeSinceDecel, timeSinceDirChange, distLast10;
int pointInTime, index;
jk.mega.dMove.Scan();
public java.lang.Object clone();
double[] location();
double[] ASLocation();
}
```
> Note: `DCWave`, `JKDCUtils`, etc. are declared inline at the bottom of
> `DrussGunDC.java` in the decompiled tree, so their *source* does exist there;
> the *standalone* `.java` files do not. Either way the jar's bytecode is
> authoritative for signatures.
---
## 5. 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.
+24
View File
@@ -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"
+36
View File
@@ -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)"
+12
View File
@@ -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"
@@ -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.
*
* <pre>
* URLClassLoader cl = new URLClassLoader(new URL[]{ drussgtJar }, robocodeParent );
* Class&lt;?&gt; 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();
* </pre>
*
* 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<Event> 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; }
}
@@ -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<String> 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<Event> pendingEvents = new ArrayList<>();
private final List<Event> 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<Event> snapshot;
synchronized (lock) {
snapshot = new ArrayList<>(pendingEvents);
pendingEvents.clear();
}
dispatch(snapshot);
synchronized (lock) {
executedTurns++;
lock.notifyAll();
}
}
private void dispatch(List<Event> 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<Event> 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<Event> getAllEvents() { synchronized (lock) { return new ArrayList<>(allEvents); } }
@Override public List<robocode.StatusEvent> getStatusEvents() { return filter(robocode.StatusEvent.class); }
@Override public List<robocode.BulletMissedEvent> getBulletMissedEvents() { return filter(robocode.BulletMissedEvent.class); }
@Override public List<robocode.BulletHitBulletEvent> getBulletHitBulletEvents() { return filter(robocode.BulletHitBulletEvent.class); }
@Override public List<robocode.BulletHitEvent> getBulletHitEvents() { return filter(robocode.BulletHitEvent.class); }
@Override public List<robocode.HitByBulletEvent> getHitByBulletEvents() { return filter(robocode.HitByBulletEvent.class); }
@Override public List<robocode.HitRobotEvent> getHitRobotEvents() { return filter(robocode.HitRobotEvent.class); }
@Override public List<robocode.HitWallEvent> getHitWallEvents() { return filter(robocode.HitWallEvent.class); }
@Override public List<robocode.RobotDeathEvent> getRobotDeathEvents() { return filter(robocode.RobotDeathEvent.class); }
@Override public List<robocode.ScannedRobotEvent> getScannedRobotEvents() { return filter(robocode.ScannedRobotEvent.class); }
@SuppressWarnings("unchecked")
private <T> List<T> filter(Class<T> type) {
List<T> 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<Event> peekAllEvents() { return Collections.unmodifiableList(allEvents); }
}
@@ -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);
}
}
@@ -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).
*
* <p>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:
* <ul>
* <li>classic Robocode: {@code 0deg = North (+y)}, clockwise</li>
* <li>Tank Royale: {@code 0deg = East (+x)}, counter-clockwise</li>
* </ul>
* 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").
*/
}