From ef16dd982d91d82e969fa3e0640cd7e8377429ed Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Sat, 26 Sep 2026 00:25:33 +0200 Subject: [PATCH] robocode_shim: generalize bridge to any legacy bot; validate 28 champions as sparring partners Generalize the classic-Robocode shim so the hosted bot's main class, jar, extra classpath and data directory are configurable (SHIM_BOT_CLASS / SHIM_BOT_JAR / SHIM_EXTRA_CP / SHIM_DATA), with DrussGT kept as the default so every existing script, fixture and generated bot dir behaves identically. - LegacyBotBridge: generalized bridge (DrussGTBridge kept as an alias). - BotHost: (mainClass, jar, extraJars, dataDir); disableShield -> generic disableStaticBoolean. - ClassicPeer: implement ITeamRobotPeer; synthesize StatusEvent each turn; make move/turnBody/turnGun/turnRadar the immediate (turn-ending) variants; guard re-entrant execute() from event handlers; record delivered events. - make_botdir.sh / run_bridge_battle.sh / run_smoke.sh generalized, legacy invocations unchanged; generated launcher uses a per-process mktemp data dir so concurrent bots/battles cannot clobber one classic data directory. Validated 35 legacy bots against a real Tank Royale battle (2 rounds vs sample SpinBot): 28 usable (27 effective + DrussGT), 5 weak-but-playing, 2 failing. Adds robots.json (manifest) and LEGACY_BOTS.md (how-to, status, missing-API costs). No third-party jar is committed. --- tools/robocode_shim/LEGACY_BOTS.md | 196 ++++++ tools/robocode_shim/build.sh | 1 + tools/robocode_shim/make_botdir.sh | 136 ++++- tools/robocode_shim/robots.json | 474 +++++++++++++++ tools/robocode_shim/run_bridge_battle.sh | 41 +- tools/robocode_shim/run_smoke.sh | 13 +- .../src/robocode_shim/BotHost.java | 86 ++- .../src/robocode_shim/ClassicPeer.java | 83 ++- .../src/robocode_shim/DrussGTBridge.java | 452 +------------- .../src/robocode_shim/LegacyBotBridge.java | 556 ++++++++++++++++++ .../src/robocode_shim/SmokeTest.java | 47 +- 11 files changed, 1553 insertions(+), 532 deletions(-) create mode 100644 tools/robocode_shim/LEGACY_BOTS.md create mode 100644 tools/robocode_shim/robots.json create mode 100644 tools/robocode_shim/src/robocode_shim/LegacyBotBridge.java diff --git a/tools/robocode_shim/LEGACY_BOTS.md b/tools/robocode_shim/LEGACY_BOTS.md new file mode 100644 index 0000000..4fcbec5 --- /dev/null +++ b/tools/robocode_shim/LEGACY_BOTS.md @@ -0,0 +1,196 @@ +# Legacy champion bots as Tank Royale sparring partners + +The classic-Robocode bridge in `tools/robocode_shim/` is now **generalized**: it can +host any unmodified classic robot jar, not just DrussGT. This document is the +short version of how to use it and what was validated. The machine-readable +manifest is [`robots.json`](robots.json). + +**No third-party jar is committed.** Every jar lives in `/tmp` and is referenced +by absolute path; the repo only contains the shim, the manifest and this doc. + +## Direct answer + +* **28 legacy champions are usable right now** (27 validated as effective + + DrussGT itself). A further **5 play but are weak/degraded** (they fire and + scan but landed 0 hits against the sample SpinBot). **2 fail** (Nene, + Seraphim — see below). +* Cost to add the top remaining failures: **~2–4 h** to add a second driver path + for bots that implement `IBasicRobot` directly and return `null` from + `getRobotRunnable()` (this is the only hard blocker left; it unlocks Nene and + Seraphim, the two Azuma-framework melee bots), plus ~0.5–1 h to diagnose the + divide-by-zero those bots hit during construction. +* The weak-but-working bots need **~2–6 h of physics-divergence investigation** + (they are not blocked on a missing API — see “Degraded bots” below). + +## Add a bot in 3 commands + +```bash +# 1. download the jar (the RoboRumble archive is browsable) +curl -Lo /tmp/bots/voidious.Diamond_1.8.28.jar \ + http://robocode-archive.strangeautomata.com/robots/voidious.Diamond_1.8.28.jar + +# 2. generate the Tank Royale bot directory +tools/robocode_shim/make_botdir.sh Diamond \ + --class voidious.Diamond --jar /tmp/bots/voidious.Diamond_1.8.28.jar + +# 3. prove it plays a real Tank Royale battle (2 rounds vs the sample SpinBot) +SHIM_BOTDIR=/tmp/tr_bots/Diamond tools/robocode_shim/run_bridge_battle.sh \ + /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 2 +``` + +`make_botdir.sh` accepts `--class`, `--jar`, `--name`, `--version`, `--authors`, +`--desc`, `--country`, `--gametypes`. The legacy forms still work unchanged: + +```bash +tools/robocode_shim/make_botdir.sh # -> /tmp/tr_bots/DrussGT (DrussGT defaults) +tools/robocode_shim/make_botdir.sh /tmp/tr_bots/DrussGT # same, explicit path +``` + +### GUI steps + +1. Launch the Tank Royale GUI. +2. **Bots → Add bot**, and select the generated bot directory `/tmp/tr_bots/` + (or its `.json`). The GUI reads the `.json` identity file. +3. Repeat for each `/tmp/tr_bots/` directory you want in the roster. +4. Start a classic 1v1 (or melee) battle and add the bots as participants. + +The generated `.sh` launcher selects the hosted classic robot via +`SHIM_BOT_CLASS` / `SHIM_BOT_JAR` and creates its own data directory, so the GUI +does not need any extra environment variables. + +### One-line smoke battle for any generated bot + +```bash +SHIM_BOTDIR=/tmp/tr_bots/ tools/robocode_shim/run_bridge_battle.sh \ + /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 2 +``` + +### Offline triage (no server, ~2 s per bot) + +```bash +SHIM_BOT_JAR=/tmp/bots/ SHIM_BOT_CLASS= tools/robocode_shim/run_smoke.sh +``` + +> Caution: this synthetic harness has **false negatives** — Diamond, Dookious and +> Lukious NPE there but play fine in a real battle. Only a real battle is +> authoritative. + +## Status table (MEASURED: 2-round battles vs sample SpinBot, embedded server) + +| # | Name | Style (inferred) | Status | Evidence (fired / hits / taken, 2 rounds) | +|---|---|---|---|---| +| — | DrussGT | DC gun + wave-surfing movement | **REFERENCE** | 141 / 17 / 0 — won 2/2 | +| 1 | Diamond | pattern-matching gun + surf | **WORKS** | 50 / 18 / 1 — won 2/2 | +| 2 | Dookious | surf movement + pattern/GF gun | **WORKS** | 86 / 16 / 1 — won 2/2 | +| 3 | Lukious | wave-surfing movement | **WORKS** | 70 / 15 / 3 — won 2/2 | +| 4 | Shadow | pattern matching (team-derived) | **WORKS** | 25 / 19 / 0 — won 2/2 | +| 5 | Phoenix | pattern matching (team-derived) | **WORKS** | 50 / 17 / 0 — won 2/2 | +| 6 | RougeDC | dynamic-clustering gun + surf | **WORKS** | 67 / 14 / 2 — won 2/2 | +| 7 | DiamondStealer | rammer / brawler | **WORKS** | 18 / 12 / 1 — won 2/2 | +| 8 | GresSuffurd | surf gun + movement | **WORKS** | 72 / 15 / 0 — won 2/2 | +| 9 | KurtWaveSurfer | wave surfer | **WORKS** | 84 / 13 / 1 — won 2/2 | +| 10 | CassiusClay | pattern-matching gun + surf | **WORKS** | 55 / 11 / 0 — won 2/2 | +| 11 | Coriantumr | mini pattern matcher | **WORKS** | 41 / 14 / 1 — won 2/2 | +| 12 | BrokenSword | mini pattern matcher | **WORKS** | 38 / 15 / 3 — won 2/2 | +| 13 | WaveSurferGF | wave surfer (GF gun) | **WORKS** | 49 / 13 / 1 — won 2/2 | +| 14 | WaveSurferPG | wave surfer (pattern gun) | **WORKS** | 57 / 16 / 3 — won 2/2 | +| 15 | TripHammer | pattern matcher | **WORKS** | 52 / 19 / 1 — won 2/2 | +| 16 | RetroGirl | perceptual pattern matcher | **WORKS** | 106 / 26 / 5 — won 2/2 | +| 17 | Jen | micro wave surfer | **WORKS** | 71 / 13 / 2 — won 2/2 | +| 18 | BlitzBat | micro brawler | **WORKS** | 34 / 12 / 1 — won 2/2 | +| 19 | Ascendant | aggressive 1v1 mega | **WORKS** | 74 / 15 / 0 — won 2/2 | +| 20 | DiamondHawk | pattern matcher | **WORKS** | 45 / 18 / 0 — won 2/2 | +| 21 | YersiniaPestis | strong aggressive mega | **WORKS** | 52 / 12 / 0 — won 2/2 | +| 22 | HawkOnFire | corner camper / low bot | **WORKS** | 74 / 12 / 0 — won 2/2 | +| 23 | WallAvoider | micro wall-avoider (blocking style) | **WORKS** | 47 / 10 / 1 — won 2/2 | +| 24 | Cigaret | mini surfer | **WORKS** | 46 / 5 / 5 — split 1-1 | +| 25 | CigaretBH | mini bullet-hiding surfer | **WORKS** | 67 / 11 / 3 — split 1-1 | +| 26 | Komarious | mini pattern matcher / surfer | **WORKS** | 62 / 20 / 10 — split 1-1 | +| 27 | LionWWSVMvoid | wave-surfing SVM gun | **WORKS** | 62 / 12 / 12 — lost 0-2 | +| 28 | FloodMini | mini flood-fill movement | WORKS_WEAK | 25 / 0 / 10 — lost 0-2 | +| 29 | Aristocles | micro surfer | WORKS_WEAK | 39 / 0 / 10 — lost 0-2 | +| 30 | PatternRobot | simple pattern robot | WORKS_WEAK | 8 / 0 / 2 — lost 0-2 | +| 31 | LightningBug | pattern matcher | WORKS_WEAK | 36 / 0 / 10 — lost 0-2 | +| 32 | Aurora | micro (team-derived) | WORKS_WEAK | 6 / 0 / 13 — lost 0-2 | +| 33 | Nene | melee (Azuma framework) | **FAILS** | 0 / 0 / 14 — inert | +| 34 | Seraphim | melee (Azuma framework) | **FAILS** | 0 / 0 / 14 — inert | + +`WORKS` = the battle completed, the bot scanned, moved and fired, and landed hits. +`WORKS_WEAK` = the battle completed and the bot scanned/fired, but landed **0** +hits against SpinBot; usable as a target/movement sparring partner, not as a +gun-quality reference. Losing to SpinBot is not itself a failure (LionWWSVMvoid +lost but is classified WORKS because it landed 12 hits and clearly fights). + +## Missing-API summary + +Four classic-Robocode APIs were missing from the original shim. Three were +**implemented in this change** because several bots failed on the same call — +each is now a one-time cost of ~0.5–1 h (already paid): + +| API | Was missing | Cost | Unlocked | +|---|---|---|---| +| `robocode.robotinterfaces.peer.ITeamRobotPeer` (`getTeammates`, `isTeammate`, `broadcastMessage`, `sendMessage`, `getMessageEvents`) | `TeamRobot.setPeer()` casts the peer to it, so every `TeamRobot`-derived bot died with `ClassCastException` | **DONE** (~1 h) | Shadow, Phoenix, RougeDC, DiamondStealer, Ascendant, Aurora | +| `robocode.StatusEvent` / `RobotStatus` synthesis each turn | never delivered; RougeDC busy-loops forever waiting for one (`ensureVersion` → “Got no status event! Dying now!”) | **DONE** (~1 h) | RougeDC; generally required by event-driven bots | +| Non-advanced **immediate** semantics of `peer.move` / `turnBody` / `turnGun` / `turnRadar` | they only set the intent; classic performs the turn **and ends the turn** (`Robot.ahead`, `AdvancedRobot.turnRadarRightRadians`, …). Bots whose `run()` is `while (true) turnRadarRightRadians(1);` spun forever | **DONE** (~0.5 h) | Komarious, KurtWaveSurfer, Cigaret/CigaretBH, RetroGirl, Jen, WallAvoider, PatternRobot, … | +| Re-entrancy guard for `execute()` called from inside an event handler | classic blocking robots call `Robot.fire()` inside `onScannedRobot`; the nested tick deadlocked | **DONE** (~0.5 h) | WallAvoider, PatternRobot | + +**Still missing (not implemented):** + +| Gap | Symptom | Bots | Cost estimate | +|---|---|---|---| +| Alternate driver for bots that implement `IBasicRobot`/`ITeamRobot` **directly** instead of extending `robocode.Robot` | `getRobotRunnable()` returns `null`; the bot is inert in battle. `cs.Azuma` even extends `java.io.PrintStream`. | Nene, Seraphim | **2–4 h** (second driver path: detect the null runnable and drive the bot’s own `execute()`/event listener instead). Plus **0.5–1 h** to diagnose the `ArithmeticException: divide by zero` Nene throws during construction. | +| Degraded gun/movement for several micro/mini bots | they scan and fire but land 0 hits; not a missing API — almost certainly the documented physics divergences (move/turn ordering, distance bookkeeping; see README §5.9) | FloodMini, Aristocles, PatternRobot, LightningBug, Aurora | **2–6 h** of measurement/diagnosis; not blocking (they still play). | + +## Isolation guarantees (parallel battles) + +* **Data directory:** the generated `.sh` sets + `SHIM_DATA="${SHIM_DATA:-${DRUSSGT_DATA:-$(mktemp -d …)}}"` — a fresh + per-process directory. Two bots, or two concurrent battles with the same bot, + can never share or clobber a classic data directory. Callers can still pin a + directory via `SHIM_DATA` (new) or `DRUSSGT_DATA` (legacy, e.g. `tools/ab/`). +* **Bot selection:** `SHIM_BOT_CLASS` + `SHIM_BOT_JAR` (+ optional + `SHIM_EXTRA_CP` for dependencies) are per-bot, so different legacy bots can + run concurrently in different JVMs. +* **Ports / processes:** the Tank Royale embedded server picks ephemeral ports + and each battle is a separate process (the existing `tools/ab` machinery + already relies on this and is unchanged). + +## Generalizing the shim (what changed) + +* `src/robocode_shim/LegacyBotBridge.java` — the generalized bridge (the old + DrussGT-specific logic, now configurable). Defaults reproduce DrussGT exactly: + `SHIM_BOT_CLASS=jk.mega.DrussGT`, `SHIM_BOT_JAR=${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}`, + `SHIM_DATA=${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}`, `DRUSSGT_SHIELD=1` still + keeps the EnergyDome shield on. +* `src/robocode_shim/DrussGTBridge.java` — now a thin backwards-compatible alias + so already-generated bot directories keep launching. +* `src/robocode_shim/BotHost.java` — takes `(mainClass, jar, extraJars, dataDir)`; + `disableShield()` generalized to `disableStaticBoolean(field)` (a silent no-op + for bots without the field). +* `src/robocode_shim/ClassicPeer.java` — `ITeamRobotPeer`, `StatusEvent` + synthesis, immediate `move`/`turn*` semantics, re-entrant `execute()` guard, + delivered events recorded in `getAllEvents()`. +* `make_botdir.sh`, `run_bridge_battle.sh`, `run_smoke.sh` — generalized; all + legacy invocations (no args, positional path, `DRUSSGT_BOTDIR`/`DRUSSGT_DATA`) + still behave identically. + +## Reproducing the validation + +```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 +tools/robocode_shim/build.sh +tools/robocode_shim/compile_drussgt.sh # DrussGT acceptance: 20/22 files, 43 classes +tools/robocode_shim/run_smoke.sh # DrussGT default path unchanged: OK +tools/robocode_shim/run_bridge_battle.sh \ + /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 2 +``` + +### MEASURED vs INFERRED + +* **MEASURED:** every status in the table, all fire/hit/taken counts, the + missing-API symptoms (exception + method), the jar HTTP 200 downloads, the + DrussGT default-path smoke/acceptance/battle. +* **INFERRED:** the “style” column (from bot names/docs, not decompilation) and + the cost estimates. The cause of the degraded micro/mini bots is inferred + from the known physics divergences, not isolated to a single call. diff --git a/tools/robocode_shim/build.sh b/tools/robocode_shim/build.sh index c1807e5..4cd9174 100755 --- a/tools/robocode_shim/build.sh +++ b/tools/robocode_shim/build.sh @@ -32,6 +32,7 @@ javac -nowarn -cp "$ROBOCODE_JAR:$TR_BOT_API_JAR" -d "$OUT" \ "$HERE/src/robocode_shim/TankRoyaleBridge.java" \ "$HERE/src/robocode_shim/ThreadManagerFix.java" \ "$HERE/src/robocode_shim/ThreadManagerFixTest.java" \ + "$HERE/src/robocode_shim/LegacyBotBridge.java" \ "$HERE/src/robocode_shim/DrussGTBridge.java" \ "$HERE/src/robocode_shim/SmokeTest.java" echo "shim built -> $OUT" diff --git a/tools/robocode_shim/make_botdir.sh b/tools/robocode_shim/make_botdir.sh index 267613e..9773abf 100755 --- a/tools/robocode_shim/make_botdir.sh +++ b/tools/robocode_shim/make_botdir.sh @@ -1,30 +1,107 @@ #!/usr/bin/env bash -# Generate a Tank Royale bot directory that boots the DrussGT bridge. +# Generate a Tank Royale bot directory that boots a classic Robocode robot +# through the robocode_shim bridge. # -# tools/robocode_shim/make_botdir.sh [TARGET_DIR] +# tools/robocode_shim/make_botdir.sh [TARGET|NAME] \ +# [--class FQCN] [--jar PATH] [--name DISPLAY] \ +# [--version V] [--authors A] [--desc D] [--country CC] [--gametypes G] +# +# The first positional argument may be either +# * a directory path (legacy form, e.g. /tmp/tr_bots/DrussGT), or +# * a bare bot name, in which case the dir is /tmp/tr_bots/. +# With no arguments at all it reproduces the original DrussGT directory. # # The generated directory contains only text files (.json + .sh); the # third-party jars stay outside the repo and are referenced by absolute path. +# +# Defaults (so the original no-arg DrussGT invocation is byte-for-byte usable): +# --class jk.mega.DrussGT +# --jar $DRUSSGT_JAR (/tmp/drussgt/DrussGT.jar) +# display name = bot-dir basename, version parsed from the jar name (or 3.1.4159) +# +# Data-directory isolation: the generated launcher sets SHIM_DATA to a fresh +# per-process mktemp directory unless the caller already provided SHIM_DATA / +# DRUSSGT_DATA. Two bots (or two concurrent battles) therefore can never share +# or clobber one classic data directory. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TARGET="${1:-/tmp/tr_bots/DrussGT}" -NAME="$(basename "$TARGET")" + +TARGET="" +CLASS="" +JAR="" +DISPLAY="" +VERSION="" +AUTHORS="" +DESC="" +COUNTRY="se" +GAMETYPES="classic,1v1" + +if [[ $# -gt 0 && "${1}" != --* ]]; then + TARGET="$1"; shift +fi +while [[ $# -gt 0 ]]; do + case "$1" in + --class) CLASS="$2"; shift 2;; + --jar) JAR="$2"; shift 2;; + --name) DISPLAY="$2"; shift 2;; + --version) VERSION="$2"; shift 2;; + --authors) AUTHORS="$2"; shift 2;; + --desc) DESC="$2"; shift 2;; + --country) COUNTRY="$2"; shift 2;; + --gametypes) GAMETYPES="$2"; shift 2;; + -h|--help) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0;; + *) echo "unknown argument: $1" >&2; exit 2;; + esac +done + +# --- resolve target dir + name (legacy forms preserved) --- +if [[ -z "$TARGET" ]]; then + TARGET="/tmp/tr_bots/DrussGT" +elif [[ "$TARGET" != */* ]]; then + TARGET="/tmp/tr_bots/$TARGET" +fi +DIR_NAME="$(basename "$TARGET")" + +DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}" +CLASS="${CLASS:-jk.mega.DrussGT}" +JAR="${JAR:-$DRUSSGT_JAR}" +NAME="${DISPLAY:-$DIR_NAME}" + +# --- defaults that keep the DrussGT case identical to the old script --- +if [[ -z "$VERSION" ]]; then + if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then + VERSION="3.1.4159" + else + stem="$(basename "$JAR")"; stem="${stem%.jar}" + if [[ "$stem" == *_* ]]; then VERSION="${stem##*_}"; else VERSION="1.0"; fi + fi +fi +[[ -n "$AUTHORS" ]] || { if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then AUTHORS="Jacob"; else AUTHORS="unknown"; fi; } +[[ -n "$DESC" ]] || { if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then + DESC="Real classic DrussGT running on Tank Royale via the robocode_shim bridge" +else + DESC="Classic Robocode robot ${CLASS} running on Tank Royale via the robocode_shim bridge" +fi; } +GAMETYPES_JSON="$(printf '%s' "$GAMETYPES" | sed 's/,/\", \"/g')" 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}" +EXTRA_CP="${SHIM_EXTRA_CP:-}" +EXTRA_LINE="" +[[ -n "$EXTRA_CP" ]] && EXTRA_LINE="export SHIM_EXTRA_CP=\"$EXTRA_CP\"" mkdir -p "$TARGET" -cat > "$TARGET/$NAME.json" < "$TARGET/$DIR_NAME.json" < "$TARGET/$NAME.sh" < "$TARGET/$DIR_NAME.sh" < --class --jar ", + "bots": [ + { + "name": "DrussGT", + "main_class": "jk.mega.DrussGT", + "jar": "/tmp/drussgt/DrussGT.jar", + "version": "3.1.4159", + "style": "dynamic-clustering gun + wave-surfing movement (Voidious-style mega)", + "status": "REFERENCE", + "obtained": "pre-existing (spike asset)", + "source_url": "", + "license": "not stated in jar", + "botdir": "/tmp/tr_bots/DrussGT", + "evidence": "run_bridge_battle.sh default path, 2 rounds vs SpinBot: fired 141, hits 17; won 2/2" + }, + { + "name": "Diamond", + "main_class": "voidious.Diamond", + "jar": "/tmp/bots/voidious.Diamond_1.8.28.jar", + "version": "1.8.28", + "style": "pattern-matching gun + wave-surfing movement", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.Diamond_1.8.28.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Diamond", + "evidence": "2 rounds vs SpinBot: scans=783 bulletsFired=50 bulletHits=18 bulletMisses=29 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "Dookious", + "main_class": "voidious.Dookious", + "jar": "/tmp/bots/voidious.Dookious_1.60.jar", + "version": "1.60", + "style": "wave-surfing movement + pattern/GF gun", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.Dookious_1.60.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Dookious", + "evidence": "2 rounds vs SpinBot: scans=396 bulletsFired=86 bulletHits=16 bulletMisses=70 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "Lukious", + "main_class": "voidious.Lukious", + "jar": "/tmp/bots/voidious.Lukious_1.321.jar", + "version": "1.321", + "style": "wave-surfing movement", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.Lukious_1.321.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Lukious", + "evidence": "2 rounds vs SpinBot: scans=191 bulletsFired=70 bulletHits=15 bulletMisses=53 bulletHitBullets=0 hitsTaken=3; result unknown" + }, + { + "name": "Shadow", + "main_class": "abc.Shadow", + "jar": "/tmp/bots/abc.Shadow_3.84i.jar", + "version": "3.84i", + "style": "pattern-matching (team-derived, runs 1v1)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/abc.Shadow_3.84i.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Shadow", + "evidence": "2 rounds vs SpinBot: scans=410 bulletsFired=25 bulletHits=19 bulletMisses=5 bulletHitBullets=1 hitsTaken=0; result unknown" + }, + { + "name": "Phoenix", + "main_class": "davidalves.Phoenix", + "jar": "/tmp/bots/davidalves.Phoenix_1.02.jar", + "version": "1.02", + "style": "pattern-matching (team-derived, runs 1v1)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/davidalves.Phoenix_1.02.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Phoenix", + "evidence": "2 rounds vs SpinBot: scans=735 bulletsFired=50 bulletHits=17 bulletMisses=29 bulletHitBullets=0 hitsTaken=0; result unknown" + }, + { + "name": "RougeDC", + "main_class": "ags.rougedc.RougeDC", + "jar": "/tmp/bots/ags.rougedc.RougeDC_willow.jar", + "version": "willow", + "style": "dynamic-clustering gun + surfing (melee-capable)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/ags.rougedc.RougeDC_willow.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/RougeDC", + "evidence": "2 rounds vs SpinBot: scans=113 bulletsFired=67 bulletHits=14 bulletMisses=49 bulletHitBullets=0 hitsTaken=2; result unknown" + }, + { + "name": "DiamondStealer", + "main_class": "jab.DiamondStealer", + "jar": "/tmp/bots/jab.DiamondStealer_5.jar", + "version": "5", + "style": "rammer / close-range brawler", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/jab.DiamondStealer_5.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/DiamondStealer", + "evidence": "2 rounds vs SpinBot: scans=305 bulletsFired=18 bulletHits=12 bulletMisses=3 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "GresSuffurd", + "main_class": "gh.GresSuffurd", + "jar": "/tmp/bots/gh.GresSuffurd_0.4.18.jar", + "version": "0.4.18", + "style": "wave-surfing gun + movement", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/gh.GresSuffurd_0.4.18.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/GresSuffurd", + "evidence": "2 rounds vs SpinBot: scans=432 bulletsFired=72 bulletHits=15 bulletMisses=55 bulletHitBullets=1 hitsTaken=0; result unknown" + }, + { + "name": "KurtWaveSurfer", + "main_class": "kurt.robot.KurtWaveSurfer", + "jar": "/tmp/bots/kurt.robot.KurtWaveSurfer_1.6.jar", + "version": "1.6", + "style": "wave surfer", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/kurt.robot.KurtWaveSurfer_1.6.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/KurtWaveSurfer", + "evidence": "2 rounds vs SpinBot: scans=245 bulletsFired=84 bulletHits=13 bulletMisses=69 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "CassiusClay", + "main_class": "pez.rumble.CassiusClay", + "jar": "/tmp/bots/pez.rumble.CassiusClay_2rho.02no.jar", + "version": "2rho.02no", + "style": "pattern-matching gun + surf movement", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/pez.rumble.CassiusClay_2rho.02no.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/CassiusClay", + "evidence": "2 rounds vs SpinBot: scans=929 bulletsFired=55 bulletHits=11 bulletMisses=39 bulletHitBullets=1 hitsTaken=0; result unknown" + }, + { + "name": "Coriantumr", + "main_class": "kawigi.mini.Coriantumr", + "jar": "/tmp/bots/kawigi.mini.Coriantumr_1.1.jar", + "version": "1.1", + "style": "mini pattern matcher", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/kawigi.mini.Coriantumr_1.1.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Coriantumr", + "evidence": "2 rounds vs SpinBot: scans=1113 bulletsFired=41 bulletHits=14 bulletMisses=24 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "BrokenSword", + "main_class": "voidious.mini.BrokenSword", + "jar": "/tmp/bots/voidious.mini.BrokenSword_2.0.3.jar", + "version": "2.0.3", + "style": "mini pattern matcher", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.mini.BrokenSword_2.0.3.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/BrokenSword", + "evidence": "2 rounds vs SpinBot: scans=617 bulletsFired=38 bulletHits=15 bulletMisses=18 bulletHitBullets=0 hitsTaken=3; result unknown" + }, + { + "name": "WaveSurferGF", + "main_class": "suh.mega.WaveSurferGF", + "jar": "/tmp/bots/suh.mega.WaveSurferGF_1.04.jar", + "version": "1.04", + "style": "wave surfer (guess-factor gun)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/suh.mega.WaveSurferGF_1.04.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/WaveSurferGF", + "evidence": "2 rounds vs SpinBot: scans=835 bulletsFired=49 bulletHits=13 bulletMisses=33 bulletHitBullets=1 hitsTaken=1; result unknown" + }, + { + "name": "WaveSurferPG", + "main_class": "suh.mega.WaveSurferPG", + "jar": "/tmp/bots/suh.mega.WaveSurferPG_1.06.jar", + "version": "1.06", + "style": "wave surfer (pattern gun)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/suh.mega.WaveSurferPG_1.06.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/WaveSurferPG", + "evidence": "2 rounds vs SpinBot: scans=917 bulletsFired=57 bulletHits=16 bulletMisses=41 bulletHitBullets=0 hitsTaken=3; result unknown" + }, + { + "name": "TripHammer", + "main_class": "voidious.TripHammer", + "jar": "/tmp/bots/voidious.TripHammer_R2.1.0.jar", + "version": "R2.1.0", + "style": "pattern matcher", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.TripHammer_R2.1.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/TripHammer", + "evidence": "2 rounds vs SpinBot: scans=812 bulletsFired=52 bulletHits=19 bulletMisses=29 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "RetroGirl", + "main_class": "voidious.perceptual.RetroGirl", + "jar": "/tmp/bots/voidious.perceptual.RetroGirl_1.0.0.jar", + "version": "1.0.0", + "style": "perceptual pattern matcher", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.perceptual.RetroGirl_1.0.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/RetroGirl", + "evidence": "2 rounds vs SpinBot: scans=223 bulletsFired=106 bulletHits=26 bulletMisses=77 bulletHitBullets=0 hitsTaken=5; result unknown" + }, + { + "name": "Jen", + "main_class": "voidious.micro.Jen", + "jar": "/tmp/bots/voidious.micro.Jen_1.092.jar", + "version": "1.092", + "style": "micro wave surfer", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.micro.Jen_1.092.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Jen", + "evidence": "2 rounds vs SpinBot: scans=170 bulletsFired=71 bulletHits=13 bulletMisses=55 bulletHitBullets=0 hitsTaken=2; result unknown" + }, + { + "name": "BlitzBat", + "main_class": "voidious.micro.BlitzBat", + "jar": "/tmp/bots/voidious.micro.BlitzBat_1.05.jar", + "version": "1.05", + "style": "micro brawler", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.micro.BlitzBat_1.05.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/BlitzBat", + "evidence": "2 rounds vs SpinBot: scans=79 bulletsFired=34 bulletHits=12 bulletMisses=16 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "Ascendant", + "main_class": "mue.Ascendant", + "jar": "/tmp/bots/mue.Ascendant_1.2.27.jar", + "version": "1.2.27", + "style": "aggressive 1v1 mega (team-derived base)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/mue.Ascendant_1.2.27.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Ascendant", + "evidence": "2 rounds vs SpinBot: scans=1127 bulletsFired=74 bulletHits=15 bulletMisses=56 bulletHitBullets=1 hitsTaken=0; result unknown" + }, + { + "name": "DiamondHawk", + "main_class": "wiki.DiamondHawk", + "jar": "/tmp/bots/wiki.DiamondHawk_1.02.jar", + "version": "1.02", + "style": "pattern matcher", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/wiki.DiamondHawk_1.02.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/DiamondHawk", + "evidence": "2 rounds vs SpinBot: scans=703 bulletsFired=45 bulletHits=18 bulletMisses=22 bulletHitBullets=1 hitsTaken=0; result unknown" + }, + { + "name": "YersiniaPestis", + "main_class": "zyx.mega.YersiniaPestis", + "jar": "/tmp/bots/zyx.mega.YersiniaPestis_4.0.jar", + "version": "4.0", + "style": "strong aggressive 1v1 mega", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/zyx.mega.YersiniaPestis_4.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/YersiniaPestis", + "evidence": "2 rounds vs SpinBot: scans=842 bulletsFired=52 bulletHits=12 bulletMisses=38 bulletHitBullets=0 hitsTaken=0; result unknown" + }, + { + "name": "HawkOnFire", + "main_class": "rz.HawkOnFire", + "jar": "/tmp/bots/rz.HawkOnFire_0.1.jar", + "version": "0.1", + "style": "corner camper / low bot", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/rz.HawkOnFire_0.1.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/HawkOnFire", + "evidence": "2 rounds vs SpinBot: scans=1152 bulletsFired=74 bulletHits=12 bulletMisses=56 bulletHitBullets=0 hitsTaken=0; result unknown" + }, + { + "name": "WallAvoider", + "main_class": "sp.Micros.WallAvoider", + "jar": "/tmp/bots/sp.Micros.WallAvoider_1.0.jar", + "version": "1.0", + "style": "micro wall-avoider (blocking Robot style)", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/sp.Micros.WallAvoider_1.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/WallAvoider", + "evidence": "2 rounds vs SpinBot: scans=718 bulletsFired=47 bulletHits=10 bulletMisses=33 bulletHitBullets=0 hitsTaken=1; result unknown" + }, + { + "name": "Cigaret", + "main_class": "cx.mini.Cigaret", + "jar": "/tmp/bots/cx.mini.Cigaret_1.31.jar", + "version": "1.31", + "style": "mini surfer", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/cx.mini.Cigaret_1.31.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Cigaret", + "evidence": "2 rounds vs SpinBot: scans=266 bulletsFired=46 bulletHits=5 bulletMisses=37 bulletHitBullets=0 hitsTaken=5; result unknown" + }, + { + "name": "CigaretBH", + "main_class": "cx.CigaretBH", + "jar": "/tmp/bots/cx.CigaretBH_1.03.jar", + "version": "1.03", + "style": "mini bullet-hiding surfer", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/cx.CigaretBH_1.03.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/CigaretBH", + "evidence": "2 rounds vs SpinBot: scans=237 bulletsFired=67 bulletHits=11 bulletMisses=53 bulletHitBullets=0 hitsTaken=3; result unknown" + }, + { + "name": "Komarious", + "main_class": "voidious.mini.Komarious", + "jar": "/tmp/bots/voidious.mini.Komarious_1.88.jar", + "version": "1.88", + "style": "mini pattern matcher / surfer", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/voidious.mini.Komarious_1.88.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Komarious", + "evidence": "2 rounds vs SpinBot: scans=134 bulletsFired=62 bulletHits=20 bulletMisses=36 bulletHitBullets=1 hitsTaken=10; result unknown" + }, + { + "name": "LionWWSVMvoid", + "main_class": "sqTank.waveSurfing.LionWWSVMvoid", + "jar": "/tmp/bots/sqTank.waveSurfing.LionWWSVMvoid_0.01.jar", + "version": "0.01", + "style": "wave-surfing SVM gun", + "status": "WORKS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/sqTank.waveSurfing.LionWWSVMvoid_0.01.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/LionWWSVMvoid", + "evidence": "2 rounds vs SpinBot: scans=137 bulletsFired=62 bulletHits=12 bulletMisses=45 bulletHitBullets=0 hitsTaken=12; result unknown" + }, + { + "name": "FloodMini", + "main_class": "kawigi.sbf.FloodMini", + "jar": "/tmp/bots/kawigi.sbf.FloodMini_1.4.jar", + "version": "1.4", + "style": "mini flood-fill movement", + "status": "WORKS_WEAK", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/kawigi.sbf.FloodMini_1.4.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/FloodMini", + "evidence": "2 rounds vs SpinBot: scans=94 bulletsFired=25 bulletHits=0 bulletMisses=23 bulletHitBullets=0 hitsTaken=10; result unknown", + "note": "battle completes and the bot scans/fires, but it landed 0 hits vs SpinBot and lost both rounds; treat as a weak/possibly-degraded sparring partner, not a champion." + }, + { + "name": "Aristocles", + "main_class": "pez.micro.Aristocles", + "jar": "/tmp/bots/pez.micro.Aristocles_0.3.9.jar", + "version": "0.3.9", + "style": "micro surfer", + "status": "WORKS_WEAK", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/pez.micro.Aristocles_0.3.9.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Aristocles", + "evidence": "2 rounds vs SpinBot: scans=89 bulletsFired=39 bulletHits=0 bulletMisses=38 bulletHitBullets=0 hitsTaken=10; result unknown", + "note": "battle completes and the bot scans/fires, but it landed 0 hits vs SpinBot and lost both rounds; treat as a weak/possibly-degraded sparring partner, not a champion." + }, + { + "name": "PatternRobot", + "main_class": "stelo.PatternRobot", + "jar": "/tmp/bots/stelo.PatternRobot_1.0.jar", + "version": "1.0", + "style": "simple pattern robot (blocking Robot style)", + "status": "WORKS_WEAK", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/stelo.PatternRobot_1.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/PatternRobot", + "evidence": "2 rounds vs SpinBot: scans=220 bulletsFired=8 bulletHits=0 bulletMisses=6 bulletHitBullets=0 hitsTaken=2; result unknown", + "note": "battle completes and the bot scans/fires, but it landed 0 hits vs SpinBot and lost both rounds; treat as a weak/possibly-degraded sparring partner, not a champion." + }, + { + "name": "LightningBug", + "main_class": "ry.LightningBug", + "jar": "/tmp/bots/ry.LightningBug_1.0.jar", + "version": "1.0", + "style": "pattern matcher", + "status": "WORKS_WEAK", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/ry.LightningBug_1.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/LightningBug", + "evidence": "2 rounds vs SpinBot: scans=80 bulletsFired=36 bulletHits=0 bulletMisses=31 bulletHitBullets=0 hitsTaken=10; result unknown", + "note": "battle completes and the bot scans/fires, but it landed 0 hits vs SpinBot and lost both rounds; treat as a weak/possibly-degraded sparring partner, not a champion." + }, + { + "name": "Aurora", + "main_class": "dmp.micro.Aurora", + "jar": "/tmp/bots/dmp.micro.Aurora_1.41.jar", + "version": "1.41", + "style": "micro (team-derived base)", + "status": "WORKS_WEAK", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/dmp.micro.Aurora_1.41.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Aurora", + "evidence": "2 rounds vs SpinBot: scans=69 bulletsFired=6 bulletHits=0 bulletMisses=6 bulletHitBullets=0 hitsTaken=13; result unknown", + "note": "battle completes and the bot scans/fires, but it landed 0 hits vs SpinBot and lost both rounds; treat as a weak/possibly-degraded sparring partner, not a champion." + }, + { + "name": "Nene", + "main_class": "cs.Nene", + "jar": "/tmp/bots/cs.Nene_N0.jar", + "version": "N0", + "style": "melee (Azuma framework)", + "status": "FAILS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/cs.Nene_N0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Nene", + "evidence": "2 rounds vs SpinBot: scans=0 bulletsFired=0 bulletHits=0 bulletMisses=0 bulletHitBullets=0 hitsTaken=14; result unknown", + "note": "battle completes but the bot is inert (0 scans, 0 shots). See missing_api.", + "missing_api": "getRobotRunnable() returns null: the Azuma framework implements robocode.robotinterfaces.IBasicRobot/ITeamRobot directly (cs.Azuma extends java.io.PrintStream) instead of extending robocode.Robot, and returns null from getRobotRunnable(); the bridge has no driver path for that. SmokeTest also throws ArithmeticException (divide by zero) for cs.Nene." + }, + { + "name": "Seraphim", + "main_class": "cs.s3.Seraphim", + "jar": "/tmp/bots/cs.s3.Seraphim_3.0.jar", + "version": "3.0", + "style": "melee (Azuma framework)", + "status": "FAILS", + "obtained": "downloaded jar", + "source_url": "http://robocode-archive.strangeautomata.com/robots/cs.s3.Seraphim_3.0.jar", + "license": "not stated in jar (RoboRumble archive distribution)", + "botdir": "/tmp/tr_bots/Seraphim", + "evidence": "2 rounds vs SpinBot: scans=0 bulletsFired=0 bulletHits=0 bulletMisses=0 bulletHitBullets=0 hitsTaken=14; result unknown", + "note": "battle completes but the bot is inert (0 scans, 0 shots). See missing_api.", + "missing_api": "getRobotRunnable() returns null: the Azuma framework implements robocode.robotinterfaces.IBasicRobot/ITeamRobot directly (cs.Azuma extends java.io.PrintStream) instead of extending robocode.Robot, and returns null from getRobotRunnable(); the bridge has no driver path for that. SmokeTest also throws ArithmeticException (divide by zero) for cs.Nene." + } + ] +} diff --git a/tools/robocode_shim/run_bridge_battle.sh b/tools/robocode_shim/run_bridge_battle.sh index 9e00753..5da328f 100755 --- a/tools/robocode_shim/run_bridge_battle.sh +++ b/tools/robocode_shim/run_bridge_battle.sh @@ -1,9 +1,22 @@ #!/usr/bin/env bash -# Run a real Tank Royale battle with the DrussGT bridge against an adversary and -# dump the movement fixture + statistics. +# Run a real Tank Royale battle with a classic-Robocode-shim bot (the "subject") +# against an adversary and dump the movement fixture + statistics. # # tools/robocode_shim/run_bridge_battle.sh [rounds] [outJsonl] # +# The subject is the DrussGT bridge bot dir by default (legacy behaviour). Point +# it at any other generated legacy bot directory with SHIM_BOTDIR: +# +# SHIM_BOTDIR=/tmp/tr_bots/Diamond \ +# tools/robocode_shim/run_bridge_battle.sh 2 /tmp/diamond.jsonl +# +# Environment: +# SHIM_BOTDIR subject bot dir; if set it is used as-is (not regenerated) +# DRUSSGT_BOTDIR legacy alias for the DrussGT subject dir (regenerated) +# SHIM_BOT_NAME subject name as reported in the battle (default: dir basename) +# SHIM_DATA classic data dir for the subject; unset => fresh dir per battle +# TR_EVENTS_OUT optional raw bullet-event sidecar +# # Example: # tools/robocode_shim/run_bridge_battle.sh \ # /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5 @@ -13,14 +26,22 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "$HERE/../.." && pwd)" ADVERSARY="${1:?usage: run_bridge_battle.sh [rounds] [outJsonl]}" ROUNDS="${2:-5}" -OUT_JSONL="${3:-/tmp/tr_drussgt_$(basename "$ADVERSARY" | tr 'A-Z' 'a-z').jsonl}" +OUT_JSONL="${3:-/tmp/tr_shim_$(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 +if [[ -n "${SHIM_BOTDIR:-}" ]]; then + SUBJECT_BOTDIR="$SHIM_BOTDIR" + [[ -d "$SUBJECT_BOTDIR" ]] || { echo "subject bot dir not found: $SUBJECT_BOTDIR" >&2; exit 1; } +else + # Legacy DrussGT path: regenerate the shared/overridden DrussGT bot dir. + SUBJECT_BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}" + export DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}" + export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}" + "$HERE/make_botdir.sh" "$SUBJECT_BOTDIR" >/dev/null +fi + +SUBJECT_NAME="${SHIM_BOT_NAME:-$(basename "$SUBJECT_BOTDIR")}" EVENTS_ARGS=() if [[ -n "${TR_EVENTS_OUT:-}" ]]; then @@ -29,10 +50,10 @@ fi java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \ --rounds "$ROUNDS" --out "$OUT_JSONL" \ - --bot "$BOTDIR" --bot "$ADVERSARY" \ - --subject DrussGT --adversary "$(basename "$ADVERSARY")" \ + --bot "$SUBJECT_BOTDIR" --bot "$ADVERSARY" \ + --subject "$SUBJECT_NAME" --adversary "$(basename "$ADVERSARY")" \ "${EVENTS_ARGS[@]}" echo -echo "=== movement statistics (TR) ===" +echo "=== movement statistics (TR, subject=$SUBJECT_NAME) ===" python3 "$REPO/tools/robocode_fixture_capture/analyze.py" "$OUT_JSONL" diff --git a/tools/robocode_shim/run_smoke.sh b/tools/robocode_shim/run_smoke.sh index cd7686e..97da018 100755 --- a/tools/robocode_shim/run_smoke.sh +++ b/tools/robocode_shim/run_smoke.sh @@ -1,12 +1,17 @@ #!/usr/bin/env bash -# Runtime proof for Approach (B): load the real DrussGT jar, drive it through -# ClassicPeer for 200 synthetic ticks and assert it emits movement intents. +# Runtime (triage) proof for Approach (B): load the real bot jar, drive it +# through ClassicPeer for 200 synthetic ticks and assert it emits movement +# intents. No Tank Royale server / physics is involved. +# +# ./run_smoke.sh # DrussGT (default, unchanged) +# SHIM_BOT_JAR=/tmp/bots/Diamond.jar SHIM_BOT_CLASS=voidious.Diamond ./run_smoke.sh set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}" -DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}" +JAR="${SHIM_BOT_JAR:-${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}}" +CLASS="${SHIM_BOT_CLASS:-jk.mega.DrussGT}" OUT="${OUT:-$HERE/out}" WORK="$(mktemp -d /tmp/shim_smoke.XXXXXX)" -java -cp "$OUT:$ROBOCODE_JAR" robocode_shim.SmokeTest "$DRUSSGT_JAR" "$WORK" +java -cp "$OUT:$ROBOCODE_JAR" robocode_shim.SmokeTest "$JAR" "$WORK" "$CLASS" diff --git a/tools/robocode_shim/src/robocode_shim/BotHost.java b/tools/robocode_shim/src/robocode_shim/BotHost.java index 9d0ea99..9abc182 100644 --- a/tools/robocode_shim/src/robocode_shim/BotHost.java +++ b/tools/robocode_shim/src/robocode_shim/BotHost.java @@ -2,24 +2,24 @@ package robocode_shim; import robocode.Event; import robocode.robotinterfaces.IBasicRobot; -import robocode.robotinterfaces.IAdvancedRobot; import java.io.File; import java.io.PrintStream; -import java.lang.reflect.Method; +import java.lang.reflect.Field; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; import java.util.List; /** - * APPROACH (B): load the REAL, unmodified DrussGT jar with a classloader, hand - * it our {@link ClassicPeer} via {@code _RobotBase.setPeer()}, and run its own - * {@code run()} loop on a worker thread. No robocode.* class is re-implemented. + * APPROACH (B): load a REAL, unmodified classic Robocode robot jar with a + * classloader, hand it our {@link ClassicPeer} via {@code _RobotBase.setPeer()}, + * and run its own {@code run()} loop on a worker thread. No robocode.* class is + * re-implemented. * *
- *   URLClassLoader cl = new URLClassLoader(new URL[]{ drussgtJar }, robocodeParent );
- *   Class<?> c = cl.loadClass("jk.mega.DrussGT");
+ *   URLClassLoader cl = new URLClassLoader(new URL[]{ botJar }, robocodeParent );
+ *   Class<?> c = cl.loadClass(mainClass);
  *   IBasicRobot bot = (IBasicRobot) c.getDeclaredConstructor().newInstance();
  *   bot.setPeer(peer);            // the seam
  *   peer.attachRobot(bot);
@@ -27,24 +27,41 @@ import java.util.List;
  * 
* * The classloader's parent must be the classloader that loaded robocode.jar, so - * that DrussGT's {@code IBasicRobot} is the same type we call setPeer on. + * that the bot's {@code IBasicRobot} is the same type we call setPeer on. + * + *

The main class and the jar list are configurable ({@code SHIM_BOT_CLASS} / + * {@code SHIM_BOT_JAR} / {@code SHIM_EXTRA_CP}); the no-arg / default remains + * the original {@code jk.mega.DrussGT} from {@code /tmp/drussgt/DrussGT.jar}. */ public class BotHost { + /** Default hosted class — the original hardcoded DrussGT (backwards compatible). */ + public static final String DEFAULT_MAIN_CLASS = LegacyBotBridge.DEFAULT_BOT_CLASS; + public final ClassicPeer peer; public volatile IBasicRobot robot; public final URLClassLoader loader; + public final String mainClass; private final File dataDir; private final Thread thread; private volatile Throwable botError; - public BotHost(File drussgtJar, File dataDir) throws Exception { + /** DrussGT default: {@code jk.mega.DrussGT}. */ + public BotHost(File botJar, File dataDir) throws Exception { + this(DEFAULT_MAIN_CLASS, botJar, Collections.emptyList(), dataDir); + } + + public BotHost(String mainClass, File botJar, List extraJars, File dataDir) throws Exception { + this.mainClass = mainClass; this.dataDir = dataDir; this.peer = new ClassicPeer(dataDir); - this.loader = new URLClassLoader( - new URL[]{drussgtJar.toURI().toURL()}, - BotHost.class.getClassLoader()); - Class cls = loader.loadClass("jk.mega.DrussGT"); + URL[] urls = new URL[1 + extraJars.size()]; + urls[0] = botJar.toURI().toURL(); + for (int i = 0; i < extraJars.size(); i++) { + urls[i + 1] = extraJars.get(i).toURI().toURL(); + } + this.loader = new URLClassLoader(urls, BotHost.class.getClassLoader()); + Class cls = loader.loadClass(mainClass); this.robot = (IBasicRobot) cls.getDeclaredConstructor().newInstance(); robot.setPeer(peer); robot.setOut(new PrintStream(new File(dataDir, "bot.out"))); @@ -54,37 +71,50 @@ public class BotHost { robot.getRobotRunnable().run(); } catch (Throwable t) { botError = t; + if (!t.getClass().getName().contains("ThreadInterrupted")) { + t.printStackTrace(); + } } finally { peer.markDead(); } - }, "DrussGT-run"); + }, mainClass + "-run"); 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. + * fields survive). 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"); + Class cls = loader.loadClass(mainClass); 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"); - f.setAccessible(true); - f.setBoolean(null, false); - } - - /** Optional: call a named no-arg method via the IBasicEvents interface. */ - public void invokeBasicEvent(String method, Class argType, Event event) throws Exception { - Method m = robot.getClass().getMethod(method, argType); - m.invoke(robot, event); + /** + * Force a static boolean field of the hosted class to {@code false}. Used + * for DrussGT's {@code shieldEnabled} (its EnergyDome shield is a separate, + * timing-sensitive subsystem; the proven path is the pure wave surfer). + * Silent no-op when the class has no such field, so it is harmless for other + * bots. + * + * @return true if the field existed and was set. + */ + public boolean disableStaticBoolean(String fieldName) { + try { + Field f = loader.loadClass(mainClass).getDeclaredField(fieldName); + if (f.getType() != boolean.class && f.getType() != Boolean.class) { + return false; + } + f.setAccessible(true); + f.setBoolean(null, false); + return true; + } catch (Throwable t) { + return false; + } } public void start() { diff --git a/tools/robocode_shim/src/robocode_shim/ClassicPeer.java b/tools/robocode_shim/src/robocode_shim/ClassicPeer.java index caa1570..6947a2d 100644 --- a/tools/robocode_shim/src/robocode_shim/ClassicPeer.java +++ b/tools/robocode_shim/src/robocode_shim/ClassicPeer.java @@ -3,13 +3,17 @@ package robocode_shim; import robocode.Bullet; import robocode.Condition; import robocode.Event; +import robocode.MessageEvent; +import robocode.StatusEvent; import robocode.robotinterfaces.IBasicEvents; import robocode.robotinterfaces.IBasicRobot; -import robocode.robotinterfaces.peer.IAdvancedRobotPeer; +import robocode.robotinterfaces.peer.ITeamRobotPeer; import java.awt.Color; import java.awt.Graphics2D; import java.io.File; +import java.io.IOException; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,7 +40,7 @@ import java.util.List; * tracked. * */ -public class ClassicPeer implements IAdvancedRobotPeer { +public class ClassicPeer implements ITeamRobotPeer { // ---- optional live world model (battle mode) ---- public volatile WorldModel world; @@ -102,6 +106,7 @@ public class ClassicPeer implements IAdvancedRobotPeer { private volatile boolean turnReady = false; private volatile boolean shutdown = false; private volatile boolean dead = false; + private volatile boolean dispatching = false; private int executedTurns = 0; private final List pendingEvents = new ArrayList<>(); private final List allEvents = new ArrayList<>(); @@ -144,10 +149,17 @@ public class ClassicPeer implements IAdvancedRobotPeer { @Override public double getRadarTurnRemaining() { WorldModel w = world; return w != null ? w.radarTurnRemaining() : radarTurnRemaining; } // ================= movement / action intents ================= - @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); } + // NB: move/turnBody/turnGun/turnRadar are the *immediate* (non-advanced) + // variants used by robocode.Robot.ahead()/turnRight()/... and by + // AdvancedRobot.turnRightRadians()/turnRadarRightRadians()/...: the genuine + // engine performs the turn AND ends the turn in one call. A bot whose run() + // is `while (true) turnRadarRightRadians(1);` (e.g. Komarious) therefore + // advances one turn per call; setting the intent without executing made such + // bots spin forever. + @Override public void move(double distance) { setMove(distance); execute(); } + @Override public void turnBody(double radians) { setTurnBody(radians); execute(); } + @Override public void turnGun(double radians) { setTurnGun(radians); execute(); } + @Override public void turnRadar(double radians) { setTurnRadar(radians); execute(); } @Override public void setMove(double distance) { lastMove = distance; moveSet = true; @@ -196,6 +208,14 @@ public class ClassicPeer implements IAdvancedRobotPeer { h.onExecute(); return; } + // Re-entrant execute() from inside an event handler (classic blocking + // robots call Robot.fire()/ahead() inside onScannedRobot): the genuine + // engine ends the turn, but in this single-threaded standalone model the + // driver is already mid-dispatch, so a nested wait would deadlock. The + // intent has been recorded and will be applied on the next turn. + if (dispatching) { + return; + } synchronized (lock) { while (!turnReady && !shutdown) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } @@ -207,7 +227,12 @@ public class ClassicPeer implements IAdvancedRobotPeer { snapshot = new ArrayList<>(pendingEvents); pendingEvents.clear(); } - dispatch(snapshot); + dispatching = true; + try { + dispatch(snapshot); + } finally { + dispatching = false; + } synchronized (lock) { executedTurns++; lock.notifyAll(); @@ -240,9 +265,42 @@ public class ClassicPeer implements IAdvancedRobotPeer { /** Battle mode: forward one already-built classic event to the robot's listeners. */ public void deliver(Event e) { + synchronized (lock) { allEvents.add(e); } dispatch(Collections.singletonList(e)); } + /** + * Synthesise a classic {@link StatusEvent} snapshot of the current world + * state. Classic Robocode delivers one every turn; bots that inspect + * {@code getStatusEvents()}/{@code getAllEvents()} or override + * {@code onStatus()} rely on it (e.g. RougeDC's version check busy-loops + * forever without it). {@code RobotStatus} has a private constructor, so it + * is built reflectively. + */ + public StatusEvent makeStatusEvent() { + try { + if (STATUS_CTOR == null) { + Class rs = Class.forName("robocode.RobotStatus"); + STATUS_CTOR = rs.getDeclaredConstructor( + double.class, double.class, double.class, double.class, + double.class, double.class, double.class, double.class, + double.class, double.class, double.class, double.class, + int.class, int.class, int.class, int.class, long.class); + STATUS_CTOR.setAccessible(true); + } + Object status = STATUS_CTOR.newInstance( + getEnergy(), getX(), getY(), getBodyHeading(), getGunHeading(), getRadarHeading(), + getVelocity(), getBodyTurnRemaining(), getRadarTurnRemaining(), getGunTurnRemaining(), + getDistanceRemaining(), getGunHeat(), + getOthers(), getNumSentries(), getRoundNum(), getNumRounds(), getTime()); + return new StatusEvent((robocode.RobotStatus) status); + } catch (Throwable t) { + return null; + } + } + + private static volatile java.lang.reflect.Constructor STATUS_CTOR; + // ================= tick bridge control (driver side) ================= /** Standalone driver: publish this turn's events, then unblock the bot's execute(). */ public void provideTurn(List events) { @@ -288,6 +346,17 @@ public class ClassicPeer implements IAdvancedRobotPeer { @Override public void setRadarColor(Color c) { radarColor = c; } @Override public void setBulletColor(Color c) { bulletColor = c; } @Override public void setScanColor(Color c) { scanColor = c; } + + // ================= team peer surface (1v1: no teammates, no messages) ================= + // Robocode's TeamRobot setPeer() casts the peer to ITeamRobotPeer, so a + // whole class of classic bots (many of which merely used TeamRobot as a + // base class) could not even start without these. + @Override public String[] getTeammates() { return new String[0]; } + @Override public boolean isTeammate(String name) { return false; } + @Override public void broadcastMessage(Serializable message) throws IOException { } + @Override public void sendMessage(String name, Serializable message) throws IOException { } + @Override public List getMessageEvents() { return Collections.emptyList(); } + @Override public void getCall() { } @Override public void setCall() { } @Override public Graphics2D getGraphics() { return null; } diff --git a/tools/robocode_shim/src/robocode_shim/DrussGTBridge.java b/tools/robocode_shim/src/robocode_shim/DrussGTBridge.java index c006b7e..1783427 100644 --- a/tools/robocode_shim/src/robocode_shim/DrussGTBridge.java +++ b/tools/robocode_shim/src/robocode_shim/DrussGTBridge.java @@ -1,456 +1,18 @@ 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}. + * Backwards-compatible alias for the original DrussGT-specific bridge. * - *

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

- *   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()
- * 
- * - *

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. + *

The bridge is now generalized in {@link LegacyBotBridge}: the hosted bot's + * main class/jar/data directory are configurable and DrussGT is only the + * default. This class remains so that already-generated bot directories + * (e.g. {@code /tmp/tr_bots/DrussGT/DrussGT.sh}) that launch + * {@code robocode_shim.DrussGTBridge} keep working unchanged. */ -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 unmatchedFired = new ArrayList<>(); - private final Map 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 final class DrussGTBridge extends LegacyBotBridge { 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; - } } diff --git a/tools/robocode_shim/src/robocode_shim/LegacyBotBridge.java b/tools/robocode_shim/src/robocode_shim/LegacyBotBridge.java new file mode 100644 index 0000000..e1568f8 --- /dev/null +++ b/tools/robocode_shim/src/robocode_shim/LegacyBotBridge.java @@ -0,0 +1,556 @@ +package robocode_shim; + +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 an arbitrary, unmodified classic + * Robocode robot jar and drives it through {@link ClassicPeer} / + * {@link BotHost}. + * + *

This is the generalized form of the original hardcoded DrussGT bridge; the + * default configuration (no environment variables) still loads + * {@code jk.mega.DrussGT} from {@code /tmp/drussgt/DrussGT.jar}, so every + * pre-existing script, fixture and bot directory behaves exactly as before. + * + *

Configuration (all optional; the DrussGT values are the defaults): + *

+ *   SHIM_BOT_CLASS   fully-qualified main class of the classic robot   (jk.mega.DrussGT)
+ *   SHIM_BOT_JAR     jar (or classes dir) that contains it             ($DRUSSGT_JAR)
+ *   SHIM_EXTRA_CP    extra jars/dirs for the bot's child classloader   (empty)
+ *   SHIM_DATA        the bot's classic data directory                  ($DRUSSGT_DATA)
+ *   SHIM_BOT_NAME    classic name reported to the bot (defaults to BOT_NAME)
+ *   SHIM_DISABLE_FIELDS  comma-separated static boolean fields to force false
+ *   DRUSSGT_SHIELD   legacy: "1" keeps DrussGT's EnergyDome shield on
+ * 
+ * + *

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: + * + *

+ *   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()
+ * 
+ * + *

The hosted bot'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 class LegacyBotBridge extends Bot + implements ClassicPeer.WorldModel, ClassicPeer.TickHost { + + /** Default main class: the original hardcoded bot (backwards compatibility). */ + public static final String DEFAULT_BOT_CLASS = "jk.mega.DrussGT"; + public static final String DEFAULT_BOT_JAR = "/tmp/drussgt/DrussGT.jar"; + public static final String DEFAULT_DATA_DIR = "/tmp/drussgt_bridge_data"; + + // ---- hosted-bot host ---- + private BotHost host; + private ClassicPeer peer; + private IBasicRobot robot; + private boolean firstRound = true; + + // ---- configuration (resolved once) ---- + private final String mainClass; + private final String botName; + + // ---- 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 unmatchedFired = new ArrayList<>(); + private final Map firedById = new HashMap<>(); + private int nextTempBulletId = -1; + private static final boolean DEBUG = envIsSet("SHIM_BRIDGE_DEBUG", "DRUSSGT_BRIDGE_DEBUG"); + private static PrintWriter debugWriter; + + private static synchronized void debugLog(String line) { + if (debugWriter == null) { + String path = firstEnv("SHIM_DEBUG_LOG", "DRUSSGT_DEBUG_LOG", "/tmp/shim_bridge_debug.log"); + try { + debugWriter = new PrintWriter(new java.io.BufferedWriter( + new java.io.FileWriter(path, true))); + } catch (Exception e) { + return; + } + } + debugWriter.println(line); + debugWriter.flush(); + } + + public static void main(String[] args) { + ThreadManagerFix.install(); + new LegacyBotBridge().start(); + } + + public LegacyBotBridge() { + super(); + mainClass = firstEnv("SHIM_BOT_CLASS", "DRUSSGT_CLASS", DEFAULT_BOT_CLASS); + botName = firstEnv("SHIM_BOT_NAME", "BOT_NAME", simpleName(mainClass)); + } + + private static String simpleName(String fqcn) { + int dot = fqcn.lastIndexOf('.'); + return dot < 0 ? fqcn : fqcn.substring(dot + 1); + } + + // ====================================================================== + // 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); + + // The hosted bot'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(firstEnv("SHIM_BOT_JAR", "DRUSSGT_JAR", DEFAULT_BOT_JAR)); + File dataDir = new File(firstEnv("SHIM_DATA", "DRUSSGT_DATA", DEFAULT_DATA_DIR)); + List extra = extraClasspath(); + host = new BotHost(mainClass, jar, extra, 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 is to run the pure wave surfer (DrussMoveGT). + // DRUSSGT_SHIELD=1 keeps the shield on. The same mechanism can disable + // any static boolean flag of the hosted bot via SHIM_DISABLE_FIELDS. + if (!"1".equals(System.getenv("DRUSSGT_SHIELD"))) { + host.disableStaticBoolean("shieldEnabled"); + } + String extra_fields = System.getenv("SHIM_DISABLE_FIELDS"); + if (extra_fields != null) { + for (String f : extra_fields.split(",")) { + if (!f.isBlank()) { + host.disableStaticBoolean(f.trim()); + } + } + } + } + + /** Extra entries (jars or directories) for the hosted bot's child classloader. */ + private static List extraClasspath() { + List out = new ArrayList<>(); + String cp = firstEnvOrNull("SHIM_EXTRA_CP", "DRUSSGT_EXTRA_CP"); + if (cp != null) { + for (String part : cp.split(File.pathSeparator)) { + if (!part.isBlank()) { + out.add(new File(part)); + } + } + } + return out; + } + + private void resetRoundState() { + unmatchedFired.clear(); + firedById.clear(); + nextTempBulletId = -1; + if (peer != null) { + peer.stopSet = false; + } + } + + // ====================================================================== + // env helpers + // ====================================================================== + + /** First non-blank value among {@code keys}, else {@code fallback}. */ + private static String firstEnv(String k1, String k2, String fallback) { + String v = firstEnvOrNull(k1, k2); + return v == null ? fallback : v; + } + + private static String firstEnvOrNull(String k1, String k2) { + String v = System.getenv(k1); + if (v != null && !v.isBlank()) { + return v; + } + v = System.getenv(k2); + return (v != null && !v.isBlank()) ? v : null; + } + + private static boolean envIsSet(String k1, String k2) { + return firstEnvOrNull(k1, k2) != null; + } + + // ====================================================================== + // ClassicPeer.TickHost — driven from the hosted bot's execute() + // ====================================================================== + + @Override + public void onExecute() { + // Re-entrant execute() from inside an event handler (classic blocking + // robots call Robot.fire()/ahead() inside onScannedRobot): the intents + // have already been recorded and the current TR tick is still in flight, + // so a nested go() would be re-entrant. Return; the outer go() ends the + // turn. + if (inTick) { + return; + } + inTick = true; + try { + applyIntents(); + go(); + deliverStatusEvent(); + } finally { + inTick = false; + } + } + + private volatile boolean inTick = false; + + /** Classic Robocode delivers a StatusEvent every turn; some bots require it. */ + private void deliverStatusEvent() { + robocode.StatusEvent status = peer.makeStatusEvent(); + if (status != null) { + status.setTime(getTurnNumber()); + peer.deliver(status); + } + } + + 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 the hosted bot 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; + } +} diff --git a/tools/robocode_shim/src/robocode_shim/SmokeTest.java b/tools/robocode_shim/src/robocode_shim/SmokeTest.java index caafc29..57346ba 100644 --- a/tools/robocode_shim/src/robocode_shim/SmokeTest.java +++ b/tools/robocode_shim/src/robocode_shim/SmokeTest.java @@ -6,27 +6,36 @@ import java.io.File; import java.util.Collections; /** - * Runtime proof for APPROACH (B): instantiate the real DrussGT from its jar, - * setPeer(), run its own run() loop, feed it synthetic ScannedRobotEvents and - * observe that it emits movement/fire intents through ClassicPeer. + * Runtime proof for APPROACH (B): instantiate a real classic Robocode robot + * from its jar, setPeer(), run its own run() loop, feed it synthetic + * ScannedRobotEvents and observe that it emits movement/fire intents through + * ClassicPeer. * - * This is an architecture spike: there is no physics here and the peer's world - * state does not integrate the intents. If the bot starts, survives its - * constructor (DrussMoveGT/MeleeRadar/DrussGunDC/EnergyDomeWorker) and produces - * non-zero setAhead/setTurnRight intents, the seam is proven operational. + *

This is a triage tool (no physics, no Tank Royale server): it + * answers "does this bot's class load, construct, run its event loop and emit + * intents through the peer?" far faster than a battle. A real battle is still + * the authoritative validation (see {@code run_bridge_battle.sh}). + * + *

+ *   java -cp out:robocode.jar robocode_shim.SmokeTest <jar> <workdir> [mainClass]
+ * 
+ * + * With no {@code mainClass} the original DrussGT path is used unchanged. */ 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 jar = new File(args.length > 0 ? args[0] : LegacyBotBridge.DEFAULT_BOT_JAR); File work = new File(args.length > 1 ? args[1] : "/tmp/shim_smoke"); + String mainClass = args.length > 2 ? args[2] : BotHost.DEFAULT_MAIN_CLASS; if (!work.exists()) work.mkdirs(); - System.out.println("DrussGT jar: " + jar + " (" + jar.length() + " bytes)"); + System.out.println("bot jar : " + jar + " (" + jar.length() + " bytes)"); + System.out.println("main class: " + mainClass); - BotHost host = new BotHost(jar, work); - host.disableShield(); // run the pure surfer path (skip EnergyDome shield) + BotHost host = new BotHost(mainClass, jar, java.util.Collections.emptyList(), work); + host.disableStaticBoolean("shieldEnabled"); // DrussGT: run the pure surfer path host.start(); int ticks = 200; @@ -47,8 +56,8 @@ public class SmokeTest { p.gunHeat = Math.max(0, 3.0 - t * 0.1); p.distanceRemaining = 0; - // A moving, orbiting enemy: enough feature variation that the - // KD-tree in EnemyMoves does not see degenerate duplicate points. + // A moving, orbiting enemy: enough feature variation that a + // KD-tree does not see degenerate duplicate points. double theta = Math.toRadians(t * 7.0); double bearing = theta; // bot heading is 0 double distance = 300.0 + 80.0 * Math.sin(theta / 2.3); @@ -56,7 +65,15 @@ public class SmokeTest { double enemyEnergy = 100.0 - 0.1 * (t % 10); ScannedRobotEvent scan = new ScannedRobotEvent( "Enemy", enemyEnergy, bearing, distance, enemyHeading, 8.0); - if (!host.tick(Collections.singletonList(scan))) { botSurvived = false; break; } + // Classic delivers a StatusEvent every turn (priority 0 => first). + robocode.StatusEvent status = p.makeStatusEvent(); + java.util.List turn = new java.util.ArrayList<>(); + if (status != null) { + status.setTime(t); + turn.add(status); + } + turn.add(scan); + if (!host.tick(turn)) { botSurvived = false; break; } if (p.lastMove != 0 || p.lastTurnBody != 0) { if (movementIntentTicks == 0) { firstMove = p.lastMove; firstTurn = p.lastTurnBody; } @@ -84,7 +101,7 @@ public class SmokeTest { host.stop(); boolean ok = movementIntentTicks > 0 && botSurvived; - System.out.println(ok ? "SMOKE TEST: OK (real DrussGT drove through the peer seam)" + System.out.println(ok ? "SMOKE TEST: OK (" + mainClass + " drove through the peer seam)" : "SMOKE TEST: no movement intents observed"); System.exit(ok ? 0 : 1); }