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.
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._AdvancedRadiansRobotand implementsIAdvancedRobot, 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 ofAdvancedRobot):getRobotRunnable()returnsthis;getBasicEventListener()returnsthis. So there is no engine requirement to start the bot: implement the peer,setPeer(), and rungetRobotRunnable()on a thread. -
The seam is one narrow interface.
IAdvancedRobotPeerextendsIStandardRobotPeerextendsIBasicRobotPeer— 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):
robocode.RobotDeathEventis required and was NOT predicted. The prediction listed onlyDeathEvent.DrussGT.onRobotDeath(RobotDeathEvent)forwards toDrussMoveGT.onRobotDeathandMeleeRadar, and callse.getName().DeathEvent(our own death) andRobotDeathEvent(another robot died) are distinct classic types.RobocodeFileOutputStreamis reached only throughDrussGT.contain()(DrussGT.java:259) and has a hard engine dependency —javap -cshows it callsnet.sf.robocode.core.ContainerBase.getComponent(IThreadManagerBase)and throwsRobotException: 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.Bullet.equals()is called — DrussGT comparesBulletinstances (our fired-bullet tracker), soequalssemantics matter, not just the getters.Rulesneeds the three instance methodsgetBulletDamage,getBulletSpeed,getGunHeat— these are the classic physics constants we do not otherwise have.HitByBulletEvent.getVelocity()andBulletHitEvent.getEnergy()are used (not justgetBullet()).java.util.Vectoris the declared return ofAdvancedRobot.getAllEvents()(used inonDeath), andjava.util.Hashtable/Vectorappear inMeleeRadar— 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 ofDrussGunDC.javain the decompiled tree, so their source does exist there; the standalone.javafiles 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)
ScannedRobotEventconstructor 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 yieldsdistance=0,BFT=0, an immediate KD-tree insert and an NPE insideEnemyMoves.predict.javap -comits the owner onthis-calls; API extraction must include unqualified// Method name:descreferences from the bot's own class.DrussMoveGT/EnemyMoveskeep static KD-trees; the shim must not load two copies of the bot in one JVM if that state is to stay meaningful.