Files
SirRoboGarage/tools/robocode_shim/README.md
T
SirStone e0bfa9b5e1 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.
2026-09-20 23:52:51 +02:00

20 KiB
Raw Blame History

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.:

    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:

    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:

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)

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

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

javac -nowarn -cp "$ROBOCODE_JAR:$DRUSSGT_JAR" -d /tmp/shim_out \
  $(find /tmp/drussgt/src -name '*.java')          # 22 files
/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)

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

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

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

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

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)

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·
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·

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

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)
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.