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:
@@ -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.
|
||||
@@ -32,6 +32,7 @@ javac -nowarn -cp "$ROBOCODE_JAR:$TR_BOT_API_JAR" -d "$OUT" \
|
||||
"$HERE/src/robocode_shim/TankRoyaleBridge.java" \
|
||||
"$HERE/src/robocode_shim/ThreadManagerFix.java" \
|
||||
"$HERE/src/robocode_shim/ThreadManagerFixTest.java" \
|
||||
"$HERE/src/robocode_shim/LegacyBotBridge.java" \
|
||||
"$HERE/src/robocode_shim/DrussGTBridge.java" \
|
||||
"$HERE/src/robocode_shim/SmokeTest.java"
|
||||
echo "shim built -> $OUT"
|
||||
|
||||
@@ -1,30 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate a Tank Royale bot directory that boots the DrussGT bridge.
|
||||
# Generate a Tank Royale bot directory that boots a classic Robocode robot
|
||||
# through the robocode_shim bridge.
|
||||
#
|
||||
# tools/robocode_shim/make_botdir.sh [TARGET_DIR]
|
||||
# tools/robocode_shim/make_botdir.sh [TARGET|NAME] \
|
||||
# [--class FQCN] [--jar PATH] [--name DISPLAY] \
|
||||
# [--version V] [--authors A] [--desc D] [--country CC] [--gametypes G]
|
||||
#
|
||||
# The first positional argument may be either
|
||||
# * a directory path (legacy form, e.g. /tmp/tr_bots/DrussGT), or
|
||||
# * a bare bot name, in which case the dir is /tmp/tr_bots/<NAME>.
|
||||
# With no arguments at all it reproduces the original DrussGT directory.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Defaults (so the original no-arg DrussGT invocation is byte-for-byte usable):
|
||||
# --class jk.mega.DrussGT
|
||||
# --jar $DRUSSGT_JAR (/tmp/drussgt/DrussGT.jar)
|
||||
# display name = bot-dir basename, version parsed from the jar name (or 3.1.4159)
|
||||
#
|
||||
# Data-directory isolation: the generated launcher sets SHIM_DATA to a fresh
|
||||
# per-process mktemp directory unless the caller already provided SHIM_DATA /
|
||||
# DRUSSGT_DATA. Two bots (or two concurrent battles) therefore can never share
|
||||
# or clobber one classic data directory.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET="${1:-/tmp/tr_bots/DrussGT}"
|
||||
NAME="$(basename "$TARGET")"
|
||||
|
||||
TARGET=""
|
||||
CLASS=""
|
||||
JAR=""
|
||||
DISPLAY=""
|
||||
VERSION=""
|
||||
AUTHORS=""
|
||||
DESC=""
|
||||
COUNTRY="se"
|
||||
GAMETYPES="classic,1v1"
|
||||
|
||||
if [[ $# -gt 0 && "${1}" != --* ]]; then
|
||||
TARGET="$1"; shift
|
||||
fi
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--class) CLASS="$2"; shift 2;;
|
||||
--jar) JAR="$2"; shift 2;;
|
||||
--name) DISPLAY="$2"; shift 2;;
|
||||
--version) VERSION="$2"; shift 2;;
|
||||
--authors) AUTHORS="$2"; shift 2;;
|
||||
--desc) DESC="$2"; shift 2;;
|
||||
--country) COUNTRY="$2"; shift 2;;
|
||||
--gametypes) GAMETYPES="$2"; shift 2;;
|
||||
-h|--help) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- resolve target dir + name (legacy forms preserved) ---
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="/tmp/tr_bots/DrussGT"
|
||||
elif [[ "$TARGET" != */* ]]; then
|
||||
TARGET="/tmp/tr_bots/$TARGET"
|
||||
fi
|
||||
DIR_NAME="$(basename "$TARGET")"
|
||||
|
||||
DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}"
|
||||
CLASS="${CLASS:-jk.mega.DrussGT}"
|
||||
JAR="${JAR:-$DRUSSGT_JAR}"
|
||||
NAME="${DISPLAY:-$DIR_NAME}"
|
||||
|
||||
# --- defaults that keep the DrussGT case identical to the old script ---
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then
|
||||
VERSION="3.1.4159"
|
||||
else
|
||||
stem="$(basename "$JAR")"; stem="${stem%.jar}"
|
||||
if [[ "$stem" == *_* ]]; then VERSION="${stem##*_}"; else VERSION="1.0"; fi
|
||||
fi
|
||||
fi
|
||||
[[ -n "$AUTHORS" ]] || { if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then AUTHORS="Jacob"; else AUTHORS="unknown"; fi; }
|
||||
[[ -n "$DESC" ]] || { if [[ "$CLASS" == "jk.mega.DrussGT" ]]; then
|
||||
DESC="Real classic DrussGT running on Tank Royale via the robocode_shim bridge"
|
||||
else
|
||||
DESC="Classic Robocode robot ${CLASS} running on Tank Royale via the robocode_shim bridge"
|
||||
fi; }
|
||||
GAMETYPES_JSON="$(printf '%s' "$GAMETYPES" | sed 's/,/\", \"/g')"
|
||||
|
||||
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}"
|
||||
TR_BOT_API_JAR="${TR_BOT_API_JAR:-$HOME/Downloads/sample-bots-java-1.0.2/lib/robocode-tankroyale-bot-api-1.0.2.jar}"
|
||||
OUT="${OUT:-$HERE/out}"
|
||||
EXTRA_CP="${SHIM_EXTRA_CP:-}"
|
||||
EXTRA_LINE=""
|
||||
[[ -n "$EXTRA_CP" ]] && EXTRA_LINE="export SHIM_EXTRA_CP=\"$EXTRA_CP\""
|
||||
|
||||
mkdir -p "$TARGET"
|
||||
cat > "$TARGET/$NAME.json" <<JSON
|
||||
cat > "$TARGET/$DIR_NAME.json" <<JSON
|
||||
{
|
||||
"name": "DrussGT",
|
||||
"version": "3.1.4159",
|
||||
"authors": ["Jacob"],
|
||||
"description": "Real classic DrussGT running on Tank Royale via the robocode_shim bridge",
|
||||
"name": "$NAME",
|
||||
"version": "$VERSION",
|
||||
"authors": ["$AUTHORS"],
|
||||
"description": "$DESC",
|
||||
"homepage": "",
|
||||
"countryCodes": ["se"],
|
||||
"gameTypes": ["classic", "1v1"],
|
||||
"countryCodes": ["$COUNTRY"],
|
||||
"gameTypes": ["$GAMETYPES_JSON"],
|
||||
"platform": "JVM",
|
||||
"programmingLang": "Java 8"
|
||||
}
|
||||
@@ -35,26 +112,39 @@ JSON
|
||||
# .json above), but running this script BY HAND does not - and the API then
|
||||
# rejects the handshake with "Required bot property 'name' is missing".
|
||||
# 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
|
||||
cd "\$(dirname "\$0")"
|
||||
|
||||
# Bot identity - keep in sync with $NAME.json
|
||||
export BOT_NAME="DrussGT"
|
||||
export BOT_VERSION="3.1.4159"
|
||||
export BOT_AUTHORS="Jacob"
|
||||
export BOT_DESCRIPTION="Real classic DrussGT running on Tank Royale via the robocode_shim bridge"
|
||||
# Hosted classic robot selection - keep in sync with $DIR_NAME.json
|
||||
export SHIM_BOT_CLASS="$CLASS"
|
||||
export SHIM_BOT_JAR="$JAR"
|
||||
$EXTRA_LINE
|
||||
: "\${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_COUNTRY_CODES="se"
|
||||
export BOT_GAME_TYPES="classic,1v1"
|
||||
export BOT_COUNTRY_CODES="$COUNTRY"
|
||||
export BOT_GAME_TYPES="$GAMETYPES"
|
||||
export BOT_PLATFORM="JVM"
|
||||
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
|
||||
chmod +x "$TARGET/$NAME.sh"
|
||||
chmod +x "$TARGET/$DIR_NAME.sh"
|
||||
|
||||
echo "bot dir ready: $TARGET"
|
||||
echo " $TARGET/$NAME.json"
|
||||
echo " $TARGET/$NAME.sh"
|
||||
echo " $TARGET/$DIR_NAME.json"
|
||||
echo " $TARGET/$DIR_NAME.sh"
|
||||
echo " class: $CLASS"
|
||||
echo " jar: $JAR"
|
||||
echo " (classpath: $OUT:$ROBOCODE_JAR:$TR_BOT_API_JAR)"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run a real Tank Royale battle with the DrussGT bridge against an adversary and
|
||||
# dump the movement fixture + statistics.
|
||||
# Run a real Tank Royale battle with a classic-Robocode-shim bot (the "subject")
|
||||
# against an adversary and dump the movement fixture + statistics.
|
||||
#
|
||||
# tools/robocode_shim/run_bridge_battle.sh <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:
|
||||
# tools/robocode_shim/run_bridge_battle.sh \
|
||||
# /home/davide/Projects/tank-royale/sample-bots/java/build/archive/SpinBot 5
|
||||
@@ -13,14 +26,22 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "$HERE/../.." && pwd)"
|
||||
ADVERSARY="${1:?usage: run_bridge_battle.sh <adversaryBotDir> [rounds] [outJsonl]}"
|
||||
ROUNDS="${2:-5}"
|
||||
OUT_JSONL="${3:-/tmp/tr_drussgt_$(basename "$ADVERSARY" | tr 'A-Z' 'a-z').jsonl}"
|
||||
OUT_JSONL="${3:-/tmp/tr_shim_$(basename "$ADVERSARY" | tr 'A-Z' 'a-z').jsonl}"
|
||||
|
||||
RUNNER="${TR_RUNNER_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}"
|
||||
export DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}"
|
||||
export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}"
|
||||
|
||||
BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
||||
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null
|
||||
if [[ -n "${SHIM_BOTDIR:-}" ]]; then
|
||||
SUBJECT_BOTDIR="$SHIM_BOTDIR"
|
||||
[[ -d "$SUBJECT_BOTDIR" ]] || { echo "subject bot dir not found: $SUBJECT_BOTDIR" >&2; exit 1; }
|
||||
else
|
||||
# Legacy DrussGT path: regenerate the shared/overridden DrussGT bot dir.
|
||||
SUBJECT_BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
||||
export DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}"
|
||||
export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}"
|
||||
"$HERE/make_botdir.sh" "$SUBJECT_BOTDIR" >/dev/null
|
||||
fi
|
||||
|
||||
SUBJECT_NAME="${SHIM_BOT_NAME:-$(basename "$SUBJECT_BOTDIR")}"
|
||||
|
||||
EVENTS_ARGS=()
|
||||
if [[ -n "${TR_EVENTS_OUT:-}" ]]; then
|
||||
@@ -29,10 +50,10 @@ fi
|
||||
|
||||
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
|
||||
--rounds "$ROUNDS" --out "$OUT_JSONL" \
|
||||
--bot "$BOTDIR" --bot "$ADVERSARY" \
|
||||
--subject DrussGT --adversary "$(basename "$ADVERSARY")" \
|
||||
--bot "$SUBJECT_BOTDIR" --bot "$ADVERSARY" \
|
||||
--subject "$SUBJECT_NAME" --adversary "$(basename "$ADVERSARY")" \
|
||||
"${EVENTS_ARGS[@]}"
|
||||
|
||||
echo
|
||||
echo "=== movement statistics (TR) ==="
|
||||
echo "=== movement statistics (TR, subject=$SUBJECT_NAME) ==="
|
||||
python3 "$REPO/tools/robocode_fixture_capture/analyze.py" "$OUT_JSONL"
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runtime proof for Approach (B): load the real DrussGT jar, drive it through
|
||||
# ClassicPeer for 200 synthetic ticks and assert it emits movement intents.
|
||||
# Runtime (triage) proof for Approach (B): load the real bot jar, drive it
|
||||
# through ClassicPeer for 200 synthetic ticks and assert it emits movement
|
||||
# intents. No Tank Royale server / physics is involved.
|
||||
#
|
||||
# ./run_smoke.sh # DrussGT (default, unchanged)
|
||||
# SHIM_BOT_JAR=/tmp/bots/Diamond.jar SHIM_BOT_CLASS=voidious.Diamond ./run_smoke.sh
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROBOCODE_JAR="${ROBOCODE_JAR:-/tmp/robocode/install/libs/robocode.jar}"
|
||||
DRUSSGT_JAR="${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}"
|
||||
JAR="${SHIM_BOT_JAR:-${DRUSSGT_JAR:-/tmp/drussgt/DrussGT.jar}}"
|
||||
CLASS="${SHIM_BOT_CLASS:-jk.mega.DrussGT}"
|
||||
OUT="${OUT:-$HERE/out}"
|
||||
|
||||
WORK="$(mktemp -d /tmp/shim_smoke.XXXXXX)"
|
||||
java -cp "$OUT:$ROBOCODE_JAR" robocode_shim.SmokeTest "$DRUSSGT_JAR" "$WORK"
|
||||
java -cp "$OUT:$ROBOCODE_JAR" robocode_shim.SmokeTest "$JAR" "$WORK" "$CLASS"
|
||||
|
||||
@@ -2,24 +2,24 @@ package robocode_shim;
|
||||
|
||||
import robocode.Event;
|
||||
import robocode.robotinterfaces.IBasicRobot;
|
||||
import robocode.robotinterfaces.IAdvancedRobot;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* APPROACH (B): load the REAL, unmodified DrussGT jar with a classloader, hand
|
||||
* it our {@link ClassicPeer} via {@code _RobotBase.setPeer()}, and run its own
|
||||
* {@code run()} loop on a worker thread. No robocode.* class is re-implemented.
|
||||
* APPROACH (B): load a REAL, unmodified classic Robocode robot jar with a
|
||||
* classloader, hand it our {@link ClassicPeer} via {@code _RobotBase.setPeer()},
|
||||
* and run its own {@code run()} loop on a worker thread. No robocode.* class is
|
||||
* re-implemented.
|
||||
*
|
||||
* <pre>
|
||||
* URLClassLoader cl = new URLClassLoader(new URL[]{ drussgtJar }, robocodeParent );
|
||||
* Class<?> c = cl.loadClass("jk.mega.DrussGT");
|
||||
* URLClassLoader cl = new URLClassLoader(new URL[]{ botJar }, robocodeParent );
|
||||
* Class<?> c = cl.loadClass(mainClass);
|
||||
* IBasicRobot bot = (IBasicRobot) c.getDeclaredConstructor().newInstance();
|
||||
* bot.setPeer(peer); // the seam
|
||||
* peer.attachRobot(bot);
|
||||
@@ -27,24 +27,41 @@ import java.util.List;
|
||||
* </pre>
|
||||
*
|
||||
* 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 {
|
||||
|
||||
/** Default hosted class — the original hardcoded DrussGT (backwards compatible). */
|
||||
public static final String DEFAULT_MAIN_CLASS = LegacyBotBridge.DEFAULT_BOT_CLASS;
|
||||
|
||||
public final ClassicPeer peer;
|
||||
public volatile IBasicRobot robot;
|
||||
public final URLClassLoader loader;
|
||||
public final String mainClass;
|
||||
private final File dataDir;
|
||||
private final Thread thread;
|
||||
private volatile Throwable botError;
|
||||
|
||||
public BotHost(File drussgtJar, File dataDir) throws Exception {
|
||||
/** DrussGT default: {@code jk.mega.DrussGT}. */
|
||||
public BotHost(File botJar, File dataDir) throws Exception {
|
||||
this(DEFAULT_MAIN_CLASS, botJar, Collections.emptyList(), dataDir);
|
||||
}
|
||||
|
||||
public BotHost(String mainClass, File botJar, List<File> extraJars, File dataDir) throws Exception {
|
||||
this.mainClass = mainClass;
|
||||
this.dataDir = dataDir;
|
||||
this.peer = new ClassicPeer(dataDir);
|
||||
this.loader = new URLClassLoader(
|
||||
new URL[]{drussgtJar.toURI().toURL()},
|
||||
BotHost.class.getClassLoader());
|
||||
Class<?> cls = loader.loadClass("jk.mega.DrussGT");
|
||||
URL[] urls = new URL[1 + extraJars.size()];
|
||||
urls[0] = botJar.toURI().toURL();
|
||||
for (int i = 0; i < extraJars.size(); i++) {
|
||||
urls[i + 1] = extraJars.get(i).toURI().toURL();
|
||||
}
|
||||
this.loader = new URLClassLoader(urls, BotHost.class.getClassLoader());
|
||||
Class<?> cls = loader.loadClass(mainClass);
|
||||
this.robot = (IBasicRobot) cls.getDeclaredConstructor().newInstance();
|
||||
robot.setPeer(peer);
|
||||
robot.setOut(new PrintStream(new File(dataDir, "bot.out")));
|
||||
@@ -54,37 +71,50 @@ public class BotHost {
|
||||
robot.getRobotRunnable().run();
|
||||
} catch (Throwable t) {
|
||||
botError = t;
|
||||
if (!t.getClass().getName().contains("ThreadInterrupted")) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
peer.markDead();
|
||||
}
|
||||
}, "DrussGT-run");
|
||||
}, mainClass + "-run");
|
||||
this.thread.setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classic Robocode gives every round a fresh robot instance (only static
|
||||
* fields survive). DrussGT sets up per-round state in run(), but mirroring
|
||||
* the engine's instance lifetime removes a whole class of cross-round bugs.
|
||||
* fields survive). Mirroring the engine's instance lifetime removes a whole
|
||||
* class of cross-round bugs.
|
||||
*/
|
||||
public IBasicRobot newRobot() throws Exception {
|
||||
Class<?> cls = loader.loadClass("jk.mega.DrussGT");
|
||||
Class<?> cls = loader.loadClass(mainClass);
|
||||
IBasicRobot r = (IBasicRobot) cls.getDeclaredConstructor().newInstance();
|
||||
r.setPeer(peer);
|
||||
r.setOut(new PrintStream(new File(dataDir, "bot.out")));
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Optional: force the shield subsystem off so movement runs its pure surfer path. */
|
||||
public void disableShield() throws Exception {
|
||||
java.lang.reflect.Field f = loader.loadClass("jk.mega.DrussGT").getDeclaredField("shieldEnabled");
|
||||
f.setAccessible(true);
|
||||
f.setBoolean(null, false);
|
||||
}
|
||||
|
||||
/** Optional: call a named no-arg method via the IBasicEvents interface. */
|
||||
public void invokeBasicEvent(String method, Class<?> argType, Event event) throws Exception {
|
||||
Method m = robot.getClass().getMethod(method, argType);
|
||||
m.invoke(robot, event);
|
||||
/**
|
||||
* Force a static boolean field of the hosted class to {@code false}. Used
|
||||
* for DrussGT's {@code shieldEnabled} (its EnergyDome shield is a separate,
|
||||
* timing-sensitive subsystem; the proven path is the pure wave surfer).
|
||||
* Silent no-op when the class has no such field, so it is harmless for other
|
||||
* bots.
|
||||
*
|
||||
* @return true if the field existed and was set.
|
||||
*/
|
||||
public boolean disableStaticBoolean(String fieldName) {
|
||||
try {
|
||||
Field f = loader.loadClass(mainClass).getDeclaredField(fieldName);
|
||||
if (f.getType() != boolean.class && f.getType() != Boolean.class) {
|
||||
return false;
|
||||
}
|
||||
f.setAccessible(true);
|
||||
f.setBoolean(null, false);
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
|
||||
@@ -3,13 +3,17 @@ package robocode_shim;
|
||||
import robocode.Bullet;
|
||||
import robocode.Condition;
|
||||
import robocode.Event;
|
||||
import robocode.MessageEvent;
|
||||
import robocode.StatusEvent;
|
||||
import robocode.robotinterfaces.IBasicEvents;
|
||||
import robocode.robotinterfaces.IBasicRobot;
|
||||
import robocode.robotinterfaces.peer.IAdvancedRobotPeer;
|
||||
import robocode.robotinterfaces.peer.ITeamRobotPeer;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -36,7 +40,7 @@ import java.util.List;
|
||||
* tracked.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
public class ClassicPeer implements ITeamRobotPeer {
|
||||
|
||||
// ---- optional live world model (battle mode) ----
|
||||
public volatile WorldModel world;
|
||||
@@ -102,6 +106,7 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
private volatile boolean turnReady = false;
|
||||
private volatile boolean shutdown = false;
|
||||
private volatile boolean dead = false;
|
||||
private volatile boolean dispatching = false;
|
||||
private int executedTurns = 0;
|
||||
private final List<Event> pendingEvents = 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; }
|
||||
|
||||
// ================= movement / action intents =================
|
||||
@Override public void move(double distance) { setMove(distance); }
|
||||
@Override public void turnBody(double radians) { setTurnBody(radians); }
|
||||
@Override public void turnGun(double radians) { setTurnGun(radians); }
|
||||
@Override public void turnRadar(double radians) { setTurnRadar(radians); }
|
||||
// NB: move/turnBody/turnGun/turnRadar are the *immediate* (non-advanced)
|
||||
// variants used by robocode.Robot.ahead()/turnRight()/... and by
|
||||
// AdvancedRobot.turnRightRadians()/turnRadarRightRadians()/...: the genuine
|
||||
// engine performs the turn AND ends the turn in one call. A bot whose run()
|
||||
// is `while (true) turnRadarRightRadians(1);` (e.g. Komarious) therefore
|
||||
// advances one turn per call; setting the intent without executing made such
|
||||
// bots spin forever.
|
||||
@Override public void move(double distance) { setMove(distance); execute(); }
|
||||
@Override public void turnBody(double radians) { setTurnBody(radians); execute(); }
|
||||
@Override public void turnGun(double radians) { setTurnGun(radians); execute(); }
|
||||
@Override public void turnRadar(double radians) { setTurnRadar(radians); execute(); }
|
||||
|
||||
@Override public void setMove(double distance) {
|
||||
lastMove = distance; moveSet = true;
|
||||
@@ -196,6 +208,14 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
h.onExecute();
|
||||
return;
|
||||
}
|
||||
// Re-entrant execute() from inside an event handler (classic blocking
|
||||
// robots call Robot.fire()/ahead() inside onScannedRobot): the genuine
|
||||
// engine ends the turn, but in this single-threaded standalone model the
|
||||
// driver is already mid-dispatch, so a nested wait would deadlock. The
|
||||
// intent has been recorded and will be applied on the next turn.
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
synchronized (lock) {
|
||||
while (!turnReady && !shutdown) {
|
||||
try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
@@ -207,7 +227,12 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
snapshot = new ArrayList<>(pendingEvents);
|
||||
pendingEvents.clear();
|
||||
}
|
||||
dispatch(snapshot);
|
||||
dispatching = true;
|
||||
try {
|
||||
dispatch(snapshot);
|
||||
} finally {
|
||||
dispatching = false;
|
||||
}
|
||||
synchronized (lock) {
|
||||
executedTurns++;
|
||||
lock.notifyAll();
|
||||
@@ -240,9 +265,42 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
|
||||
/** Battle mode: forward one already-built classic event to the robot's listeners. */
|
||||
public void deliver(Event e) {
|
||||
synchronized (lock) { allEvents.add(e); }
|
||||
dispatch(Collections.singletonList(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesise a classic {@link StatusEvent} snapshot of the current world
|
||||
* state. Classic Robocode delivers one every turn; bots that inspect
|
||||
* {@code getStatusEvents()}/{@code getAllEvents()} or override
|
||||
* {@code onStatus()} rely on it (e.g. RougeDC's version check busy-loops
|
||||
* forever without it). {@code RobotStatus} has a private constructor, so it
|
||||
* is built reflectively.
|
||||
*/
|
||||
public StatusEvent makeStatusEvent() {
|
||||
try {
|
||||
if (STATUS_CTOR == null) {
|
||||
Class<?> rs = Class.forName("robocode.RobotStatus");
|
||||
STATUS_CTOR = rs.getDeclaredConstructor(
|
||||
double.class, double.class, double.class, double.class,
|
||||
double.class, double.class, double.class, double.class,
|
||||
double.class, double.class, double.class, double.class,
|
||||
int.class, int.class, int.class, int.class, long.class);
|
||||
STATUS_CTOR.setAccessible(true);
|
||||
}
|
||||
Object status = STATUS_CTOR.newInstance(
|
||||
getEnergy(), getX(), getY(), getBodyHeading(), getGunHeading(), getRadarHeading(),
|
||||
getVelocity(), getBodyTurnRemaining(), getRadarTurnRemaining(), getGunTurnRemaining(),
|
||||
getDistanceRemaining(), getGunHeat(),
|
||||
getOthers(), getNumSentries(), getRoundNum(), getNumRounds(), getTime());
|
||||
return new StatusEvent((robocode.RobotStatus) status);
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile java.lang.reflect.Constructor<?> STATUS_CTOR;
|
||||
|
||||
// ================= tick bridge control (driver side) =================
|
||||
/** Standalone driver: publish this turn's events, then unblock the bot's execute(). */
|
||||
public void provideTurn(List<Event> events) {
|
||||
@@ -288,6 +346,17 @@ public class ClassicPeer implements IAdvancedRobotPeer {
|
||||
@Override public void setRadarColor(Color c) { radarColor = c; }
|
||||
@Override public void setBulletColor(Color c) { bulletColor = c; }
|
||||
@Override public void setScanColor(Color c) { scanColor = c; }
|
||||
|
||||
// ================= team peer surface (1v1: no teammates, no messages) =================
|
||||
// Robocode's TeamRobot setPeer() casts the peer to ITeamRobotPeer, so a
|
||||
// whole class of classic bots (many of which merely used TeamRobot as a
|
||||
// base class) could not even start without these.
|
||||
@Override public String[] getTeammates() { return new String[0]; }
|
||||
@Override public boolean isTeammate(String name) { return false; }
|
||||
@Override public void broadcastMessage(Serializable message) throws IOException { }
|
||||
@Override public void sendMessage(String name, Serializable message) throws IOException { }
|
||||
@Override public List<MessageEvent> getMessageEvents() { return Collections.emptyList(); }
|
||||
|
||||
@Override public void getCall() { }
|
||||
@Override public void setCall() { }
|
||||
@Override public Graphics2D getGraphics() { return null; }
|
||||
|
||||
@@ -1,456 +1,18 @@
|
||||
package robocode_shim;
|
||||
|
||||
import dev.robocode.tankroyale.botapi.BaseBot;
|
||||
import dev.robocode.tankroyale.botapi.Bot;
|
||||
import dev.robocode.tankroyale.botapi.BulletState;
|
||||
import dev.robocode.tankroyale.botapi.events.BotDeathEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletFiredEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitBulletEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.BulletHitWallEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.HitBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.HitByBulletEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.ScannedBotEvent;
|
||||
import dev.robocode.tankroyale.botapi.events.WonRoundEvent;
|
||||
|
||||
import robocode.robotinterfaces.IBasicRobot;
|
||||
import robocode.util.Utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A genuine Tank Royale bot that hosts the unmodified classic Robocode DrussGT
|
||||
* jar and drives it through {@link ClassicPeer} / {@link BotHost}.
|
||||
* Backwards-compatible alias for the original DrussGT-specific bridge.
|
||||
*
|
||||
* <p>It is a {@link Bot} (the TR API's classic-movement subclass) so that the
|
||||
* peer's classic intents can be forwarded one-to-one onto the TR Bot API's own
|
||||
* classic motion model:
|
||||
*
|
||||
* <pre>
|
||||
* classic (peer) -> Tank Royale Bot API
|
||||
* setAhead(d) setForward(d)
|
||||
* setTurnRightRadians(r) setTurnRight(toDegrees(r))
|
||||
* setTurnGunRightRadians setTurnGunRight(toDegrees(r))
|
||||
* setTurnRadarRightRadians setTurnRadarRight(toDegrees(r))
|
||||
* setFireBullet(p) setFire(p) + bullet-identity tracking
|
||||
* execute() go()
|
||||
* </pre>
|
||||
*
|
||||
* <p>DrussGT's own {@code run()} is executed on the Tank Royale bot thread
|
||||
* (exactly where the official robocode-api-bridge runs a legacy robot), and its
|
||||
* {@code execute()} calls {@code go()} through {@link ClassicPeer.TickHost}.
|
||||
* TR events are synthesised into classic events and delivered synchronously, so
|
||||
* classic event ordering (TR's event queue priorities) and the classic "events
|
||||
* are delivered before the next turn's run() body" contract both hold.
|
||||
* <p>The bridge is now generalized in {@link LegacyBotBridge}: the hosted bot's
|
||||
* main class/jar/data directory are configurable and DrussGT is only the
|
||||
* default. This class remains so that already-generated bot directories
|
||||
* (e.g. {@code /tmp/tr_bots/DrussGT/DrussGT.sh}) that launch
|
||||
* {@code robocode_shim.DrussGTBridge} keep working unchanged.
|
||||
*/
|
||||
public final class DrussGTBridge extends Bot
|
||||
implements ClassicPeer.WorldModel, ClassicPeer.TickHost {
|
||||
|
||||
// ---- DrussGT host ----
|
||||
private BotHost host;
|
||||
private ClassicPeer peer;
|
||||
private IBasicRobot robot;
|
||||
private boolean firstRound = true;
|
||||
|
||||
// ---- bullet identity ----
|
||||
private static final class BulletReflection {
|
||||
static final Field X, Y, ACTIVE;
|
||||
static {
|
||||
Field x = null, y = null, active = null;
|
||||
try {
|
||||
x = robocode.Bullet.class.getDeclaredField("x");
|
||||
x.setAccessible(true);
|
||||
y = robocode.Bullet.class.getDeclaredField("y");
|
||||
y.setAccessible(true);
|
||||
active = robocode.Bullet.class.getDeclaredField("isActive");
|
||||
active.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
// leave null; position/active updates are best-effort
|
||||
}
|
||||
X = x; Y = y; ACTIVE = active;
|
||||
}
|
||||
|
||||
static void setXY(robocode.Bullet b, double x, double y) {
|
||||
try { if (X != null) X.setDouble(b, x); if (Y != null) Y.setDouble(b, y); } catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
static void setActive(robocode.Bullet b, boolean active) {
|
||||
try { if (ACTIVE != null) ACTIVE.setBoolean(b, active); } catch (Exception ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private final List<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 final class DrussGTBridge extends LegacyBotBridge {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ThreadManagerFix.install();
|
||||
new DrussGTBridge().start();
|
||||
}
|
||||
|
||||
public DrussGTBridge() {
|
||||
super();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// lifecycle
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ensureHost();
|
||||
resetRoundState();
|
||||
|
||||
IBasicRobot bot;
|
||||
if (firstRound) {
|
||||
firstRound = false;
|
||||
bot = host.robot;
|
||||
} else {
|
||||
bot = host.newRobot();
|
||||
}
|
||||
robot = bot;
|
||||
peer.attachRobot(bot);
|
||||
|
||||
// DrussGT's own run() lives on this (the Tank Royale bot) thread.
|
||||
bot.getRobotRunnable().run();
|
||||
} catch (Throwable t) {
|
||||
if (!isInterrupt(t)) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInterrupt(Throwable t) {
|
||||
return t != null && t.getClass().getName().contains("ThreadInterrupted");
|
||||
}
|
||||
|
||||
private void ensureHost() throws Exception {
|
||||
if (host != null) {
|
||||
return;
|
||||
}
|
||||
ThreadManagerFix.install();
|
||||
File jar = new File(envOr("DRUSSGT_JAR", "/tmp/drussgt/DrussGT.jar"));
|
||||
File dataDir = new File(envOr("DRUSSGT_DATA", "/tmp/drussgt_bridge_data"));
|
||||
host = new BotHost(jar, dataDir);
|
||||
peer = host.peer;
|
||||
peer.world = this;
|
||||
peer.tickHost = this;
|
||||
// DrussGT's EnergyDome shield is a separate "precise" subsystem whose
|
||||
// first-round behaviour depends on exact classic bullet-detection timing;
|
||||
// the proven path (see SmokeTest / README) is to run the pure wave
|
||||
// surfer (DrussMoveGT). Set DRUSSGT_SHIELD=1 to keep the shield on.
|
||||
if (!"1".equals(System.getenv("DRUSSGT_SHIELD"))) {
|
||||
host.disableShield();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetRoundState() {
|
||||
unmatchedFired.clear();
|
||||
firedById.clear();
|
||||
nextTempBulletId = -1;
|
||||
if (peer != null) {
|
||||
peer.stopSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String envOr(String key, String fallback) {
|
||||
String v = System.getenv(key);
|
||||
return (v == null || v.isBlank()) ? fallback : v;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ClassicPeer.TickHost — driven from DrussGT's execute()
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void onExecute() {
|
||||
applyIntents();
|
||||
go();
|
||||
}
|
||||
|
||||
private void applyIntents() {
|
||||
if (DEBUG) {
|
||||
debugLog(String.format(java.util.Locale.ROOT,
|
||||
"t=%d move=%s/%.3f body=%.4f gun=%.4f radar=%.4f trDistRem=%.3f trSpeed=%.3f trTurnRem=%.3f trRadarRem=%.3f",
|
||||
getTurnNumber(), peer.moveSet, peer.lastMove, peer.lastTurnBody, peer.lastTurnGun,
|
||||
peer.lastTurnRadar, getDistanceRemaining(), getSpeed(), getTurnRemaining(), getRadarTurnRemaining()));
|
||||
}
|
||||
if (peer.stopSet) {
|
||||
if (peer.stopRequested) {
|
||||
setStop();
|
||||
} else {
|
||||
setResume();
|
||||
}
|
||||
peer.stopSet = false;
|
||||
}
|
||||
if (peer.moveSet) {
|
||||
setForward(peer.lastMove);
|
||||
peer.moveSet = false;
|
||||
}
|
||||
if (peer.turnBodySet) {
|
||||
setTurnRight(Math.toDegrees(peer.lastTurnBody));
|
||||
peer.turnBodySet = false;
|
||||
}
|
||||
if (peer.turnGunSet) {
|
||||
setTurnGunRight(Math.toDegrees(peer.lastTurnGun));
|
||||
peer.turnGunSet = false;
|
||||
}
|
||||
if (peer.turnRadarSet) {
|
||||
setTurnRadarRight(Math.toDegrees(peer.lastTurnRadar));
|
||||
peer.turnRadarSet = false;
|
||||
}
|
||||
if (peer.maxVelocitySet) {
|
||||
setMaxSpeed(peer.maxVelocity);
|
||||
peer.maxVelocitySet = false;
|
||||
}
|
||||
if (peer.maxTurnRateSet) {
|
||||
setMaxTurnRate(peer.maxTurnRate);
|
||||
peer.maxTurnRateSet = false;
|
||||
}
|
||||
setAdjustGunForBodyTurn(peer.adjustGunForBodyTurn);
|
||||
setAdjustRadarForGunTurn(peer.adjustRadarForGunTurn);
|
||||
setAdjustRadarForBodyTurn(peer.adjustRadarForBodyTurn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public robocode.Bullet onFire(double power) {
|
||||
if (Double.isNaN(power) || power <= 0) {
|
||||
return null;
|
||||
}
|
||||
boolean accepted;
|
||||
try {
|
||||
accepted = setFire(power);
|
||||
} catch (RuntimeException ex) {
|
||||
return null;
|
||||
}
|
||||
if (!accepted) {
|
||||
return null;
|
||||
}
|
||||
robocode.Bullet bullet = new robocode.Bullet(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getGunDirection())),
|
||||
getX(), getY(), power, botName, null, true, nextTempBulletId--);
|
||||
unmatchedFired.add(bullet);
|
||||
return bullet;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ClassicPeer.WorldModel — live TR state converted to classic
|
||||
// ======================================================================
|
||||
|
||||
@Override public String name() {
|
||||
return botName;
|
||||
}
|
||||
@Override public long time() { return getTurnNumber(); }
|
||||
@Override public double energy() { return getEnergy(); }
|
||||
@Override public double x() { return getX(); }
|
||||
@Override public double y() { return getY(); }
|
||||
@Override public double velocity() { return getSpeed(); }
|
||||
@Override public double bodyHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getDirection())); }
|
||||
@Override public double gunHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getGunDirection())); }
|
||||
@Override public double radarHeading() { return TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(getRadarDirection())); }
|
||||
@Override public double gunHeat() { return getGunHeat(); }
|
||||
@Override public double battleFieldWidth() { return getArenaWidth(); }
|
||||
@Override public double battleFieldHeight() { return getArenaHeight(); }
|
||||
@Override public int others() { return getEnemyCount(); }
|
||||
@Override public int numSentries() { return 0; }
|
||||
@Override public int numRounds() { return getNumberOfRounds(); }
|
||||
@Override public int roundNum() { return getRoundNumber() - 1; }
|
||||
@Override public int sentryBorderSize() { return 0; }
|
||||
@Override public double gunCoolingRate() { return getGunCoolingRate(); }
|
||||
@Override public double distanceRemaining() { return getDistanceRemaining(); }
|
||||
@Override public double bodyTurnRemaining() { return -Math.toRadians(getTurnRemaining()); }
|
||||
@Override public double gunTurnRemaining() { return -Math.toRadians(getGunTurnRemaining()); }
|
||||
@Override public double radarTurnRemaining() { return -Math.toRadians(getRadarTurnRemaining()); }
|
||||
|
||||
// ======================================================================
|
||||
// Tank Royale events -> classic events
|
||||
// ======================================================================
|
||||
|
||||
@Override
|
||||
public void onScannedBot(ScannedBotEvent e) {
|
||||
double dx = e.getX() - getX();
|
||||
double dy = e.getY() - getY();
|
||||
double distance = Math.hypot(dx, dy);
|
||||
// classic absolute bearing: atan2(dx, dy), 0 = North, clockwise
|
||||
double absBearing = Math.atan2(dx, dy);
|
||||
double bearing = Utils.normalRelativeAngle(absBearing - bodyHeading());
|
||||
double enemyHeading = Utils.normalAbsoluteAngle(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(e.getDirection())));
|
||||
robocode.ScannedRobotEvent classic = new robocode.ScannedRobotEvent(
|
||||
String.valueOf(e.getScannedBotId()), e.getEnergy(), bearing, distance,
|
||||
enemyHeading, e.getSpeed());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitByBullet(HitByBulletEvent e) {
|
||||
BulletState bs = e.getBullet();
|
||||
double bearing = classicBearingTo(bs.getX(), bs.getY());
|
||||
robocode.Bullet bullet = mapBullet(bs, name());
|
||||
robocode.HitByBulletEvent classic = new robocode.HitByBulletEvent(bearing, bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHit(BulletHitBotEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletHitEvent classic = new robocode.BulletHitEvent(
|
||||
String.valueOf(e.getVictimId()), e.getEnergy(), bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHitBullet(BulletHitBulletEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
robocode.Bullet hitBullet = mapBullet(e.getHitBullet(), null);
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletHitBulletEvent classic = new robocode.BulletHitBulletEvent(bullet, hitBullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletHitWall(BulletHitWallEvent e) {
|
||||
robocode.Bullet bullet = ownBullet(e.getBullet());
|
||||
BulletReflection.setActive(bullet, false);
|
||||
robocode.BulletMissedEvent classic = new robocode.BulletMissedEvent(bullet);
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBulletFired(BulletFiredEvent e) {
|
||||
BulletState bs = e.getBullet();
|
||||
robocode.Bullet match = takeNearestUnmatched(bs.getX(), bs.getY());
|
||||
if (match != null) {
|
||||
firedById.put(bs.getBulletId(), match);
|
||||
BulletReflection.setXY(match, bs.getX(), bs.getY());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitBot(HitBotEvent e) {
|
||||
double bearing = classicBearingTo(e.getX(), e.getY());
|
||||
robocode.HitRobotEvent classic = new robocode.HitRobotEvent(
|
||||
String.valueOf(e.getVictimId()), bearing, e.getEnergy(), e.isRammed());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHitWall(dev.robocode.tankroyale.botapi.events.HitWallEvent e) {
|
||||
robocode.HitWallEvent classic = new robocode.HitWallEvent(wallBearing());
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBotDeath(BotDeathEvent e) {
|
||||
robocode.RobotDeathEvent classic = new robocode.RobotDeathEvent(String.valueOf(e.getVictimId()));
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWonRound(WonRoundEvent e) {
|
||||
robocode.WinEvent classic = new robocode.WinEvent();
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(dev.robocode.tankroyale.botapi.events.DeathEvent e) {
|
||||
robocode.DeathEvent classic = new robocode.DeathEvent();
|
||||
classic.setTime(getTurnNumber());
|
||||
peer.deliver(classic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkippedTurn(dev.robocode.tankroyale.botapi.events.SkippedTurnEvent e) {
|
||||
// Deliberately NOT forwarded: classic SkippedTurnEvent is a diagnostic
|
||||
// that only exists because a robot took too long; our bridge never
|
||||
// "skips" a turn, and DrussGT treats it as a fatal signal.
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// helpers
|
||||
// ======================================================================
|
||||
|
||||
private double classicBearingTo(double tx, double ty) {
|
||||
double dx = tx - getX();
|
||||
double dy = ty - getY();
|
||||
return Utils.normalRelativeAngle(Math.atan2(dx, dy) - bodyHeading());
|
||||
}
|
||||
|
||||
/** Classic HitWallEvent bearing to the nearest wall normal. */
|
||||
private double wallBearing() {
|
||||
double dx = getX() - getArenaWidth() / 2.0;
|
||||
double dy = getY() - getArenaHeight() / 2.0;
|
||||
double abs;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
abs = dx >= 0 ? Math.toRadians(270) : Math.toRadians(90);
|
||||
} else {
|
||||
abs = dy >= 0 ? Math.toRadians(180) : 0.0;
|
||||
}
|
||||
return Utils.normalRelativeAngle(abs - bodyHeading());
|
||||
}
|
||||
|
||||
private robocode.Bullet mapBullet(BulletState bs, String victim) {
|
||||
return new robocode.Bullet(
|
||||
TankRoyaleBridge.tankRadToClassicRad(Math.toRadians(bs.getDirection())),
|
||||
bs.getX(), bs.getY(), bs.getPower(),
|
||||
String.valueOf(bs.getOwnerId()), victim, false, bs.getBulletId());
|
||||
}
|
||||
|
||||
/** Our own bullet: return the exact instance DrussGT got from setFireBullet. */
|
||||
private robocode.Bullet ownBullet(BulletState bs) {
|
||||
robocode.Bullet b = firedById.get(bs.getBulletId());
|
||||
if (b == null) {
|
||||
b = mapBullet(bs, null);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private robocode.Bullet takeNearestUnmatched(double x, double y) {
|
||||
robocode.Bullet best = null;
|
||||
double bestDist = Double.MAX_VALUE;
|
||||
for (robocode.Bullet b : unmatchedFired) {
|
||||
double d = Math.pow(b.getX() - x, 2) + Math.pow(b.getY() - y, 2);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = b;
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
unmatchedFired.remove(best);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) -> 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;
|
||||
|
||||
/**
|
||||
* Runtime proof for APPROACH (B): instantiate the real DrussGT from its jar,
|
||||
* setPeer(), run its own run() loop, feed it synthetic ScannedRobotEvents and
|
||||
* observe that it emits movement/fire intents through ClassicPeer.
|
||||
* Runtime proof for APPROACH (B): instantiate a real classic Robocode robot
|
||||
* from its jar, setPeer(), run its own run() loop, feed it synthetic
|
||||
* ScannedRobotEvents and observe that it emits movement/fire intents through
|
||||
* ClassicPeer.
|
||||
*
|
||||
* This is an architecture spike: there is no physics here and the peer's world
|
||||
* state does not integrate the intents. If the bot starts, survives its
|
||||
* constructor (DrussMoveGT/MeleeRadar/DrussGunDC/EnergyDomeWorker) and produces
|
||||
* non-zero setAhead/setTurnRight intents, the seam is proven operational.
|
||||
* <p>This is a <em>triage</em> tool (no physics, no Tank Royale server): it
|
||||
* answers "does this bot's class load, construct, run its event loop and emit
|
||||
* intents through the peer?" far faster than a battle. A real battle is still
|
||||
* the authoritative validation (see {@code run_bridge_battle.sh}).
|
||||
*
|
||||
* <pre>
|
||||
* java -cp out:robocode.jar robocode_shim.SmokeTest <jar> <workdir> [mainClass]
|
||||
* </pre>
|
||||
*
|
||||
* With no {@code mainClass} the original DrussGT path is used unchanged.
|
||||
*/
|
||||
public class SmokeTest {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ThreadManagerFix.install();
|
||||
File jar = new File(args.length > 0 ? args[0] : "/tmp/drussgt/DrussGT.jar");
|
||||
File jar = new File(args.length > 0 ? args[0] : LegacyBotBridge.DEFAULT_BOT_JAR);
|
||||
File work = new File(args.length > 1 ? args[1] : "/tmp/shim_smoke");
|
||||
String mainClass = args.length > 2 ? args[2] : BotHost.DEFAULT_MAIN_CLASS;
|
||||
if (!work.exists()) work.mkdirs();
|
||||
|
||||
System.out.println("DrussGT jar: " + jar + " (" + jar.length() + " bytes)");
|
||||
System.out.println("bot jar : " + jar + " (" + jar.length() + " bytes)");
|
||||
System.out.println("main class: " + mainClass);
|
||||
|
||||
BotHost host = new BotHost(jar, work);
|
||||
host.disableShield(); // run the pure surfer path (skip EnergyDome shield)
|
||||
BotHost host = new BotHost(mainClass, jar, java.util.Collections.emptyList(), work);
|
||||
host.disableStaticBoolean("shieldEnabled"); // DrussGT: run the pure surfer path
|
||||
host.start();
|
||||
|
||||
int ticks = 200;
|
||||
@@ -47,8 +56,8 @@ public class SmokeTest {
|
||||
p.gunHeat = Math.max(0, 3.0 - t * 0.1);
|
||||
p.distanceRemaining = 0;
|
||||
|
||||
// A moving, orbiting enemy: enough feature variation that the
|
||||
// KD-tree in EnemyMoves does not see degenerate duplicate points.
|
||||
// A moving, orbiting enemy: enough feature variation that a
|
||||
// KD-tree does not see degenerate duplicate points.
|
||||
double theta = Math.toRadians(t * 7.0);
|
||||
double bearing = theta; // bot heading is 0
|
||||
double distance = 300.0 + 80.0 * Math.sin(theta / 2.3);
|
||||
@@ -56,7 +65,15 @@ public class SmokeTest {
|
||||
double enemyEnergy = 100.0 - 0.1 * (t % 10);
|
||||
ScannedRobotEvent scan = new ScannedRobotEvent(
|
||||
"Enemy", enemyEnergy, bearing, distance, enemyHeading, 8.0);
|
||||
if (!host.tick(Collections.singletonList(scan))) { botSurvived = false; break; }
|
||||
// Classic delivers a StatusEvent every turn (priority 0 => first).
|
||||
robocode.StatusEvent status = p.makeStatusEvent();
|
||||
java.util.List<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 (movementIntentTicks == 0) { firstMove = p.lastMove; firstTurn = p.lastTurnBody; }
|
||||
@@ -84,7 +101,7 @@ public class SmokeTest {
|
||||
|
||||
host.stop();
|
||||
boolean ok = movementIntentTicks > 0 && botSurvived;
|
||||
System.out.println(ok ? "SMOKE TEST: OK (real DrussGT drove through the peer seam)"
|
||||
System.out.println(ok ? "SMOKE TEST: OK (" + mainClass + " drove through the peer seam)"
|
||||
: "SMOKE TEST: no movement intents observed");
|
||||
System.exit(ok ? 0 : 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user