The unmodified DrussGT.jar connects, wave-surfs, fires and beats every adversary we have. 5 rounds each, all rounds won: SpinBot 542-16, Corners 823-4, Crazy 604-0, RamFire 900-0, ModularBot 506-72. It is genuinely surfing, not drifting or stalling. Movement statistics against the classic captures, same metrics, same analyzer: opp perp TR/classic reversing TR/cl median range TR/cl SpinBot 0.967 / 0.962 0.466 / 0.464 462 / 402 Corners 0.894 / 0.920 0.462 / 0.419 520 / 504 Crazy 0.841 / 0.829 0.527 / 0.439 370 / 378 RamFire 0.762 / 0.651 0.533 / 0.417 326 / 283 ModularBot 0.956 / - 0.466 / - 453 / - Every round starts moving within 3-11 ticks and runs at 75-82% full speed. Implemented: the classic remaining-quantity motion model (delegated to the TR Bot's own Nat-Pavasant model - identical constants: accel 1, decel -2, max 8, body turn 10-0.75|v|, gun 20, radar 45 - so getDistanceRemaining and getTurnRemaining are exactly self-consistent with what is emulated); event synthesis with classic ordering; bullet identity via object identity; gun heat; rounds; radar cadence; firing translation; and the ThreadManager landmine is killed by installing a no-op IThreadManagerBase in ContainerBase.instance (verified: without it 'RobotException: ThreadManager cannot be null!' kills the bot thread; with it the write succeeds). Also adds TrBattleCapture, an observer that dumps per-tick state in the SAME JSONL fixture format as the classic capture, so legacy bots can be captured from Tank Royale battles too. HONEST DIVERGENCES (README section 5.9): the TR server moves along the PRE-turn heading and then turns, while classic aligns displacement with the POST-turn heading, so the analyzer's conversion error is ~1.5 deg rather than 0.000 deg; distanceRemaining decrements by target speed rather than actual distance; collision clamping differs; BulletMissedEvent can fire less often because age-expiry has no TR event; bulletId is a local temp id; StatusEvent/onPaint/SkippedTurnEvent are never delivered. Physics fidelity diverges by construction - expect to retune. EnergyDomeWorker (the bullet shield) is off by default: its precise bullet-detection warm-up makes round 1 up to 79% stationary vs 35% in classic. Rounds 2+ match classic closely with it on, but the pure surfer is the consistent path. DRUSSGT_SHIELD=1 re-enables it. Jars remain out of git.
29 KiB
Robocode shim — running the real DrussGT as a Tank Royale bot
Question: can the unmodified legacy DrussGT jar be driven from a shim, and
which is cheaper — (A) re-implement robocode.* or (B) reuse the genuine
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 was an API-coverage / architecture spike and is now a working
bridge: the real, unmodified DrussGT jar connects to a Tank Royale server,
plays real battles, wave-surfs and fires. The physics/engine semantics are
supplied by the shim (ClassicPeer + DrussGTBridge) and documented in §5;
the measured movement statistics against classic-Robocode captures are in §5.8.
Status: COMPLETE (Java-only). See §5 for what was implemented, §5.8 for the verification evidence, and §5.9 for the honest list of physics divergences.
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. The bridge is implemented (Java-only, working)
The five remaining-work items are done. The bridge is DrussGTBridge (a Tank
Royale Bot) + ClassicPeer (the genuine-jar peer) + BotHost (child
classloader) + ThreadManagerFix.
| # | Item | Status | What was done |
|---|---|---|---|
| 1 | Coordinate rotation | DONE | Every TR angle is converted on read (tankRadToClassicRad) and every classic intent on write (toDegrees of the classic signed value). See TankRoyaleBridge. |
| 2 | RobocodeFileOutputStream / ThreadManager |
DONE | ThreadManagerFix.install() registers a no-op IThreadManagerBase in ContainerBase.instance, so DrussGT.contain() writes a normal file instead of throwing RobotException: ThreadManager cannot be null! and killing the thread. Proven by ThreadManagerFixTest. |
| 3 | Classic motion model | DONE | The peer's captured setAhead/setTurn* are forwarded to the TR Bot's own classic motion model (setForward/setTurnRight/setTurnGunRight/setTurnRadarRight), which is Nat Pavasant's optimal-velocity model using the same constants (ACCELERATION=1, DECELERATION=-2, MAX_SPEED=8, body turn `10-0.75· |
| 4 | Event synthesis & ordering | DONE | TR events are mapped to classic events and delivered synchronously; TR's event queue priorities are the classic priorities, so ordering matches. ScannedRobotEvent, HitByBulletEvent, BulletHitEvent, BulletHitBulletEvent, BulletMissedEvent, HitRobotEvent, HitWallEvent, RobotDeathEvent, WinEvent, DeathEvent. SkippedTurnEvent is deliberately never delivered. |
| 5 | Physics / state / bullets / rounds / radar | DONE | Energy, position, headings, speed, gun heat and cooling rate are read live from the TR bot; getRoundNum()=roundNumber-1, getNumRounds, getOthers, getTime()=turnNumber. Bullet identity is tracked: the Bullet returned by setFireBullet is the same instance later handed back in BulletHit* events, so DrussGT's Bullet.equals (which compares class + bulletId) and identity checks both work. Radar/gun adjust flags are forwarded. |
5.1 How the tick contract works
DrussGT's own run() is executed on the Tank Royale bot thread (this is
exactly where the official robocode-api-bridge runs a legacy robot). Its
execute() calls back into ClassicPeer.TickHost.onExecute(), which:
- applies the captured intents (
setAhead→setForward, …) to the TR bot, - calls the TR
Bot.go().
go() sends the intent, waits for the next tick and then dispatches that tick's
events. Our DrussGTBridge overrides the TR event handlers and forwards each
one to the classic listeners via ClassicPeer.deliver. Because the TR Bot
model dispatches the new turn's events before run()'s next iteration, the
classic contract "events for turn N are delivered before the turn-N run() body"
holds, as does the classic "new robot instance per round, static state survives"
lifetime (a fresh DrussGT is instantiated each round).
5.2 Classic motion model mapping (exact)
| classic (peer) | Tank Royale |
|---|---|
setAhead(d) / setMove(d) |
setForward(d) |
setTurnRightRadians(r) |
setTurnRight(toDegrees(r)) |
setTurnGunRightRadians(r) |
setTurnGunRight(toDegrees(r)) |
setTurnRadarRightRadians(r) |
setTurnRadarRight(toDegrees(r)) |
setMaxVelocity(v) / setMaxTurnRate(r) |
setMaxSpeed(v) / setMaxTurnRate(r) |
setAdjustGunForRobotTurn |
setAdjustGunForBodyTurn |
setAdjustRadarForRobotTurn / setAdjustRadarForGunTurn |
setAdjustRadarForBodyTurn / setAdjustRadarForGunTurn |
setFireBullet(p) |
setFire(p) + create/return a robocode.Bullet |
execute() |
go() |
getDistanceRemaining() |
getDistanceRemaining() |
getTurnRemaining() / gun / radar |
-toRadians(getTurnRemaining()) … (TR positive = left) |
A command is only forwarded when DrussGT actually issued it that turn (dirty flags), because classic remaining-quantities persist until overwritten.
5.3 Bullet identity
At setFireBullet(p) the bridge calls the TR setFire(p); if accepted it
creates a plain robocode.Bullet (never a subclass — Bullet.equals compares
getClass()), stores it in unmatchedFired, and returns it. When the TR
BulletFiredEvent arrives the nearest unmatched bullet is bound to the TR
bulletId. Later BulletHitBotEvent / BulletHitBulletEvent /
BulletHitWallEvent look the id up and pass the same instance to DrussGT, so
e.getBullet().equals(w.bullet) in DrussGunDC.onBulletHitBullet is true.
Position/isActive are updated by reflection (best effort).
5.4 Shield (EnergyDome) handling
DrussGT's EnergyDomeWorker ("shield") is a separate precise subsystem that
detects enemy bullets from per-scan energy drops. Its first-round warm-up is
sensitive to exact classic timing. The proven path (also used by SmokeTest) is
to run the pure wave surfer (DrussMoveGT): the bridge sets
DrussGT.shieldEnabled = false via BotHost.disableShield() unless
DRUSSGT_SHIELD=1. This is the configuration the statistics in §5.8 use.
5.5 Event mapping details / gotchas honoured
ScannedRobotEvent(name, energy, bearing, distance, heading, velocity)— the documented, non-intuitive argument order;bearingis the classic body-relative bearing andheadingthe enemy's classic absolute heading.BulletHitWallEvent(TR) →BulletMissedEvent(classic).BotDeathEvent→RobotDeathEvent;WonRoundEvent→WinEvent; TRDeathEvent→ classicDeathEvent.HitWallEventbearing is computed from the nearest wall normal.onPaint/AWT,StatusEvent,SkippedTurnEvent,onCustomEventare not delivered (DrussGT does not use them;getGraphics()returnsnull).
5.6 Class-loading topology
BotHost loads jk.mega.DrussGT in a child URLClassLoader whose parent is the
shim's loader (which holds the genuine robocode.jar), so IBasicRobot is the
same type the peer implements. One DrussGT class is loaded per JVM, so the static
KD-trees persist across rounds exactly like classic.
5.7 How to run it
export ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar
export TR_BOT_API_JAR=$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar
export DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar
./build.sh
./run_smoke.sh # standalone 200-tick proof
java -cp "out:$ROBOCODE_JAR" robocode_shim.ThreadManagerFixTest # item 3 proof
# real battle + movement fixture (embedded server, boots the bot dir):
./run_bridge_battle.sh /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
make_botdir.sh [DIR] generates the Tank Royale bot directory (a <DIR>.json
identity file and a <DIR>.sh launcher that runs robocode_shim.DrussGTBridge).
TrBattleCapture is the observer: it runs the battle through the Tank Royale
battle-runner API and dumps every bot's per-turn state in the same JSONL
fixture format as tools/robocode_fixture_capture/Capture.java, so
analyze.py runs on both unchanged.
5.8 Verification — it is really DrussGT, not a stub
All runs: embedded Tank Royale server, maxSpeed, shield disabled (pure surfer),
5 rounds each, against the same sample bots whose classic captures are in
tools/fixtures/. Stats computed by tools/robocode_fixture_capture/analyze.py
on the TR capture (5 rounds) and the 20-round classic fixture.
Battle outcomes (DrussGT first, total score over 5 rounds):
| opponent | DrussGT | opponent | rounds won | bullets fired / hits / misses |
|---|---|---|---|---|
| SpinBot | 542 | 16 | 5/5 | 327 / 39 / 277 |
| Corners | 823 | 4 | 5/5 | 79 / 36 / 31 |
| Crazy | 604 | 0 | 5/5 | 332 / 45 / 274 |
| RamFire | 900 | 0 | 5/5 | 54 / 38 / 12 |
| ModularBot (Nim, real) | 506 | 72 | 5/5 | 491 / 56 / 418 (+5 bullet-bullet; took 18 hits) |
Movement statistics (TR vs the matching classic capture):
| opponent | metric | TR (bridge) | classic fixture |
|---|---|---|---|
| SpinBot | perp frac / radial frac | 0.967 / 0.000 | 0.962 / 0.001 |
| full speed ≥7.5 / rest <0.5 / reversing | 0.781 / 0.017 / 0.466 | 0.701 / 0.067 / 0.464 | |
| median range (px) | 462 | 402 | |
| Corners | perp / radial | 0.894 / 0.002 | 0.920 / 0.004 |
| full / rest / reversing | 0.754 / 0.028 / 0.462 | 0.586 / 0.092 / 0.419 | |
| median range | 520 | 504 | |
| Crazy | perp / radial | 0.841 / 0.003 | 0.829 / 0.010 |
| full / rest / reversing | 0.811 / 0.017 / 0.527 | 0.668 / 0.137 / 0.439 | |
| median range | 370 | 378 | |
| RamFire | perp / radial | 0.762 / 0.002 | 0.651 / 0.013 |
| full / rest / reversing | 0.747 / 0.043 / 0.533 | 0.723 / 0.046 / 0.417 | |
| median range | 326 | 283 | |
| ModularBot | perp / radial | 0.956 / 0.000 | — |
The perpendicular-to-opponent fraction, the near-zero radial fraction and the ~42–53% reversal rate are the signature of DrussGT's wave surfer and match the classic captures closely. The TR run is slightly faster / shorter-ranged because the shield is off and the rounds are shorter (5 vs 20). Per-round, every round now starts moving within 3–11 ticks and runs at 75–82% full speed. This is not a straight line, not a standstill, and not a crash.
5.9 Known physics divergences (honest list)
- Move/turn ordering. The TR server calls
moveToNewPosition()using the pre-turn heading and only then applies the turn, whereas the classic capture aligns each tick's displacement with the post-turn heading (analyze.pyconversion error 0.000° classic vs ~1.5° TR). Trajectories therefore diverge even with identical intents. - Distance bookkeeping. The TR
Botmotion model decrementsdistanceRemainingby the target speed (Nat Pavasant's over-drive method); classic decrements by the distance actually travelled, and collision/wall clamping differs. - No exact trajectory equivalence. Bullet speeds/damage match
(
20-3p,4p (+2(p-1) if p>1)), gun heat matches (1+p/5, cool 0.1/turn), turn rates match — but the integration order above means "same decisions, diverging trajectories" is the ceiling. - First-round shield warm-up. With the shield enabled
(
DRUSSGT_SHIELD=1) round 1 can be 79% stationary vs 35% in classic, because the shield's precise bullet detection is timing-sensitive. Disabled by default. BulletMissedEvent. TR emitsBulletHitWallEventon wall hits; bullets that expire by age may not produce an event, soonBulletMissedcan fire less often than classic. DrussGT does not use it.bulletId. The classicBullet'sbulletIdis a local temporary id, not the TR server id; identity is preserved by object identity, not by id.onPaint/AWT,StatusEvent,SkippedTurnEvent, custom events are not delivered. DrussGT does not rely on them (onPaintis debug-only).- Tick phase. TR
getTurnNumber()is 1-based like classicgetTime(), but the observer snapshot phase differs by the move/turn ordering above; per-tick state read by the bot is always the current turn's.
There are no remaining API unknowns: the compiler resolved everything, and the bridge now runs full battles, surf and fire.
6. Repo layout & how to run
tools/robocode_shim/
README.md this report
build.sh compile the shim + capture
compile_drussgt.sh ACCEPTANCE TEST: javac the real DrussGT sources
run_smoke.sh load + drive the real jar, assert intents (standalone)
make_botdir.sh generate a Tank Royale bot dir that boots the bridge
run_bridge_battle.sh run a real TR battle + movement statistics
.gitignore out/ and *.jar never committed
src/robocode_shim/
ClassicPeer.java IAdvancedRobotPeer impl + world/tick seams
BotHost.java URLClassLoader + setPeer + per-round instances
TankRoyaleBridge.java exact angle conversion
ThreadManagerFix.java no-op IThreadManagerBase in ContainerBase
ThreadManagerFixTest.java proof of the above
DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
SmokeTest.java the standalone runtime proof
TrBattleCapture.java observer: battle -> classic JSONL fixture format
out/ build output (gitignored)
export ROBOCODE_JAR=/tmp/robocode/install/libs/robocode.jar
export TR_BOT_API_JAR=$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar
export DRUSSGT_JAR=/tmp/drussgt/DrussGT.jar
./build.sh
./compile_drussgt.sh
./run_smoke.sh
./run_bridge_battle.sh /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
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, DrussGT.jar at
/tmp/drussgt/DrussGT.jar, the Tank Royale Java bot API at
~/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar
and the Tank Royale battle runner at
/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar;
the scripts reference them via ROBOCODE_JAR, DRUSSGT_JAR, TR_BOT_API_JAR
and TR_RUNNER_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.