feat(tools): the real DrussGT now plays and wins Tank Royale battles
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.
This commit is contained in:
+210
-32
@@ -1,4 +1,4 @@
|
||||
# Robocode shim spike — running the real DrussGT as a Tank Royale bot
|
||||
# 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
|
||||
@@ -10,8 +10,14 @@ delegates to a `peer` interface, and that seam is public. DrussGT compiles with
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -330,30 +336,190 @@ class jk.mega.dMove.Scan implements java.lang.Cloneable {
|
||||
|
||||
---
|
||||
|
||||
## 5. Remaining work to go from "compiles" to "runs a battle"
|
||||
## 5. The bridge is implemented (Java-only, working)
|
||||
|
||||
Everything below is now the *inverse* problem: the API is complete, but the
|
||||
**engine semantics** behind it are not implemented.
|
||||
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 | Notes |
|
||||
| # | Item | Status | What was done |
|
||||
|---|---|---|---|
|
||||
| 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". |
|
||||
| 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·|v|`, gun `20`, radar `45`). `getDistanceRemaining()`/`getTurnRemaining()` delegate to the same model, so they are exactly consistent with what is emulated. |
|
||||
| 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:
|
||||
|
||||
1. applies the captured intents (`setAhead` → `setForward`, …) to the TR bot,
|
||||
2. 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; `bearing` is the classic body-relative
|
||||
bearing and `heading` the enemy's classic absolute heading.
|
||||
* `BulletHitWallEvent` (TR) → `BulletMissedEvent` (classic).
|
||||
* `BotDeathEvent` → `RobotDeathEvent`; `WonRoundEvent` → `WinEvent`; TR
|
||||
`DeathEvent` → classic `DeathEvent`.
|
||||
* `HitWallEvent` bearing is computed from the nearest wall normal.
|
||||
* `onPaint`/AWT, `StatusEvent`, `SkippedTurnEvent`, `onCustomEvent` are not
|
||||
delivered (DrussGT does not use them; `getGraphics()` returns `null`).
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
1. **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.py` conversion error 0.000° classic vs ~1.5° TR). Trajectories
|
||||
therefore diverge even with identical intents.
|
||||
2. **Distance bookkeeping.** The TR `Bot` motion model decrements
|
||||
`distanceRemaining` by the *target* speed (Nat Pavasant's over-drive method);
|
||||
classic decrements by the distance actually travelled, and collision/wall
|
||||
clamping differs.
|
||||
3. **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.
|
||||
4. **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.
|
||||
5. **`BulletMissedEvent`.** TR emits `BulletHitWallEvent` on wall hits; bullets
|
||||
that expire by age may not produce an event, so `onBulletMissed` can fire less
|
||||
often than classic. DrussGT does not use it.
|
||||
6. **`bulletId`.** The classic `Bullet`'s `bulletId` is a local temporary id, not
|
||||
the TR server id; identity is preserved by object identity, not by id.
|
||||
7. **`onPaint`/AWT, `StatusEvent`, `SkippedTurnEvent`, custom events** are not
|
||||
delivered. DrussGT does not rely on them (`onPaint` is debug-only).
|
||||
8. **Tick phase.** TR `getTurnNumber()` is 1-based like classic `getTime()`, 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 smoke test exercised the full event → decision → intent path.
|
||||
the bridge now runs full battles, surf and fire.
|
||||
|
||||
---
|
||||
|
||||
@@ -362,34 +528,46 @@ the smoke test exercised the full event → decision → intent path.
|
||||
```text
|
||||
tools/robocode_shim/
|
||||
README.md this report
|
||||
build.sh compile the shim (ClassicPeer/BotHost/Bridge/SmokeTest)
|
||||
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
|
||||
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 + execute() tick bridge
|
||||
BotHost.java URLClassLoader + setPeer + run() thread
|
||||
TankRoyaleBridge.java angle conversion + documented tick contract (skeleton)
|
||||
SmokeTest.java the runtime proof
|
||||
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)
|
||||
```
|
||||
|
||||
```bash
|
||||
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` 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.
|
||||
`/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)
|
||||
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
#!/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.
|
||||
# Build the Approach-(B) shim.
|
||||
#
|
||||
# ROBOCODE_JAR=/path/to/robocode.jar ./build.sh
|
||||
# Requires JAVA_HOME/javac on PATH, the genuine Robocode 1.9.5.5 jar (for the
|
||||
# robocode.* API the peer implements) and the Tank Royale Java bot API jar (for
|
||||
# DrussGTBridge / TrBattleCapture).
|
||||
#
|
||||
# ROBOCODE_JAR=/path/to/robocode.jar TR_BOT_API_JAR=/path/to/bot-api.jar ./build.sh
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}"
|
||||
TR_BOT_API_JAR="${TR_BOT_API_JAR:-$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar}"
|
||||
OUT="${OUT:-$HERE/out}"
|
||||
RUNNER_JAR="${TR_RUNNER_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}"
|
||||
|
||||
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
|
||||
if [[ ! -f "$TR_BOT_API_JAR" ]]; then
|
||||
echo "Tank Royale bot API jar not found: $TR_BOT_API_JAR" >&2
|
||||
echo "Set TR_BOT_API_JAR to robocode-tankroyale-bot-api-*.jar" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
javac -nowarn -cp "$ROBOCODE_JAR" -d "$OUT" \
|
||||
javac -nowarn -cp "$ROBOCODE_JAR:$TR_BOT_API_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/ThreadManagerFix.java" \
|
||||
"$HERE/src/robocode_shim/ThreadManagerFixTest.java" \
|
||||
"$HERE/src/robocode_shim/DrussGTBridge.java" \
|
||||
"$HERE/src/robocode_shim/SmokeTest.java"
|
||||
echo "shim built -> $OUT"
|
||||
|
||||
# The observer/capture program needs the battle-runner jar (which bundles the
|
||||
# Tank Royale client model + embedded server) rather than the bot API.
|
||||
if [[ -f "$RUNNER_JAR" ]]; then
|
||||
javac -nowarn -cp "$RUNNER_JAR" -d "$OUT" \
|
||||
"$HERE/src/robocode_shim/TrBattleCapture.java"
|
||||
echo "capture built -> $OUT"
|
||||
else
|
||||
echo "runner jar not found ($RUNNER_JAR); skipped TrBattleCapture" >&2
|
||||
fi
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate a Tank Royale bot directory that boots the DrussGT bridge.
|
||||
#
|
||||
# tools/robocode_shim/make_botdir.sh [TARGET_DIR]
|
||||
#
|
||||
# The generated directory contains only text files (<dir>.json + <dir>.sh); the
|
||||
# third-party jars stay outside the repo and are referenced by absolute path.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET="${1:-/tmp/tr_bots/DrussGT}"
|
||||
NAME="$(basename "$TARGET")"
|
||||
|
||||
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}"
|
||||
TR_BOT_API_JAR="${TR_BOT_API_JAR:-$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar}"
|
||||
OUT="${OUT:-$HERE/out}"
|
||||
|
||||
mkdir -p "$TARGET"
|
||||
cat > "$TARGET/$NAME.json" <<JSON
|
||||
{
|
||||
"name": "DrussGT",
|
||||
"version": "3.1.4159",
|
||||
"authors": ["Jacob"],
|
||||
"description": "Real classic DrussGT running on Tank Royale via the robocode_shim bridge",
|
||||
"homepage": "",
|
||||
"countryCodes": ["se"],
|
||||
"gameTypes": ["classic", "1v1"],
|
||||
"platform": "JVM",
|
||||
"programmingLang": "Java 8"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$TARGET/$NAME.sh" <<SH
|
||||
#!/bin/sh
|
||||
cd "\$(dirname "\$0")"
|
||||
exec java -cp "$OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR" robocode_shim.DrussGTBridge
|
||||
SH
|
||||
chmod +x "$TARGET/$NAME.sh"
|
||||
|
||||
echo "bot dir ready: $TARGET"
|
||||
echo " $TARGET/$NAME.json"
|
||||
echo " $TARGET/$NAME.sh"
|
||||
echo " (classpath: $OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR)"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run a real Tank Royale battle with the DrussGT bridge against an adversary and
|
||||
# dump the movement fixture + statistics.
|
||||
#
|
||||
# tools/robocode_shim/run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl]
|
||||
#
|
||||
# Example:
|
||||
# tools/robocode_shim/run_bridge_battle.sh \
|
||||
# /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "$HERE/../.." && pwd)"
|
||||
ADVERSARY="${1:?usage: run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl]}"
|
||||
ROUNDS="${2:-5}"
|
||||
OUT_JSONL="${3:-/tmp/tr_drussgt_$(basename "$ADVERSARY" | tr 'A-Z' 'a-z').jsonl}"
|
||||
|
||||
RUNNER="${TR_RUNNER_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}"
|
||||
export DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}"
|
||||
export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}"
|
||||
|
||||
BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
||||
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null
|
||||
|
||||
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
|
||||
--rounds "$ROUNDS" --out "$OUT_JSONL" \
|
||||
--bot "$BOTDIR" --bot "$ADVERSARY" \
|
||||
--subject DrussGT --adversary "$(basename "$ADVERSARY")"
|
||||
|
||||
echo
|
||||
echo "=== movement statistics (TR) ==="
|
||||
python3 "$REPO/tools/robocode_fixture_capture/analyze.py" "$OUT_JSONL"
|
||||
@@ -32,12 +32,14 @@ import java.util.List;
|
||||
public class BotHost {
|
||||
|
||||
public final ClassicPeer peer;
|
||||
public final IBasicRobot robot;
|
||||
public volatile IBasicRobot robot;
|
||||
public final URLClassLoader loader;
|
||||
private final File dataDir;
|
||||
private final Thread thread;
|
||||
private volatile Throwable botError;
|
||||
|
||||
public BotHost(File drussgtJar, File dataDir) throws Exception {
|
||||
this.dataDir = dataDir;
|
||||
this.peer = new ClassicPeer(dataDir);
|
||||
this.loader = new URLClassLoader(
|
||||
new URL[]{drussgtJar.toURI().toURL()},
|
||||
@@ -59,6 +61,19 @@ public class BotHost {
|
||||
this.thread.setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classic Robocode gives every round a fresh robot instance (only static
|
||||
* fields survive). DrussGT sets up per-round state in run(), but mirroring
|
||||
* the engine's instance lifetime removes a whole class of cross-round bugs.
|
||||
*/
|
||||
public IBasicRobot newRobot() throws Exception {
|
||||
Class<?> cls = loader.loadClass("jk.mega.DrussGT");
|
||||
IBasicRobot r = (IBasicRobot) cls.getDeclaredConstructor().newInstance();
|
||||
r.setPeer(peer);
|
||||
r.setOut(new PrintStream(new File(dataDir, "bot.out")));
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 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");
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -19,24 +18,31 @@ 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}.
|
||||
* 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.
|
||||
* <p>Two modes:
|
||||
* <ul>
|
||||
* <li><b>standalone / synthetic</b> (SmokeTest): the public volatile world
|
||||
* fields are the world model and {@link #execute()} blocks on a lock fed by
|
||||
* {@link #provideTurn(List)}. No physics.</li>
|
||||
* <li><b>battle</b>: a {@link WorldModel} is installed (the live Tank Royale
|
||||
* bot) and a {@link TickHost} is installed. Getters then read the real TR
|
||||
* state (converted), {@link #execute()} calls back into the host (which drives
|
||||
* the TR tick), and fire is forwarded to the host so bullet identity can be
|
||||
* tracked.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
|
||||
// ---- world state (would be pushed from Tank Royale each tick) ----
|
||||
// ---- optional live world model (battle mode) ----
|
||||
public volatile WorldModel world;
|
||||
public volatile TickHost tickHost;
|
||||
|
||||
// ---- static world state (standalone mode / captured intents) ----
|
||||
public volatile String name = "DrussGT";
|
||||
public volatile long time = 0;
|
||||
public volatile double energy = 100.0;
|
||||
@@ -59,15 +65,29 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
public volatile double gunTurnRemaining = 0.0;
|
||||
public volatile double radarTurnRemaining = 0.0;
|
||||
|
||||
// ---- captured intents (the engine would normally read/execute these) ----
|
||||
// ---- captured intents (the engine would normally read 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;
|
||||
|
||||
/**
|
||||
* "The robot issued this command this turn" flags. Classic
|
||||
* {@code setAhead(d)} stores a remaining quantity that persists until it is
|
||||
* overwritten, so the bridge must not re-apply a stale value every tick.
|
||||
*/
|
||||
public volatile boolean moveSet = false;
|
||||
public volatile boolean turnBodySet = false;
|
||||
public volatile boolean turnGunSet = false;
|
||||
public volatile boolean turnRadarSet = false;
|
||||
|
||||
public volatile double lastFirePower = Double.NaN;
|
||||
public volatile double maxVelocity = Double.POSITIVE_INFINITY;
|
||||
public volatile double maxTurnRate = Double.POSITIVE_INFINITY;
|
||||
public volatile boolean maxVelocitySet = false;
|
||||
public volatile boolean maxTurnRateSet = false;
|
||||
public volatile boolean stopRequested = false;
|
||||
public volatile boolean stopSet = false;
|
||||
|
||||
public volatile boolean adjustGunForBodyTurn = false;
|
||||
public volatile boolean adjustRadarForGunTurn = false;
|
||||
@@ -77,7 +97,7 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
public volatile Color bodyColor, gunColor, radarColor, bulletColor, scanColor;
|
||||
public final List<String> debugProperties = new ArrayList<>();
|
||||
|
||||
// ---- tick bridge ----
|
||||
// ---- tick bridge (standalone mode) ----
|
||||
private final Object lock = new Object();
|
||||
private volatile boolean turnReady = false;
|
||||
private volatile boolean shutdown = false;
|
||||
@@ -100,54 +120,68 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
}
|
||||
|
||||
// ================= 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; }
|
||||
@Override public String getName() { WorldModel w = world; return w != null ? w.name() : name; }
|
||||
@Override public long getTime() { WorldModel w = world; return w != null ? w.time() : time; }
|
||||
@Override public double getEnergy() { WorldModel w = world; return w != null ? w.energy() : energy; }
|
||||
@Override public double getX() { WorldModel w = world; return w != null ? w.x() : x; }
|
||||
@Override public double getY() { WorldModel w = world; return w != null ? w.y() : y; }
|
||||
@Override public double getVelocity() { WorldModel w = world; return w != null ? w.velocity() : velocity; }
|
||||
@Override public double getBodyHeading() { WorldModel w = world; return w != null ? w.bodyHeading() : bodyHeading; }
|
||||
@Override public double getGunHeading() { WorldModel w = world; return w != null ? w.gunHeading() : gunHeading; }
|
||||
@Override public double getRadarHeading() { WorldModel w = world; return w != null ? w.radarHeading() : radarHeading; }
|
||||
@Override public double getGunHeat() { WorldModel w = world; return w != null ? w.gunHeat() : gunHeat; }
|
||||
@Override public double getBattleFieldWidth() { WorldModel w = world; return w != null ? w.battleFieldWidth() : battleFieldWidth; }
|
||||
@Override public double getBattleFieldHeight() { WorldModel w = world; return w != null ? w.battleFieldHeight() : battleFieldHeight; }
|
||||
@Override public int getOthers() { WorldModel w = world; return w != null ? w.others() : others; }
|
||||
@Override public int getNumSentries() { WorldModel w = world; return w != null ? w.numSentries() : numSentries; }
|
||||
@Override public int getNumRounds() { WorldModel w = world; return w != null ? w.numRounds() : numRounds; }
|
||||
@Override public int getRoundNum() { WorldModel w = world; return w != null ? w.roundNum() : roundNum; }
|
||||
@Override public int getSentryBorderSize() { WorldModel w = world; return w != null ? w.sentryBorderSize() : sentryBorderSize; }
|
||||
@Override public double getGunCoolingRate() { WorldModel w = world; return w != null ? w.gunCoolingRate() : gunCoolingRate; }
|
||||
@Override public double getDistanceRemaining() { WorldModel w = world; return w != null ? w.distanceRemaining() : distanceRemaining; }
|
||||
@Override public double getBodyTurnRemaining() { WorldModel w = world; return w != null ? w.bodyTurnRemaining() : bodyTurnRemaining; }
|
||||
@Override public double getGunTurnRemaining() { WorldModel w = world; return w != null ? w.gunTurnRemaining() : gunTurnRemaining; }
|
||||
@Override public double getRadarTurnRemaining() { WorldModel w = world; return w != null ? w.radarTurnRemaining() : 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 void move(double distance) { setMove(distance); }
|
||||
@Override public void turnBody(double radians) { setTurnBody(radians); }
|
||||
@Override public void turnGun(double radians) { setTurnGun(radians); }
|
||||
@Override public void turnRadar(double radians) { setTurnRadar(radians); }
|
||||
|
||||
@Override public Bullet fire(double power) { return makeBullet(power); }
|
||||
@Override public Bullet setFire(double power) { return makeBullet(power); }
|
||||
@Override public void setMove(double distance) {
|
||||
lastMove = distance; moveSet = true;
|
||||
}
|
||||
@Override public void setTurnBody(double radians) { lastTurnBody = radians; turnBodySet = true; }
|
||||
@Override public void setTurnGun(double radians) { lastTurnGun = radians; turnGunSet = true; }
|
||||
@Override public void setTurnRadar(double radians) { lastTurnRadar = radians; turnRadarSet = true; }
|
||||
|
||||
private Bullet makeBullet(double power) {
|
||||
@Override public void setMaxTurnRate(double rate) { maxTurnRate = rate; maxTurnRateSet = true; }
|
||||
@Override public void setMaxVelocity(double velocity) { maxVelocity = velocity; maxVelocitySet = true; }
|
||||
|
||||
@Override public Bullet fire(double power) {
|
||||
TickHost h = tickHost;
|
||||
if (h != null) {
|
||||
return h.onFire(power);
|
||||
}
|
||||
lastFirePower = power;
|
||||
// Bullet(x, y, headingRadians, power, ownerName, victimName, isActive, bulletId)
|
||||
return new Bullet(x, y, gunHeading, power, name, null, true, executedTurns);
|
||||
return makeBullet(power);
|
||||
}
|
||||
|
||||
@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 Bullet setFire(double power) { return fire(power); }
|
||||
|
||||
/**
|
||||
* Standalone-mode bullet (battle mode creates its own via {@link TickHost#onFire}).
|
||||
* NB: the genuine {@code Bullet} constructor is {@code (headingRadians, x, y, power, ...)}.
|
||||
*/
|
||||
private Bullet makeBullet(double power) {
|
||||
return new Bullet(getGunHeading(), getX(), getY(), power, getName(), null, true, executedTurns);
|
||||
}
|
||||
|
||||
@Override public void stop(boolean overwrite) { stopRequested = true; stopSet = true; }
|
||||
@Override public void resume() { stopRequested = false; stopSet = true; }
|
||||
@Override public void setStop(boolean overwrite) { stopRequested = true; stopSet = true; }
|
||||
@Override public void setResume() { stopRequested = false; stopSet = true; }
|
||||
|
||||
@Override public void setAdjustGunForBodyTurn(boolean v) { adjustGunForBodyTurn = v; }
|
||||
@Override public void setAdjustRadarForGunTurn(boolean v) { adjustRadarForGunTurn = v; }
|
||||
@@ -157,6 +191,11 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
@Override public boolean isAdjustRadarForBodyTurn() { return adjustRadarForBodyTurn; }
|
||||
|
||||
@Override public void execute() {
|
||||
TickHost h = tickHost;
|
||||
if (h != null) {
|
||||
h.onExecute();
|
||||
return;
|
||||
}
|
||||
synchronized (lock) {
|
||||
while (!turnReady && !shutdown) {
|
||||
try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
@@ -178,7 +217,7 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
private void dispatch(List<Event> events) {
|
||||
if (robot == null) return;
|
||||
IBasicEvents basic = robot.getBasicEventListener();
|
||||
IAdvancedEvents adv = null;
|
||||
robocode.robotinterfaces.IAdvancedEvents adv = null;
|
||||
if (robot instanceof robocode.robotinterfaces.IAdvancedRobot) {
|
||||
adv = ((robocode.robotinterfaces.IAdvancedRobot) robot).getAdvancedEventListener();
|
||||
}
|
||||
@@ -199,8 +238,13 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Battle mode: forward one already-built classic event to the robot's listeners. */
|
||||
public void deliver(Event e) {
|
||||
dispatch(Collections.singletonList(e));
|
||||
}
|
||||
|
||||
// ================= tick bridge control (driver side) =================
|
||||
/** Driver: publish world state + this turn's events, then unblock the bot's execute(). */
|
||||
/** Standalone driver: publish this turn's events, then unblock the bot's execute(). */
|
||||
public void provideTurn(List<Event> events) {
|
||||
synchronized (lock) {
|
||||
pendingEvents.addAll(events);
|
||||
@@ -210,7 +254,7 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Driver: wait until the bot has finished consuming turn {@code n}. */
|
||||
/** Standalone driver: wait until the bot has finished consuming turn {@code n}. */
|
||||
public void awaitExecutedTurns(int n) {
|
||||
synchronized (lock) {
|
||||
while (executedTurns < n && !shutdown && !dead) {
|
||||
@@ -286,5 +330,41 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
|
||||
@Override public long getDataQuotaAvailable() { return 1024L * 1024L; }
|
||||
|
||||
public List<Event> peekAllEvents() { return Collections.unmodifiableList(allEvents); }
|
||||
// ======================================================================
|
||||
// Battle-mode seams implemented by the Tank Royale bridge.
|
||||
// ======================================================================
|
||||
|
||||
/** Live, already-classic-converted world state read by the peer getters. */
|
||||
public interface WorldModel {
|
||||
String name();
|
||||
long time();
|
||||
double energy();
|
||||
double x();
|
||||
double y();
|
||||
double velocity();
|
||||
double bodyHeading();
|
||||
double gunHeading();
|
||||
double radarHeading();
|
||||
double gunHeat();
|
||||
double battleFieldWidth();
|
||||
double battleFieldHeight();
|
||||
int others();
|
||||
int numSentries();
|
||||
int numRounds();
|
||||
int roundNum();
|
||||
int sentryBorderSize();
|
||||
double gunCoolingRate();
|
||||
double distanceRemaining();
|
||||
double bodyTurnRemaining();
|
||||
double gunTurnRemaining();
|
||||
double radarTurnRemaining();
|
||||
}
|
||||
|
||||
/** Battle-mode tick + fire callbacks. */
|
||||
public interface TickHost {
|
||||
/** The robot asked to fire; return the classic Bullet if accepted, else null. */
|
||||
Bullet onFire(double power);
|
||||
/** The robot ended its turn: apply intents to the TR bot, then advance the TR tick. */
|
||||
void onExecute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
package robocode_shim;
|
||||
|
||||
import dev.robocode.tankroyale.botapi.BaseBot;
|
||||
import dev.robocode.tankroyale.botapi.Bot;
|
||||
import dev.robocode.tankroyale.botapi.BulletState;
|
||||
import dev.robocode.tankroyale.botapi.events.BotDeathEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletFiredEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitBulletEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitWallEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.HitBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.HitByBulletEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.ScannedBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.WonRoundEvent;
|
||||
|
||||
import robocode.robotinterfaces.IBasicRobot;
|
||||
import robocode.util.Utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A genuine Tank Royale bot that hosts the unmodified classic Robocode DrussGT
|
||||
* jar and drives it through {@link ClassicPeer} / {@link BotHost}.
|
||||
*
|
||||
* <p>It is a {@link Bot} (the TR API's classic-movement subclass) so that the
|
||||
* peer's classic intents can be forwarded one-to-one onto the TR Bot API's own
|
||||
* classic motion model:
|
||||
*
|
||||
* <pre>
|
||||
* classic (peer) -> Tank Royale Bot API
|
||||
* setAhead(d) setForward(d)
|
||||
* setTurnRightRadians(r) setTurnRight(toDegrees(r))
|
||||
* setTurnGunRightRadians setTurnGunRight(toDegrees(r))
|
||||
* setTurnRadarRightRadians setTurnRadarRight(toDegrees(r))
|
||||
* setFireBullet(p) setFire(p) + bullet-identity tracking
|
||||
* execute() go()
|
||||
* </pre>
|
||||
*
|
||||
* <p>DrussGT's own {@code run()} is executed on the Tank Royale bot thread
|
||||
* (exactly where the official robocode-api-bridge runs a legacy robot), and its
|
||||
* {@code execute()} calls {@code go()} through {@link ClassicPeer.TickHost}.
|
||||
* TR events are synthesised into classic events and delivered synchronously, so
|
||||
* classic event ordering (TR's event queue priorities) and the classic "events
|
||||
* are delivered before the next turn's run() body" contract both hold.
|
||||
*/
|
||||
public final class DrussGTBridge extends Bot
|
||||
implements ClassicPeer.WorldModel, ClassicPeer.TickHost {
|
||||
|
||||
// ---- DrussGT host ----
|
||||
private BotHost host;
|
||||
private ClassicPeer peer;
|
||||
private IBasicRobot robot;
|
||||
private boolean firstRound = true;
|
||||
|
||||
// ---- bullet identity ----
|
||||
private static final class BulletReflection {
|
||||
static final Field X, Y, ACTIVE;
|
||||
static {
|
||||
Field x = null, y = null, active = null;
|
||||
try {
|
||||
x = robocode.Bullet.class.getDeclaredField("x");
|
||||
x.setAccessible(true);
|
||||
y = robocode.Bullet.class.getDeclaredField("y");
|
||||
y.setAccessible(true);
|
||||
active = robocode.Bullet.class.getDeclaredField("isActive");
|
||||
active.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
// leave null; position/active updates are best-effort
|
||||
}
|
||||
X = x; Y = y; ACTIVE = active;
|
||||
}
|
||||
|
||||
static void setXY(robocode.Bullet b, double x, double y) {
|
||||
try { if (X != null) X.setDouble(b, x); if (Y != null) Y.setDouble(b, y); } catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
static void setActive(robocode.Bullet b, boolean active) {
|
||||
try { if (ACTIVE != null) ACTIVE.setBoolean(b, active); } catch (Exception ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private final List<robocode.Bullet> unmatchedFired = new ArrayList<>();
|
||||
private final Map<Integer, robocode.Bullet> firedById = new HashMap<>();
|
||||
private int nextTempBulletId = -1;
|
||||
private final String botName = envOr("BOT_NAME", "DrussGT");
|
||||
private static final boolean DEBUG = System.getenv("DRUSSGT_BRIDGE_DEBUG") != null;
|
||||
private static PrintWriter debugWriter;
|
||||
|
||||
private static synchronized void debugLog(String line) {
|
||||
if (debugWriter == null) {
|
||||
try {
|
||||
debugWriter = new PrintWriter(new java.io.BufferedWriter(
|
||||
new java.io.FileWriter("/tmp/drussgt_bridge_debug.log", true)));
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
debugWriter.println(line);
|
||||
debugWriter.flush();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
ThreadManagerFix.install();
|
||||
new DrussGTBridge().start();
|
||||
}
|
||||
|
||||
public DrussGTBridge() {
|
||||
super();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// lifecycle
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ensureHost();
|
||||
resetRoundState();
|
||||
|
||||
IBasicRobot bot;
|
||||
if (firstRound) {
|
||||
firstRound = false;
|
||||
bot = host.robot;
|
||||
} else {
|
||||
bot = host.newRobot();
|
||||
}
|
||||
robot = bot;
|
||||
peer.attachRobot(bot);
|
||||
|
||||
// DrussGT's own run() lives on this (the Tank Royale bot) thread.
|
||||
bot.getRobotRunnable().run();
|
||||
} catch (Throwable t) {
|
||||
if (!isInterrupt(t)) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInterrupt(Throwable t) {
|
||||
return t != null && t.getClass().getName().contains("ThreadInterrupted");
|
||||
}
|
||||
|
||||
private void ensureHost() throws Exception {
|
||||
if (host != null) {
|
||||
return;
|
||||
}
|
||||
ThreadManagerFix.install();
|
||||
File jar = new File(envOr("DRUSSGT_JAR", "/tmp/drussgt/DrussGT.jar"));
|
||||
File dataDir = new File(envOr("DRUSSGT_DATA", "/tmp/drussgt_bridge_data"));
|
||||
host = new BotHost(jar, dataDir);
|
||||
peer = host.peer;
|
||||
peer.world = this;
|
||||
peer.tickHost = this;
|
||||
// DrussGT's EnergyDome shield is a separate "precise" subsystem whose
|
||||
// first-round behaviour depends on exact classic bullet-detection timing;
|
||||
// the proven path (see SmokeTest / README) is to run the pure wave
|
||||
// surfer (DrussMoveGT). Set DRUSSGT_SHIELD=1 to keep the shield on.
|
||||
if (!"1".equals(System.getenv("DRUSSGT_SHIELD"))) {
|
||||
host.disableShield();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetRoundState() {
|
||||
unmatchedFired.clear();
|
||||
firedById.clear();
|
||||
nextTempBulletId = -1;
|
||||
if (peer != null) {
|
||||
peer.stopSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String envOr(String key, String fallback) {
|
||||
String v = System.getenv(key);
|
||||
return (v == null || v.isBlank()) ? fallback : v;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ClassicPeer.TickHost — driven from DrussGT's execute()
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void onExecute() {
|
||||
applyIntents();
|
||||
go();
|
||||
}
|
||||
|
||||
private void applyIntents() {
|
||||
if (DEBUG) {
|
||||
debugLog(String.format(java.util.Locale.ROOT,
|
||||
"t=%d move=%s/%.3f body=%.4f gun=%.4f radar=%.4f trDistRem=%.3f trSpeed=%.3f trTurnRem=%.3f trRadarRem=%.3f",
|
||||
getTurnNumber(), peer.moveSet, peer.lastMove, peer.lastTurnBody, peer.lastTurnGun,
|
||||
peer.lastTurnRadar, getDistanceRemaining(), getSpeed(), getTurnRemaining(), getRadarTurnRemaining()));
|
||||
}
|
||||
if (peer.stopSet) {
|
||||
if (peer.stopRequested) {
|
||||
setStop();
|
||||
} else {
|
||||
setResume();
|
||||
}
|
||||
peer.stopSet = false;
|
||||
}
|
||||
if (peer.moveSet) {
|
||||
setForward(peer.lastMove);
|
||||
peer.moveSet = false;
|
||||
}
|
||||
if (peer.turnBodySet) {
|
||||
setTurnRight(Math.toDegrees(peer.lastTurnBody));
|
||||
peer.turnBodySet = false;
|
||||
}
|
||||
if (peer.turnGunSet) {
|
||||
setTurnGunRight(Math.toDegrees(peer.lastTurnGun));
|
||||
peer.turnGunSet = false;
|
||||
}
|
||||
if (peer.turnRadarSet) {
|
||||
setTurnRadarRight(Math.toDegrees(peer.lastTurnRadar));
|
||||
peer.turnRadarSet = false;
|
||||
}
|
||||
if (peer.maxVelocitySet) {
|
||||
setMaxSpeed(peer.maxVelocity);
|
||||
peer.maxVelocitySet = false;
|
||||
}
|
||||
if (peer.maxTurnRateSet) {
|
||||
setMaxTurnRate(peer.maxTurnRate);
|
||||
peer.maxTurnRateSet = false;
|
||||
}
|
||||
setAdjustGunForBodyTurn(peer.adjustGunForBodyTurn);
|
||||
setAdjustRadarForGunTurn(peer.adjustRadarForGunTurn);
|
||||
setAdjustRadarForBodyTurn(peer.adjustRadarForBodyTurn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public robocode.Bullet onFire(double power) {
|
||||
if (Double.isNaN(power) || power <= 0) {
|
||||
return null;
|
||||
}
|
||||
boolean accepted;
|
||||
try {
|
||||
accepted = setFire(power);
|
||||
} catch (RuntimeException ex) {
|
||||
return null;
|
||||
}
|
||||
if (!accepted) {
|
||||
return null;
|
||||
}
|
||||
robocode.Bullet bullet = new robocode.Bullet(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getGunDirection())),
|
||||
getX(), getY(), power, botName, null, true, nextTempBulletId--);
|
||||
unmatchedFired.add(bullet);
|
||||
return bullet;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ClassicPeer.WorldModel — live TR state converted to classic
|
||||
// ======================================================================
|
||||
|
||||
@Override public String name() {
|
||||
return botName;
|
||||
}
|
||||
@Override public long time() { return getTurnNumber(); }
|
||||
@Override public double energy() { return getEnergy(); }
|
||||
@Override public double x() { return getX(); }
|
||||
@Override public double y() { return getY(); }
|
||||
@Override public double velocity() { return getSpeed(); }
|
||||
@Override public double bodyHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getDirection())); }
|
||||
@Override public double gunHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getGunDirection())); }
|
||||
@Override public double radarHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getRadarDirection())); }
|
||||
@Override public double gunHeat() { return getGunHeat(); }
|
||||
@Override public double battleFieldWidth() { return getArenaWidth(); }
|
||||
@Override public double battleFieldHeight() { return getArenaHeight(); }
|
||||
@Override public int others() { return getEnemyCount(); }
|
||||
@Override public int numSentries() { return 0; }
|
||||
@Override public int numRounds() { return getNumberOfRounds(); }
|
||||
@Override public int roundNum() { return getRoundNumber() - 1; }
|
||||
@Override public int sentryBorderSize() { return 0; }
|
||||
@Override public double gunCoolingRate() { return getGunCoolingRate(); }
|
||||
@Override public double distanceRemaining() { return getDistanceRemaining(); }
|
||||
@Override public double bodyTurnRemaining() { return -Math.toRadians(getTurnRemaining()); }
|
||||
@Override public double gunTurnRemaining() { return -Math.toRadians(getGunTurnRemaining()); }
|
||||
@Override public double radarTurnRemaining() { return -Math.toRadians(getRadarTurnRemaining()); }
|
||||
|
||||
// ======================================================================
|
||||
// Tank Royale events -> classic events
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void onScannedBot(ScannedBotEvent e) {
|
||||
double dx = e.getX() - getX();
|
||||
double dy = e.getY() - getY();
|
||||
double distance = Math.hypot(dx, dy);
|
||||
// classic absolute bearing: atan2(dx, dy), 0 = North, clockwise
|
||||
double absBearing = Math.atan2(dx, dy);
|
||||
double bearing = Utils.normalRelativeAngle(absBearing - bodyHeading());
|
||||
double enemyHeading = Utils.normalAbsoluteAngle(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(e.getDirection())));
|
||||
robocode.ScannedRobotEvent classic = new robocode.ScannedRobotEvent(
|
||||
String.valueOf(e.getScannedBotId()), e.getEnergy(), bearing, distance,
|
||||
enemyHeading, e.getSpeed());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitByBullet(HitByBulletEvent e) {
|
||||
BulletState bs = e.getBullet();
|
||||
double bearing = classicBearingTo(bs.getX(), bs.getY());
|
||||
robocode.Bullet bullet = mapBullet(bs, name());
|
||||
robocode.HitByBulletEvent classic = new robocode.HitByBulletEvent(bearing, bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHit(BulletHitBotEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletHitEvent classic = new robocode.BulletHitEvent(
|
||||
String.valueOf(e.getVictimId()), e.getEnergy(), bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHitBullet(BulletHitBulletEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
robocode.Bullet hitBullet = mapBullet(e.getHitBullet(), null);
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletHitBulletEvent classic = new robocode.BulletHitBulletEvent(bullet, hitBullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHitWall(BulletHitWallEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletMissedEvent classic = new robocode.BulletMissedEvent(bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletFired(BulletFiredEvent e) {
|
||||
BulletState bs = e.getBullet();
|
||||
robocode.Bullet match = takeNearestUnmatched(bs.getX(), bs.getY());
|
||||
if (match != null) {
|
||||
firedById.put(bs.getBulletId(), match);
|
||||
BulletReflection.setXY(match, bs.getX(), bs.getY());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitBot(HitBotEvent e) {
|
||||
double bearing = classicBearingTo(e.getX(), e.getY());
|
||||
robocode.HitRobotEvent classic = new robocode.HitRobotEvent(
|
||||
String.valueOf(e.getVictimId()), bearing, e.getEnergy(), e.isRammed());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitWall(dev.robocode.tankroyale.botapi.events.HitWallEvent e) {
|
||||
robocode.HitWallEvent classic = new robocode.HitWallEvent(wallBearing());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBotDeath(BotDeathEvent e) {
|
||||
robocode.RobotDeathEvent classic = new robocode.RobotDeathEvent(String.valueOf(e.getVictimId()));
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWonRound(WonRoundEvent e) {
|
||||
robocode.WinEvent classic = new robocode.WinEvent();
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(dev.robocode.tankroyale.botapi.events.DeathEvent e) {
|
||||
robocode.DeathEvent classic = new robocode.DeathEvent();
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkippedTurn(dev.robocode.tankroyale.botapi.events.SkippedTurnEvent e) {
|
||||
// Deliberately NOT forwarded: classic SkippedTurnEvent is a diagnostic
|
||||
// that only exists because a robot took too long; our bridge never
|
||||
// "skips" a turn, and DrussGT treats it as a fatal signal.
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// helpers
|
||||
// ======================================================================
|
||||
|
||||
private double classicBearingTo(double tx, double ty) {
|
||||
double dx = tx - getX();
|
||||
double dy = ty - getY();
|
||||
return Utils.normalRelativeAngle(Math.atan2(dx, dy) - bodyHeading());
|
||||
}
|
||||
|
||||
/** Classic HitWallEvent bearing to the nearest wall normal. */
|
||||
private double wallBearing() {
|
||||
double dx = getX() - getArenaWidth() / 2.0;
|
||||
double dy = getY() - getArenaHeight() / 2.0;
|
||||
double abs;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
abs = dx >= 0 ? Math.toRadians(270) : Math.toRadians(90);
|
||||
} else {
|
||||
abs = dy >= 0 ? Math.toRadians(180) : 0.0;
|
||||
}
|
||||
return Utils.normalRelativeAngle(abs - bodyHeading());
|
||||
}
|
||||
|
||||
private robocode.Bullet mapBullet(BulletState bs, String victim) {
|
||||
return new robocode.Bullet(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(bs.getDirection())),
|
||||
bs.getX(), bs.getY(), bs.getPower(),
|
||||
String.valueOf(bs.getOwnerId()), victim, false, bs.getBulletId());
|
||||
}
|
||||
|
||||
/** Our own bullet: return the exact instance DrussGT got from setFireBullet. */
|
||||
private robocode.Bullet ownBullet(BulletState bs) {
|
||||
robocode.Bullet b = firedById.get(bs.getBulletId());
|
||||
if (b == null) {
|
||||
b = mapBullet(bs, null);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private robocode.Bullet takeNearestUnmatched(double x, double y) {
|
||||
robocode.Bullet best = null;
|
||||
double bestDist = Double.MAX_VALUE;
|
||||
for (robocode.Bullet b : unmatchedFired) {
|
||||
double d = Math.pow(b.getX() - x, 2) + Math.pow(b.getY() - y, 2);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = b;
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
unmatchedFired.remove(best);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import java.util.Collections;
|
||||
public class SmokeTest {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ThreadManagerFix.install();
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package robocode_shim;
|
||||
|
||||
import net.sf.robocode.core.ContainerBase;
|
||||
import net.sf.robocode.security.IThreadManagerBase;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Kills the ThreadManager landmine documented in README section 5 item 3.
|
||||
*
|
||||
* <p>The genuine {@code robocode.RobocodeFileOutputStream} resolves an
|
||||
* {@link IThreadManagerBase} through {@code ContainerBase.getComponent(...)} and
|
||||
* throws {@code RobotException: ThreadManager cannot be null!} when no engine is
|
||||
* present. DrussGT reaches it from its own {@code contain()} error logger, so
|
||||
* any exception inside a DrussGT event handler used to kill the bot thread.
|
||||
*
|
||||
* <p>{@code ContainerBase.instance} is a public static field and
|
||||
* {@code getBaseComponent} is the single abstract hook, so installing a no-op
|
||||
* thread manager is enough: {@code RobocodeFileOutputStream} then simply writes
|
||||
* a normal file (the same behaviour the real engine gives a robot writing to its
|
||||
* data directory). Nothing else in robocode.jar consults this instance in our
|
||||
* standalone process.
|
||||
*/
|
||||
public final class ThreadManagerFix {
|
||||
|
||||
private ThreadManagerFix() { }
|
||||
|
||||
/** The no-op thread manager: an ordinary file stream, always a "safe" thread. */
|
||||
private static final IThreadManagerBase FILE_THREAD_MANAGER = new IThreadManagerBase() {
|
||||
@Override public boolean isSafeThread() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public FileOutputStream createRobotFileStream(String fileName, boolean append)
|
||||
throws IOException {
|
||||
return new FileOutputStream(fileName, append);
|
||||
}
|
||||
};
|
||||
|
||||
private static volatile boolean installed = false;
|
||||
|
||||
public static void install() {
|
||||
if (installed) {
|
||||
return;
|
||||
}
|
||||
ContainerBase.instance = new ContainerBase() {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> T getBaseComponent(Class<T> componentClass) {
|
||||
if (componentClass == IThreadManagerBase.class) {
|
||||
return (T) FILE_THREAD_MANAGER;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
installed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package robocode_shim;
|
||||
|
||||
import robocode.RobocodeFileOutputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
|
||||
/**
|
||||
* Focused proof of README item 3: outside the engine,
|
||||
* {@code new RobocodeFileOutputStream(...)} throws
|
||||
* {@code RobotException: ThreadManager cannot be null!} and would kill DrussGT's
|
||||
* bot thread when it is reached from {@code DrussGT.contain()}.
|
||||
* {@link ThreadManagerFix#install()} makes it behave like the real engine (a
|
||||
* normal file stream).
|
||||
*/
|
||||
public final class ThreadManagerFixTest {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
File dir = new File(args.length > 0 ? args[0] : "/tmp/shim_threadmgr");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
|
||||
// 1) without the fix
|
||||
String uninstalled;
|
||||
try {
|
||||
new RobocodeFileOutputStream(new File(dir, "no-fix.bin")).close();
|
||||
uninstalled = "NO EXCEPTION (unexpected)";
|
||||
} catch (Throwable t) {
|
||||
uninstalled = t.getClass().getName() + ": " + t.getMessage();
|
||||
}
|
||||
System.out.println("without fix : " + uninstalled);
|
||||
|
||||
// 2) with the fix
|
||||
ThreadManagerFix.install();
|
||||
File out = new File(dir, "with-fix.bin");
|
||||
try (PrintStream ps = new PrintStream(new RobocodeFileOutputStream(out))) {
|
||||
ps.println("drussgt contain() would write here");
|
||||
}
|
||||
System.out.println("with fix : wrote " + out.length() + " bytes to " + out);
|
||||
|
||||
boolean ok = uninstalled.contains("ThreadManager cannot be null")
|
||||
&& out.length() > 0;
|
||||
System.out.println(ok ? "THREADMANAGER FIX: OK" : "THREADMANAGER FIX: FAILED");
|
||||
System.exit(ok ? 0 : 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package robocode_shim;
|
||||
|
||||
import dev.robocode.tankroyale.client.model.BotState;
|
||||
import dev.robocode.tankroyale.client.model.TickEvent;
|
||||
import dev.robocode.tankroyale.runner.BattleResults;
|
||||
import dev.robocode.tankroyale.runner.BattleRunner;
|
||||
import dev.robocode.tankroyale.runner.BattleSetup;
|
||||
import dev.robocode.tankroyale.runner.BotEntry;
|
||||
import dev.robocode.tankroyale.runner.BotResult;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tank Royale analogue of {@code tools/robocode_fixture_capture/Capture.java}:
|
||||
* an observer that runs a real battle via the Tank Royale battle-runner API
|
||||
* (embedded server) and dumps every bot's per-turn state to the project's
|
||||
* shared JSONL fixture format, so {@code tools/robocode_fixture_capture/analyze.py}
|
||||
* can be run on the result unchanged.
|
||||
*
|
||||
* <p>The movement subject (written as {@code e*}) is identified by name
|
||||
* substring; the other bot is written as {@code s*}. Because this uses the
|
||||
* observer tick stream, the positions are perfect information, exactly like the
|
||||
* classic capture.
|
||||
*
|
||||
* <pre>
|
||||
* java -cp out:runner.jar robocode_shim.TrBattleCapture \
|
||||
* --rounds 10 --out /tmp/tr_drussgt.jsonl \
|
||||
* --bot /path/DrussGT --bot /path/Target --subject DrussGT
|
||||
* </pre>
|
||||
*/
|
||||
public final class TrBattleCapture {
|
||||
|
||||
private static int globalTick = 0;
|
||||
private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
|
||||
private static final List<double[]> pendingRows = new ArrayList<>();
|
||||
|
||||
// battle evidence counters (subject = the bot under test)
|
||||
private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0;
|
||||
private static int scansSeen = 0, hitsTaken = 0;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int rounds = 5;
|
||||
String outFile = "/tmp/tr_drussgt.jsonl";
|
||||
String adversary = "unknown";
|
||||
String note = "Tank Royale battle captured via TrBattleCapture (observer tick stream, perfect info)";
|
||||
List<String> botDirs = new ArrayList<>();
|
||||
String subject = "DrussGT";
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "--rounds" -> rounds = Integer.parseInt(args[++i]);
|
||||
case "--out" -> outFile = args[++i];
|
||||
case "--bot" -> botDirs.add(args[++i]);
|
||||
case "--subject" -> subject = args[++i];
|
||||
case "--adversary" -> adversary = args[++i];
|
||||
case "--note" -> note = args[++i];
|
||||
default -> { System.err.println("Unknown arg: " + args[i]); System.exit(2); }
|
||||
}
|
||||
}
|
||||
if (botDirs.isEmpty()) {
|
||||
System.err.println("--bot <dir> required (repeatable)");
|
||||
System.exit(2);
|
||||
}
|
||||
final String subjectPattern = subject;
|
||||
final int numRounds = rounds;
|
||||
|
||||
List<BotEntry> bots = new ArrayList<>();
|
||||
for (String d : botDirs) {
|
||||
bots.add(BotEntry.of(Path.of(d)));
|
||||
}
|
||||
|
||||
Object owner = new Object();
|
||||
try (BattleRunner runner = BattleRunner.create(b -> b.embeddedServer().suppressServerOutput())) {
|
||||
BattleSetup setup = BattleSetup.classic(s -> {
|
||||
s.setNumberOfRounds(numRounds);
|
||||
s.setDefaultTurnsPerSecond(-1);
|
||||
});
|
||||
|
||||
System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern);
|
||||
try (var handle = runner.startBattleAsync(setup, bots)) {
|
||||
handle.getOnTickEvent().on(owner, event -> onTick(event, subjectPattern));
|
||||
BattleResults results = handle.awaitResults();
|
||||
|
||||
writeOutput(outFile, adversary, note, rounds);
|
||||
System.out.println("[capture] rows=" + globalTick + " rounds=" + roundTicks + " -> " + outFile);
|
||||
System.out.println("[capture] subject event counts: scans=" + scansSeen
|
||||
+ " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits
|
||||
+ " bulletMisses=" + bulletMisses + " bulletHitBullets=" + bulletHitBullets
|
||||
+ " hitsTaken=" + hitsTaken);
|
||||
System.out.println("=== RESULTS (" + results.getNumberOfRounds() + " rounds) ===");
|
||||
for (BotResult r : results.getResults()) {
|
||||
System.out.printf(" #%d %-20s totalScore=%d firstPlaces=%d survival=%d%n",
|
||||
r.getRank(), r.getName(), r.getTotalScore(), r.getFirstPlaces(), r.getSurvival());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onTick(TickEvent event, String subjectPattern) {
|
||||
BotState en = null, sh = null;
|
||||
for (BotState bs : event.getBotStates()) {
|
||||
String n = bs.getName();
|
||||
boolean isSubject = n != null && n.toLowerCase().contains(subjectPattern.toLowerCase());
|
||||
if (isSubject) {
|
||||
en = bs;
|
||||
} else if (sh == null) {
|
||||
sh = bs;
|
||||
}
|
||||
}
|
||||
if (en == null || sh == null) {
|
||||
return;
|
||||
}
|
||||
pendingRows.add(new double[]{
|
||||
globalTick++, en.getX(), en.getY(), en.getDirection(), en.getSpeed(), en.getEnergy(),
|
||||
sh.getX(), sh.getY(), sh.getDirection(), sh.getSpeed(), sh.getEnergy()
|
||||
});
|
||||
roundTicks.merge(event.getRoundNumber(), 1, Integer::sum);
|
||||
|
||||
int subjectId = en.getId();
|
||||
for (dev.robocode.tankroyale.client.model.Event e : event.getEvents()) {
|
||||
if (e instanceof dev.robocode.tankroyale.client.model.BulletFiredEvent bf
|
||||
&& bf.getBullet().getOwnerId() == subjectId) {
|
||||
bulletsFired++;
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBotEvent bh
|
||||
&& bh.getBullet().getOwnerId() == subjectId) {
|
||||
bulletHits++;
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBotEvent bh2
|
||||
&& bh2.getVictimId() == subjectId) {
|
||||
hitsTaken++;
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitWallEvent bw
|
||||
&& bw.getBullet().getOwnerId() == subjectId) {
|
||||
bulletMisses++;
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBulletEvent bb
|
||||
&& bb.getBullet().getOwnerId() == subjectId) {
|
||||
bulletHitBullets++;
|
||||
} else if (e instanceof dev.robocode.tankroyale.client.model.ScannedBotEvent sb
|
||||
&& sb.getScannedByBotId() == subjectId) {
|
||||
scansSeen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeOutput(String outFile, String adversary, String note, int rounds) throws Exception {
|
||||
File f = new File(outFile);
|
||||
if (f.getParentFile() != null) {
|
||||
f.getParentFile().mkdirs();
|
||||
}
|
||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
|
||||
w.println("{\"meta\":{\"adversary\":\"" + esc(adversary)
|
||||
+ "\",\"source\":\"tank-royale\",\"perfect_info\":true,"
|
||||
+ "\"arena\":{\"w\":800,\"h\":600},\"note\":\"" + esc(note) + "\"}}");
|
||||
for (double[] r : pendingRows) {
|
||||
w.println("{\"tick\":" + (long) r[0]
|
||||
+ ",\"ex\":" + fmt(r[1]) + ",\"ey\":" + fmt(r[2])
|
||||
+ ",\"eh\":" + fmt(r[3]) + ",\"es\":" + fmt(r[4]) + ",\"ee\":" + fmt(r[5])
|
||||
+ ",\"sx\":" + fmt(r[6]) + ",\"sy\":" + fmt(r[7])
|
||||
+ ",\"sh\":" + fmt(r[8]) + ",\"ss\":" + fmt(r[9]) + ",\"se\":" + fmt(r[10]) + "}");
|
||||
}
|
||||
}
|
||||
try (PrintWriter rs = new PrintWriter(new BufferedWriter(new FileWriter(outFile + ".rounds.json")))) {
|
||||
rs.print("{\"rounds\":[");
|
||||
int start = 0;
|
||||
boolean first = true;
|
||||
for (Map.Entry<Integer, Integer> e : roundTicks.entrySet()) {
|
||||
if (!first) rs.print(",");
|
||||
first = false;
|
||||
rs.print("{\"round\":" + e.getKey() + ",\"startTick\":" + start + ",\"count\":" + e.getValue() + "}");
|
||||
start += e.getValue();
|
||||
}
|
||||
rs.println("]}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String fmt(double v) {
|
||||
return String.format(Locale.ROOT, "%.4f", v);
|
||||
}
|
||||
|
||||
private static String esc(String s) {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user