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.
This commit is contained in:
2026-09-26 00:25:33 +02:00
parent 0f5cfe37b2
commit ef16dd982d
11 changed files with 1553 additions and 532 deletions
+196
View File
@@ -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/<Name>`
(or its `<Name>.json`). The GUI reads the `.json` identity file.
3. Repeat for each `/tmp/tr_bots/<Name>` directory you want in the roster.
4. Start a classic 1v1 (or melee) battle and add the bots as participants.
The generated `<Name>.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/<Name> 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/<jar> SHIM_BOT_CLASS=<fqcn> 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 `<Name>.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.
+1
View File
@@ -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/TankRoyaleBridge.java" \
"$HERE/src/robocode_shim/ThreadManagerFix.java" \ "$HERE/src/robocode_shim/ThreadManagerFix.java" \
"$HERE/src/robocode_shim/ThreadManagerFixTest.java" \ "$HERE/src/robocode_shim/ThreadManagerFixTest.java" \
"$HERE/src/robocode_shim/LegacyBotBridge.java" \
"$HERE/src/robocode_shim/DrussGTBridge.java" \ "$HERE/src/robocode_shim/DrussGTBridge.java" \
"$HERE/src/robocode_shim/SmokeTest.java" "$HERE/src/robocode_shim/SmokeTest.java"
echo "shim built -> $OUT" echo "shim built -> $OUT"
+113 -23
View File
@@ -1,30 +1,107 @@
#!/usr/bin/env bash #!/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/<NAME>.
# With no arguments at all it reproduces the original DrussGT directory.
# #
# The generated directory contains only text files (<dir>.json + <dir>.sh); the # The generated directory contains only text files (<dir>.json + <dir>.sh); the
# third-party jars stay outside the repo and are referenced by absolute path. # 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 set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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}" 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}" 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}" 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" mkdir -p "$TARGET"
cat > "$TARGET/$NAME.json" <<JSON cat > "$TARGET/$DIR_NAME.json" <<JSON
{ {
"name": "DrussGT", "name": "$NAME",
"version": "3.1.4159", "version": "$VERSION",
"authors": ["Jacob"], "authors": ["$AUTHORS"],
"description": "Real classic DrussGT running on Tank Royale via the robocode_shim bridge", "description": "$DESC",
"homepage": "", "homepage": "",
"countryCodes": ["se"], "countryCodes": ["$COUNTRY"],
"gameTypes": ["classic", "1v1"], "gameTypes": ["$GAMETYPES_JSON"],
"platform": "JVM", "platform": "JVM",
"programmingLang": "Java 8" "programmingLang": "Java 8"
} }
@@ -35,26 +112,39 @@ JSON
# .json above), but running this script BY HAND does not - and the API then # .json above), but running this script BY HAND does not - and the API then
# rejects the handshake with "Required bot property 'name' is missing". # rejects the handshake with "Required bot property 'name' is missing".
# Exporting them here makes the script work standalone as well. # Exporting them here makes the script work standalone as well.
cat > "$TARGET/$NAME.sh" <<SH #
# SHIM_BOT_CLASS / SHIM_BOT_JAR select the hosted classic robot. SHIM_DATA is a
# fresh per-process directory by default: two bots (or two concurrent battles)
# can never share or clobber the same classic data directory.
cat > "$TARGET/$DIR_NAME.sh" <<SH
#!/bin/sh #!/bin/sh
cd "\$(dirname "\$0")" cd "\$(dirname "\$0")"
# Bot identity - keep in sync with $NAME.json # Hosted classic robot selection - keep in sync with $DIR_NAME.json
export BOT_NAME="DrussGT" export SHIM_BOT_CLASS="$CLASS"
export BOT_VERSION="3.1.4159" export SHIM_BOT_JAR="$JAR"
export BOT_AUTHORS="Jacob" $EXTRA_LINE
export BOT_DESCRIPTION="Real classic DrussGT running on Tank Royale via the robocode_shim bridge" : "\${SHIM_DATA:=\${DRUSSGT_DATA:-\$(mktemp -d "\${TMPDIR:-/tmp}/tr_shim_data.XXXXXX")}}"
export SHIM_DATA
# Bot identity - keep in sync with $DIR_NAME.json
export BOT_NAME="$NAME"
export BOT_VERSION="$VERSION"
export BOT_AUTHORS="$AUTHORS"
export BOT_DESCRIPTION="$DESC"
export BOT_HOMEPAGE="" export BOT_HOMEPAGE=""
export BOT_COUNTRY_CODES="se" export BOT_COUNTRY_CODES="$COUNTRY"
export BOT_GAME_TYPES="classic,1v1" export BOT_GAME_TYPES="$GAMETYPES"
export BOT_PLATFORM="JVM" export BOT_PLATFORM="JVM"
export BOT_PROG_LANG="Java 8" export BOT_PROG_LANG="Java 8"
exec java -cp "$OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR" robocode_shim.DrussGTBridge exec java -cp "$OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR" robocode_shim.LegacyBotBridge
SH SH
chmod +x "$TARGET/$NAME.sh" chmod +x "$TARGET/$DIR_NAME.sh"
echo "bot dir ready: $TARGET" echo "bot dir ready: $TARGET"
echo " $TARGET/$NAME.json" echo " $TARGET/$DIR_NAME.json"
echo " $TARGET/$NAME.sh" echo " $TARGET/$DIR_NAME.sh"
echo " class: $CLASS"
echo " jar: $JAR"
echo " (classpath: $OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR)" echo " (classpath: $OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR)"
+474
View File
@@ -0,0 +1,474 @@
{
"manifest_version": 1,
"description": "Legacy classic-Robocode bots validated as Tank Royale sparring partners through the generalized robocode_shim bridge. No third-party jar is committed; jars live in /tmp and are referenced by absolute path. 'style' is INFERRED from the bot name/docs; 'evidence' is MEASURED (2-round Tank Royale battles vs the sample SpinBot, embedded server).",
"source_archive": "http://robocode-archive.strangeautomata.com/robots/",
"roboarchive_note": "Directory listing is browsable; every jar was fetched with HTTP 200.",
"robocode_api_jar": "/tmp/robocode/install/libs/robocode.jar",
"how_to_add": "tools/robocode_shim/make_botdir.sh <Name> --class <fqcn> --jar <path>",
"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."
}
]
}
+31 -10
View File
@@ -1,9 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Run a real Tank Royale battle with the DrussGT bridge against an adversary and # Run a real Tank Royale battle with a classic-Robocode-shim bot (the "subject")
# dump the movement fixture + statistics. # against an adversary and dump the movement fixture + statistics.
# #
# tools/robocode_shim/run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl] # tools/robocode_shim/run_bridge_battle.sh <adversaryBotDir> [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 <adversaryBotDir> 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: # Example:
# tools/robocode_shim/run_bridge_battle.sh \ # tools/robocode_shim/run_bridge_battle.sh \
# /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5 # /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)" REPO="$(cd "$HERE/../.." && pwd)"
ADVERSARY="${1:?usage: run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl]}" ADVERSARY="${1:?usage: run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl]}"
ROUNDS="${2:-5}" 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}" 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}" if [[ -n "${SHIM_BOTDIR:-}" ]]; then
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null 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=() EVENTS_ARGS=()
if [[ -n "${TR_EVENTS_OUT:-}" ]]; then if [[ -n "${TR_EVENTS_OUT:-}" ]]; then
@@ -29,10 +50,10 @@ fi
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \ java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
--rounds "$ROUNDS" --out "$OUT_JSONL" \ --rounds "$ROUNDS" --out "$OUT_JSONL" \
--bot "$BOTDIR" --bot "$ADVERSARY" \ --bot "$SUBJECT_BOTDIR" --bot "$ADVERSARY" \
--subject DrussGT --adversary "$(basename "$ADVERSARY")" \ --subject "$SUBJECT_NAME" --adversary "$(basename "$ADVERSARY")" \
"${EVENTS_ARGS[@]}" "${EVENTS_ARGS[@]}"
echo echo
echo "=== movement statistics (TR) ===" echo "=== movement statistics (TR, subject=$SUBJECT_NAME) ==="
python3 "$REPO/tools/robocode_fixture_capture/analyze.py" "$OUT_JSONL" python3 "$REPO/tools/robocode_fixture_capture/analyze.py" "$OUT_JSONL"
+9 -4
View File
@@ -1,12 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime proof for Approach (B): load the real DrussGT jar, drive it through # Runtime (triage) proof for Approach (B): load the real bot jar, drive it
# ClassicPeer for 200 synthetic ticks and assert it emits movement intents. # 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 set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}" 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}" OUT="${OUT:-$HERE/out}"
WORK="$(mktemp -d /tmp/shim_smoke.XXXXXX)" 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"
@@ -2,24 +2,24 @@ package robocode_shim;
import robocode.Event; import robocode.Event;
import robocode.robotinterfaces.IBasicRobot; import robocode.robotinterfaces.IBasicRobot;
import robocode.robotinterfaces.IAdvancedRobot;
import java.io.File; import java.io.File;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.reflect.Method; import java.lang.reflect.Field;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* APPROACH (B): load the REAL, unmodified DrussGT jar with a classloader, hand * APPROACH (B): load a REAL, unmodified classic Robocode robot jar with a
* it our {@link ClassicPeer} via {@code _RobotBase.setPeer()}, and run its own * classloader, hand it our {@link ClassicPeer} via {@code _RobotBase.setPeer()},
* {@code run()} loop on a worker thread. No robocode.* class is re-implemented. * and run its own {@code run()} loop on a worker thread. No robocode.* class is
* re-implemented.
* *
* <pre> * <pre>
* URLClassLoader cl = new URLClassLoader(new URL[]{ drussgtJar }, robocodeParent ); * URLClassLoader cl = new URLClassLoader(new URL[]{ botJar }, robocodeParent );
* Class&lt;?&gt; c = cl.loadClass("jk.mega.DrussGT"); * Class&lt;?&gt; c = cl.loadClass(mainClass);
* IBasicRobot bot = (IBasicRobot) c.getDeclaredConstructor().newInstance(); * IBasicRobot bot = (IBasicRobot) c.getDeclaredConstructor().newInstance();
* bot.setPeer(peer); // the seam * bot.setPeer(peer); // the seam
* peer.attachRobot(bot); * peer.attachRobot(bot);
@@ -27,24 +27,41 @@ import java.util.List;
* </pre> * </pre>
* *
* The classloader's parent must be the classloader that loaded robocode.jar, so * 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.
*
* <p>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 { 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 final ClassicPeer peer;
public volatile IBasicRobot robot; public volatile IBasicRobot robot;
public final URLClassLoader loader; public final URLClassLoader loader;
public final String mainClass;
private final File dataDir; private final File dataDir;
private final Thread thread; private final Thread thread;
private volatile Throwable botError; 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<File> extraJars, File dataDir) throws Exception {
this.mainClass = mainClass;
this.dataDir = dataDir; this.dataDir = dataDir;
this.peer = new ClassicPeer(dataDir); this.peer = new ClassicPeer(dataDir);
this.loader = new URLClassLoader( URL[] urls = new URL[1 + extraJars.size()];
new URL[]{drussgtJar.toURI().toURL()}, urls[0] = botJar.toURI().toURL();
BotHost.class.getClassLoader()); for (int i = 0; i < extraJars.size(); i++) {
Class<?> cls = loader.loadClass("jk.mega.DrussGT"); 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(); this.robot = (IBasicRobot) cls.getDeclaredConstructor().newInstance();
robot.setPeer(peer); robot.setPeer(peer);
robot.setOut(new PrintStream(new File(dataDir, "bot.out"))); robot.setOut(new PrintStream(new File(dataDir, "bot.out")));
@@ -54,37 +71,50 @@ public class BotHost {
robot.getRobotRunnable().run(); robot.getRobotRunnable().run();
} catch (Throwable t) { } catch (Throwable t) {
botError = t; botError = t;
if (!t.getClass().getName().contains("ThreadInterrupted")) {
t.printStackTrace();
}
} finally { } finally {
peer.markDead(); peer.markDead();
} }
}, "DrussGT-run"); }, mainClass + "-run");
this.thread.setDaemon(true); this.thread.setDaemon(true);
} }
/** /**
* Classic Robocode gives every round a fresh robot instance (only static * Classic Robocode gives every round a fresh robot instance (only static
* fields survive). DrussGT sets up per-round state in run(), but mirroring * fields survive). Mirroring the engine's instance lifetime removes a whole
* the engine's instance lifetime removes a whole class of cross-round bugs. * class of cross-round bugs.
*/ */
public IBasicRobot newRobot() throws Exception { public IBasicRobot newRobot() throws Exception {
Class<?> cls = loader.loadClass("jk.mega.DrussGT"); Class<?> cls = loader.loadClass(mainClass);
IBasicRobot r = (IBasicRobot) cls.getDeclaredConstructor().newInstance(); IBasicRobot r = (IBasicRobot) cls.getDeclaredConstructor().newInstance();
r.setPeer(peer); r.setPeer(peer);
r.setOut(new PrintStream(new File(dataDir, "bot.out"))); r.setOut(new PrintStream(new File(dataDir, "bot.out")));
return r; return r;
} }
/** Optional: force the shield subsystem off so movement runs its pure surfer path. */ /**
public void disableShield() throws Exception { * Force a static boolean field of the hosted class to {@code false}. Used
java.lang.reflect.Field f = loader.loadClass("jk.mega.DrussGT").getDeclaredField("shieldEnabled"); * for DrussGT's {@code shieldEnabled} (its EnergyDome shield is a separate,
f.setAccessible(true); * timing-sensitive subsystem; the proven path is the pure wave surfer).
f.setBoolean(null, false); * Silent no-op when the class has no such field, so it is harmless for other
} * bots.
*
/** Optional: call a named no-arg method via the IBasicEvents interface. */ * @return true if the field existed and was set.
public void invokeBasicEvent(String method, Class<?> argType, Event event) throws Exception { */
Method m = robot.getClass().getMethod(method, argType); public boolean disableStaticBoolean(String fieldName) {
m.invoke(robot, event); 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() { public void start() {
@@ -3,13 +3,17 @@ package robocode_shim;
import robocode.Bullet; import robocode.Bullet;
import robocode.Condition; import robocode.Condition;
import robocode.Event; import robocode.Event;
import robocode.MessageEvent;
import robocode.StatusEvent;
import robocode.robotinterfaces.IBasicEvents; import robocode.robotinterfaces.IBasicEvents;
import robocode.robotinterfaces.IBasicRobot; import robocode.robotinterfaces.IBasicRobot;
import robocode.robotinterfaces.peer.IAdvancedRobotPeer; import robocode.robotinterfaces.peer.ITeamRobotPeer;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -36,7 +40,7 @@ import java.util.List;
* tracked.</li> * tracked.</li>
* </ul> * </ul>
*/ */
public class ClassicPeer implements IAdvancedRobotPeer { public class ClassicPeer implements ITeamRobotPeer {
// ---- optional live world model (battle mode) ---- // ---- optional live world model (battle mode) ----
public volatile WorldModel world; public volatile WorldModel world;
@@ -102,6 +106,7 @@ public class ClassicPeer implements IAdvancedRobotPeer {
private volatile boolean turnReady = false; private volatile boolean turnReady = false;
private volatile boolean shutdown = false; private volatile boolean shutdown = false;
private volatile boolean dead = false; private volatile boolean dead = false;
private volatile boolean dispatching = false;
private int executedTurns = 0; private int executedTurns = 0;
private final List<Event> pendingEvents = new ArrayList<>(); private final List<Event> pendingEvents = new ArrayList<>();
private final List<Event> allEvents = new ArrayList<>(); private final List<Event> 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; } @Override public double getRadarTurnRemaining() { WorldModel w = world; return w != null ? w.radarTurnRemaining() : radarTurnRemaining; }
// ================= movement / action intents ================= // ================= movement / action intents =================
@Override public void move(double distance) { setMove(distance); } // NB: move/turnBody/turnGun/turnRadar are the *immediate* (non-advanced)
@Override public void turnBody(double radians) { setTurnBody(radians); } // variants used by robocode.Robot.ahead()/turnRight()/... and by
@Override public void turnGun(double radians) { setTurnGun(radians); } // AdvancedRobot.turnRightRadians()/turnRadarRightRadians()/...: the genuine
@Override public void turnRadar(double radians) { setTurnRadar(radians); } // 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) { @Override public void setMove(double distance) {
lastMove = distance; moveSet = true; lastMove = distance; moveSet = true;
@@ -196,6 +208,14 @@ public class ClassicPeer implements IAdvancedRobotPeer {
h.onExecute(); h.onExecute();
return; 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) { synchronized (lock) {
while (!turnReady && !shutdown) { while (!turnReady && !shutdown) {
try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
@@ -207,7 +227,12 @@ public class ClassicPeer implements IAdvancedRobotPeer {
snapshot = new ArrayList<>(pendingEvents); snapshot = new ArrayList<>(pendingEvents);
pendingEvents.clear(); pendingEvents.clear();
} }
dispatch(snapshot); dispatching = true;
try {
dispatch(snapshot);
} finally {
dispatching = false;
}
synchronized (lock) { synchronized (lock) {
executedTurns++; executedTurns++;
lock.notifyAll(); lock.notifyAll();
@@ -240,9 +265,42 @@ public class ClassicPeer implements IAdvancedRobotPeer {
/** Battle mode: forward one already-built classic event to the robot's listeners. */ /** Battle mode: forward one already-built classic event to the robot's listeners. */
public void deliver(Event e) { public void deliver(Event e) {
synchronized (lock) { allEvents.add(e); }
dispatch(Collections.singletonList(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) ================= // ================= tick bridge control (driver side) =================
/** Standalone driver: publish this turn's events, then unblock the bot's execute(). */ /** Standalone driver: publish this turn's events, then unblock the bot's execute(). */
public void provideTurn(List<Event> events) { public void provideTurn(List<Event> events) {
@@ -288,6 +346,17 @@ public class ClassicPeer implements IAdvancedRobotPeer {
@Override public void setRadarColor(Color c) { radarColor = c; } @Override public void setRadarColor(Color c) { radarColor = c; }
@Override public void setBulletColor(Color c) { bulletColor = c; } @Override public void setBulletColor(Color c) { bulletColor = c; }
@Override public void setScanColor(Color c) { scanColor = 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<MessageEvent> getMessageEvents() { return Collections.emptyList(); }
@Override public void getCall() { } @Override public void getCall() { }
@Override public void setCall() { } @Override public void setCall() { }
@Override public Graphics2D getGraphics() { return null; } @Override public Graphics2D getGraphics() { return null; }
@@ -1,456 +1,18 @@
package robocode_shim; 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 * Backwards-compatible alias for the original DrussGT-specific bridge.
* jar and drives it through {@link ClassicPeer} / {@link BotHost}.
* *
* <p>It is a {@link Bot} (the TR API's classic-movement subclass) so that the * <p>The bridge is now generalized in {@link LegacyBotBridge}: the hosted bot's
* peer's classic intents can be forwarded one-to-one onto the TR Bot API's own * main class/jar/data directory are configurable and DrussGT is only the
* classic motion model: * default. This class remains so that already-generated bot directories
* * (e.g. {@code /tmp/tr_bots/DrussGT/DrussGT.sh}) that launch
* <pre> * {@code robocode_shim.DrussGTBridge} keep working unchanged.
* classic (peer) -&gt; Tank Royale Bot API
* setAhead(d) setForward(d)
* setTurnRightRadians(r) setTurnRight(toDegrees(r))
* setTurnGunRightRadians setTurnGunRight(toDegrees(r))
* setTurnRadarRightRadians setTurnRadarRight(toDegrees(r))
* setFireBullet(p) setFire(p) + bullet-identity tracking
* execute() go()
* </pre>
*
* <p>DrussGT's own {@code run()} is executed on the Tank Royale bot thread
* (exactly where the official robocode-api-bridge runs a legacy robot), and its
* {@code execute()} calls {@code go()} through {@link ClassicPeer.TickHost}.
* TR events are synthesised into classic events and delivered synchronously, so
* classic event ordering (TR's event queue priorities) and the classic "events
* are delivered before the next turn's run() body" contract both hold.
*/ */
public final class DrussGTBridge extends Bot public final class DrussGTBridge extends LegacyBotBridge {
implements ClassicPeer.WorldModel, ClassicPeer.TickHost {
// ---- DrussGT host ----
private BotHost host;
private ClassicPeer peer;
private IBasicRobot robot;
private boolean firstRound = true;
// ---- bullet identity ----
private static final class BulletReflection {
static final Field X, Y, ACTIVE;
static {
Field x = null, y = null, active = null;
try {
x = robocode.Bullet.class.getDeclaredField("x");
x.setAccessible(true);
y = robocode.Bullet.class.getDeclaredField("y");
y.setAccessible(true);
active = robocode.Bullet.class.getDeclaredField("isActive");
active.setAccessible(true);
} catch (Exception e) {
// leave null; position/active updates are best-effort
}
X = x; Y = y; ACTIVE = active;
}
static void setXY(robocode.Bullet b, double x, double y) {
try { if (X != null) X.setDouble(b, x); if (Y != null) Y.setDouble(b, y); } catch (Exception ignored) { }
}
static void setActive(robocode.Bullet b, boolean active) {
try { if (ACTIVE != null) ACTIVE.setBoolean(b, active); } catch (Exception ignored) { }
}
}
private final List<robocode.Bullet> unmatchedFired = new ArrayList<>();
private final Map<Integer, robocode.Bullet> firedById = new HashMap<>();
private int nextTempBulletId = -1;
private final String botName = envOr("BOT_NAME", "DrussGT");
private static final boolean DEBUG = System.getenv("DRUSSGT_BRIDGE_DEBUG") != null;
private static PrintWriter debugWriter;
private static synchronized void debugLog(String line) {
if (debugWriter == null) {
try {
debugWriter = new PrintWriter(new java.io.BufferedWriter(
new java.io.FileWriter("/tmp/drussgt_bridge_debug.log", true)));
} catch (Exception e) {
return;
}
}
debugWriter.println(line);
debugWriter.flush();
}
public static void main(String[] args) { public static void main(String[] args) {
ThreadManagerFix.install(); ThreadManagerFix.install();
new DrussGTBridge().start(); 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;
}
} }
@@ -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 <b>arbitrary, unmodified classic
* Robocode robot jar</b> and drives it through {@link ClassicPeer} /
* {@link BotHost}.
*
* <p>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.
*
* <p>Configuration (all optional; the DrussGT values are the defaults):
* <pre>
* 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
* </pre>
*
* <p>It is a {@link Bot} (the TR API's classic-movement subclass) so that the
* peer's classic intents can be forwarded one-to-one onto the TR Bot API's own
* classic motion model:
*
* <pre>
* classic (peer) -&gt; Tank Royale Bot API
* setAhead(d) setForward(d)
* setTurnRightRadians(r) setTurnRight(toDegrees(r))
* setTurnGunRightRadians setTurnGunRight(toDegrees(r))
* setTurnRadarRightRadians setTurnRadarRight(toDegrees(r))
* setFireBullet(p) setFire(p) + bullet-identity tracking
* execute() go()
* </pre>
*
* <p>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<robocode.Bullet> unmatchedFired = new ArrayList<>();
private final Map<Integer, robocode.Bullet> 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<File> 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<File> extraClasspath() {
List<File> 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;
}
}
@@ -6,27 +6,36 @@ import java.io.File;
import java.util.Collections; import java.util.Collections;
/** /**
* Runtime proof for APPROACH (B): instantiate the real DrussGT from its jar, * Runtime proof for APPROACH (B): instantiate a real classic Robocode robot
* setPeer(), run its own run() loop, feed it synthetic ScannedRobotEvents and * from its jar, setPeer(), run its own run() loop, feed it synthetic
* observe that it emits movement/fire intents through ClassicPeer. * 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 * <p>This is a <em>triage</em> tool (no physics, no Tank Royale server): it
* state does not integrate the intents. If the bot starts, survives its * answers "does this bot's class load, construct, run its event loop and emit
* constructor (DrussMoveGT/MeleeRadar/DrussGunDC/EnergyDomeWorker) and produces * intents through the peer?" far faster than a battle. A real battle is still
* non-zero setAhead/setTurnRight intents, the seam is proven operational. * the authoritative validation (see {@code run_bridge_battle.sh}).
*
* <pre>
* java -cp out:robocode.jar robocode_shim.SmokeTest &lt;jar&gt; &lt;workdir&gt; [mainClass]
* </pre>
*
* With no {@code mainClass} the original DrussGT path is used unchanged.
*/ */
public class SmokeTest { public class SmokeTest {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
ThreadManagerFix.install(); 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"); 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(); 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); BotHost host = new BotHost(mainClass, jar, java.util.Collections.emptyList(), work);
host.disableShield(); // run the pure surfer path (skip EnergyDome shield) host.disableStaticBoolean("shieldEnabled"); // DrussGT: run the pure surfer path
host.start(); host.start();
int ticks = 200; int ticks = 200;
@@ -47,8 +56,8 @@ public class SmokeTest {
p.gunHeat = Math.max(0, 3.0 - t * 0.1); p.gunHeat = Math.max(0, 3.0 - t * 0.1);
p.distanceRemaining = 0; p.distanceRemaining = 0;
// A moving, orbiting enemy: enough feature variation that the // A moving, orbiting enemy: enough feature variation that a
// KD-tree in EnemyMoves does not see degenerate duplicate points. // KD-tree does not see degenerate duplicate points.
double theta = Math.toRadians(t * 7.0); double theta = Math.toRadians(t * 7.0);
double bearing = theta; // bot heading is 0 double bearing = theta; // bot heading is 0
double distance = 300.0 + 80.0 * Math.sin(theta / 2.3); 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); double enemyEnergy = 100.0 - 0.1 * (t % 10);
ScannedRobotEvent scan = new ScannedRobotEvent( ScannedRobotEvent scan = new ScannedRobotEvent(
"Enemy", enemyEnergy, bearing, distance, enemyHeading, 8.0); "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<robocode.Event> 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 (p.lastMove != 0 || p.lastTurnBody != 0) {
if (movementIntentTicks == 0) { firstMove = p.lastMove; firstTurn = p.lastTurnBody; } if (movementIntentTicks == 0) { firstMove = p.lastMove; firstTurn = p.lastTurnBody; }
@@ -84,7 +101,7 @@ public class SmokeTest {
host.stop(); host.stop();
boolean ok = movementIntentTicks > 0 && botSurvived; 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"); : "SMOKE TEST: no movement intents observed");
System.exit(ok ? 0 : 1); System.exit(ok ? 0 : 1);
} }