Four of the five adversaries imported the OLD package (tankroyale_botapi 1.0.1); only SittingDuck used robocode_tankroyale_botapi 1.0.7, which is what the rest of the repo requires. A previous report claimed OscillatorBot was already on 1.0.7 - that was WRONG, and OscillatorBot turned out to crash the MOST (8 SIGSEGVs in the first reproduction, 15 in its historical /tmp logs). THE CRASH, reproduced with an identical stack in every case: botThreadEntry -> run -> adversary run -> go -> dispatchPendingEvents -> tankroyale_botapi-1.0.1/event_queue.nim(89) addEvent -> realloc/rawDealloc -> SIGSEGV Counts, old API: 60 melee battles x 8 rounds gave RandomMover 1, PatternMover 3, WaveSurfer 0, OscillatorBot 8; 6 battles x 6 rounds vs SittingDuck gave 4/2/0/3. ROOT CAUSE: the main->bot event hand-off. 1.0.1 passes a lock-protected seq[BotEvent] (signalTick writes gPendingEvents, dispatchPendingEvents copies it under lock). 1.0.7 uses a Channel[seq[BotEvent]] (send(move(pending)) / tryRecv). The old path copied string-bearing BotEvent payloads across threads every tick, churning ORC refcounts on the shared heap until the freelist was corrupted. 1.0.7's own source documents this as the gdb-confirmed fix. WHY IT MATTERED MORE THAN IT LOOKED: the crash silently corrupted measurements. Against a stationary duck, crash contamination inflated WaveSurfer's rest fraction from 12.4% (clean) to 20.7%; in a focused run the server logged 'Bot left: OscillatorBot' while the game continued and its score stopped growing. So every gauntlet run tonight was fighting adversaries that were partially dead - which is a second, independent reason the user's instinct that these bots were bugged was correct, and why they should not be used as a measurement baseline. (The per-gun REAL hit rates are unaffected: those came from DrussGT battles.) FIX: all four migrated to robocode_tankroyale_botapi 1.0.7. NO API adaptations were needed beyond the module rename - every symbol these bots use is identical in 1.0.7, verified by diffing the two packages (constants/utils/json_parse/ schemas semantically identical; the movement and intent procs in bot.nim are byte-identical). The .nimble files now require robocode_tankroyale_botapi. VERIFIED: 120 melee battles x 8 rounds plus 6x6 vs SittingDuck -> 0 SIGSEGV in all four stderr logs (0 bytes). Behaviour unchanged: sub-1% absolute drift in mean speed, rest fraction, reversal rate, mean range and perpendicular fraction, all within run-to-run spread; the one >=3-sigma flag (WaveSurfer perpendicular relative to DrussGT) was isolated against a stationary opponent and shown to be the chaotic closed loop, not the migration. test_wavesurfer_velocity passes 7/7. NOT migrated, reported only: GotoTest_garage, OscillatorBot_garage (archived copy), PPO_Bot_garage, QBot_garage, SAC_LSTM_Bot_garage - older experiment garages, left alone deliberately.
test_framework
Integration test framework for Tank Royale bots. Spins up a TR server JAR on a random port, compiles your Nim bots, runs a battle via a Java TestBattleRunner subprocess, and returns structured results you can assert on. Local development tool only — no CI support.
Prerequisites
- Java JRE (11+) —
javamust be onPATH - Tank Royale server JAR —
robocode-tankroyale-server-*-all.jar - Tank Royale runner JAR —
robocode-tankroyale-runner.jar
Environment variables
| Variable | Purpose | Default |
|---|---|---|
TR_SERVER_JAR |
Path to the TR server JAR | hardcoded dev path in server_manager.nim |
TR_BATTLE_RUNNER |
Path to the TR runner JAR | hardcoded dev path in runner_process.nim |
TR_BATTLE_RUNNER_DIR |
Directory containing compiled TestBattleRunner.class |
tools/battle_runner/ |
Set at minimum TR_SERVER_JAR and TR_BATTLE_RUNNER to point at your local JAR builds.
One-time Java compilation
cd tools/battle_runner
TANK_ROYALE_JAR=/path/to/robocode-tankroyale-runner.jar ./run_test_battle.sh --help
run_test_battle.sh auto-compiles TestBattleRunner.java when .class is missing or stale. After this, TR_BATTLE_RUNNER_DIR defaults to tools/battle_runner/ and the class file is found automatically.
Setup for your garage
tests/config.nims — add the common_libs path so the import resolves:
--path:"../../common_libs"
Adjust the relative depth to match your garage's position in the repo.
YourBot.nimble — add a test task:
task test, "Compile and run integration tests":
exec "nim c -r --path:../common_libs tests/test_basic_battle.nim"
Run with:
nimble test
Writing tests
import std/[os, unittest]
import test_framework/test_framework
const
myBotDir = currentSourcePath().parentDir.parentDir # garage root
sittingDuck = currentSourcePath().parentDir / "bots" / "SittingDuck"
suite "MyBot basic battle":
test "beats SittingDuck in 3 rounds":
let r = runBattle(@[myBotDir, sittingDuck], rounds = 3)
check r.rounds.len == 3
check r.results.len == 2
let me = block:
var found: BotResult
for b in r.results:
if b.name == "MyBot": found = b
found
check me.totalScore > 0
check "MyBot" in r.winners
runBattle blocks until the battle finishes (or timeout ms elapses). The server process is started once per test binary run and killed on exit.
API reference
runBattle
proc runBattle*(botDirs: seq[string], rounds: int = 10,
timeout: int = 120000): BattleResult
| Parameter | Description |
|---|---|
botDirs |
Absolute or relative paths to bot directories (one per participating bot) |
rounds |
Number of rounds to play (default: 10) |
timeout |
Milliseconds to wait for the battle runner to finish (default: 120000) |
Each bot directory must have src/<BotName>.nim and the compiler writes the binary to out/<BotName>.
BattleResult
BattleResult = object
bots*: seq[string] # bot names in participation order
rounds*: seq[RoundResult] # one entry per round
results*: seq[BotResult] # final standings, sorted by rank
winners*: seq[string] # names of bots with rank == 1
BotResult
BotResult = object
name*: string
totalScore*: int
rank*: int
firstPlaces*: int
survivalCount*: int
RoundResult
RoundResult = object
round*: int
results*: seq[BotRoundResult]
BotRoundResult
BotRoundResult = object
name*: string
score*: int
rank*: int
survived*: bool
Compilation behavior
compileBots runs on every runBattle() call — no caching. Each test that calls runBattle() recompiles all bots. Compile timeout is 30s per bot, hardcoded, independent of runBattle()'s timeout parameter.
Exceptions
runBattle() raises:
| Exception | Cause |
|---|---|
OSError |
Bot compilation failed — compiler output included in message |
IOError |
Battle runner exited non-zero |
TimeoutError |
Any timeout: server startup (15s), compilation (30s/bot), battle runner (per timeout param) |
try:
let r = runBattle(@[myBotDir, sittingDuck], rounds = 3)
check "MyBot" in r.winners
except OSError as e:
echo "Compile failed: ", e.msg
except IOError as e:
echo "Battle runner failed: ", e.msg
except TimeoutError as e:
echo "Timed out: ", e.msg
Zero-value BotResult trap
Searching r.results by name returns a zero-initialized BotResult if the name doesn't match — no error raised. Guard against it:
var myBot: BotResult
for b in r.results:
if b.name == "MyBot": myBot = b
check myBot.name == "MyBot" # catches name mismatch / missing bot
Writing adversary bots
Shared adversary bots
Pre-built adversaries (SittingDuck, OscillatorBot) live at common_libs/test_framework/adversaries/ — import them directly, no need to copy per garage:
const sittingDuck = currentSourcePath().parentDir.parentDir.parentDir /
"common_libs" / "test_framework" / "adversaries" / "SittingDuck"
Minimal bot — does nothing, useful as a baseline target:
tests/bots/SittingDuck/src/SittingDuck.nim
import std/os
import tankroyale_botapi
const botJsonPath = currentSourcePath().parentDir / "SittingDuck.json"
type SittingDuck = ref object of Bot
method run*(bot: SittingDuck) =
while isRunning():
go()
when isMainModule:
var bot = SittingDuck()
start(bot, botJsonPath)
tests/bots/SittingDuck/src/SittingDuck.json — required metadata:
{
"name": "SittingDuck",
"version": "0.1.0",
"authors": ["Test"],
"description": "Does nothing — test adversary",
"gameTypes": ["classic", "1v1"],
"platform": "Nim",
"programmingLang": "Nim"
}
Place adversary bots under tests/bots/<BotName>/src/. The framework infers the binary name from the directory name, so the directory name must match the .nim filename.
Testing parsing offline
parseServerOutput is exported from battle_result.nim and can be imported directly. Use it for fast offline tests — no Java server needed:
import test_framework/battle_result
const fixture = """
{"event":"game_started","bots":["MyBot","Enemy"]}
{"event":"round_ended","round":1,"results":[{"name":"MyBot","score":200,"rank":1,"survived":true},{"name":"Enemy","score":0,"rank":2,"survived":false}]}
{"event":"battle_ended","results":[{"name":"MyBot","totalScore":200,"rank":1,"firstPlaces":1,"survivalCount":1},{"name":"Enemy","totalScore":0,"rank":2,"firstPlaces":0,"survivalCount":0}]}
"""
let r = parseServerOutput(fixture)
assert r.winners == @["MyBot"]
Skipping when JARs are unavailable
import std/os
if not existsEnv("TR_SERVER_JAR") or not existsEnv("TR_BATTLE_RUNNER"):
echo "Skipping integration tests: TR_SERVER_JAR / TR_BATTLE_RUNNER not set"
quit(0)
Put this at the top of your test file, before any suite blocks.
Troubleshooting
TR server JAR not found
Set TR_SERVER_JAR to the full path of your robocode-tankroyale-server-*-all.jar.
TR server did not start within 15s
The server took too long — check that the JAR is valid and the port was free. Also check for leftover java processes from a previous crashed run.
BattleRunner timed out after Nms
The timeout parameter (default 120 000 ms) was exceeded. Increase it: runBattle(..., timeout = 300_000). Also check that bots are connecting to the server (look for compilation errors in test output).
BattleRunner exited with code N
TestBattleRunner.class is missing or TR_BATTLE_RUNNER points at the wrong JAR. Re-run run_test_battle.sh once to recompile.
Zombie java processes after a crash
The server process is registered with addExitProc and killed on normal exit. A hard kill (SIGKILL) of the test binary will leave the server process behind. Kill manually: pkill -f robocode-tankroyale-server.
No .nim source found in .../src/
The bot directory's src/ folder is empty or the .nim file is missing. The compiler expects src/<DirName>.nim (stripping a _garage suffix if present).