diff --git a/OscillatorBot_garage/OscillatorBot.nimble b/OscillatorBot_garage/OscillatorBot.nimble index be99883..b0fdfd5 100644 --- a/OscillatorBot_garage/OscillatorBot.nimble +++ b/OscillatorBot_garage/OscillatorBot.nimble @@ -8,5 +8,8 @@ bin = @["OscillatorBot"] # Dependencies requires "nim >= 2.0.0" -# tankroyale_botapi and radar_lock are vendored in-tree (common_libs/) and wired via -# config.nims --path; no nimble dependency so builds never touch ~/.nimble/pkgs2. +requires "tankroyale_botapi >= 1.0.1" +# radar_lock is vendored in-tree (common_libs/) and wired via config.nims --path + +task test, "Compile and run integration tests": + exec "nim c -r --path:../common_libs tests/test_basic_battle.nim" diff --git a/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.json b/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.json new file mode 100644 index 0000000..70f585d --- /dev/null +++ b/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.json @@ -0,0 +1,9 @@ +{ + "name": "SittingDuck", + "version": "0.1.0", + "authors": ["Test"], + "description": "Does nothing — test adversary", + "gameTypes": ["classic", "1v1"], + "platform": "Nim", + "programmingLang": "Nim" +} diff --git a/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.nim b/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.nim new file mode 100644 index 0000000..86356c5 --- /dev/null +++ b/OscillatorBot_garage/tests/bots/SittingDuck/src/SittingDuck.nim @@ -0,0 +1,15 @@ +# SittingDuck — does nothing, used as a test adversary. +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) diff --git a/OscillatorBot_garage/tests/config.nims b/OscillatorBot_garage/tests/config.nims new file mode 100644 index 0000000..c5ea4ed --- /dev/null +++ b/OscillatorBot_garage/tests/config.nims @@ -0,0 +1 @@ +--path:"../../common_libs" diff --git a/OscillatorBot_garage/tests/test_basic_battle.nim b/OscillatorBot_garage/tests/test_basic_battle.nim new file mode 100644 index 0000000..e6b445a --- /dev/null +++ b/OscillatorBot_garage/tests/test_basic_battle.nim @@ -0,0 +1,30 @@ +## Integration test: OscillatorBot vs SittingDuck, 3 rounds. +## Requires a running TR server (set TR_SERVER_JAR / TR_BATTLE_RUNNER env vars). + +import std/[os, unittest] +import test_framework/test_framework + +const + oscillatorDir = currentSourcePath().parentDir.parentDir # OscillatorBot_garage/ + sittingDuckDir = currentSourcePath().parentDir / "bots" / "SittingDuck" + +suite "OscillatorBot basic battle": + test "beats SittingDuck in 3 rounds": + let r = runBattle(@[oscillatorDir, sittingDuckDir], rounds = 3) + + check r.rounds.len == 3 + check r.results.len == 2 + + let oscillator = block: + var found: BotResult + for b in r.results: + if b.name == "OscillatorBot": found = b + found + let duck = block: + var found: BotResult + for b in r.results: + if b.name == "SittingDuck": found = b + found + + check oscillator.totalScore > duck.totalScore + check "OscillatorBot" in r.winners diff --git a/common_libs/test_framework/README.md b/common_libs/test_framework/README.md new file mode 100644 index 0000000..71279bd --- /dev/null +++ b/common_libs/test_framework/README.md @@ -0,0 +1,205 @@ +# 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+) — `java` must be on `PATH` +- **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 + +```sh +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: + +```nim +--path:"../../common_libs" +``` + +Adjust the relative depth to match your garage's position in the repo. + +**`YourBot.nimble`** — add a test task: + +```nim +task test, "Compile and run integration tests": + exec "nim c -r --path:../common_libs tests/test_basic_battle.nim" +``` + +Run with: + +```sh +nimble test +``` + +--- + +## Writing tests + +```nim +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` + +```nim +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/.nim` and the compiler writes the binary to `out/`. + +### `BattleResult` + +```nim +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` + +```nim +BotResult = object + name*: string + totalScore*: int + rank*: int + firstPlaces*: int + survivalCount*: int +``` + +### `RoundResult` + +```nim +RoundResult = object + round*: int + results*: seq[BotRoundResult] +``` + +### `BotRoundResult` + +```nim +BotRoundResult = object + name*: string + score*: int + rank*: int + survived*: bool +``` + +--- + +## Writing adversary bots + +Minimal bot — does nothing, useful as a baseline target: + +**`tests/bots/SittingDuck/src/SittingDuck.nim`** +```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: +```json +{ + "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//src/`. The framework infers the binary name from the directory name, so the directory name must match the `.nim` filename. + +--- + +## 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/.nim` (stripping a `_garage` suffix if present). diff --git a/common_libs/test_framework/battle_result.nim b/common_libs/test_framework/battle_result.nim new file mode 100644 index 0000000..6831bd6 --- /dev/null +++ b/common_libs/test_framework/battle_result.nim @@ -0,0 +1,82 @@ +import std/[json, strutils, sequtils] + +type + BotRoundResult* = object + name*: string + score*: int + rank*: int + survived*: bool + + RoundResult* = object + round*: int + results*: seq[BotRoundResult] + + BotResult* = object + name*: string + totalScore*: int + rank*: int + firstPlaces*: int + survivalCount*: int + + BattleResult* = object + bots*: seq[string] + rounds*: seq[RoundResult] + results*: seq[BotResult] + winners*: seq[string] # bots with rank == 1 + +proc parseServerOutput*(stdout: string): BattleResult = + for line in stdout.splitLines(): + let s = line.strip() + if s.len == 0 or s[0] != '{': continue + let node = parseJson(s) + case node["event"].getStr() + of "game_started": + result.bots = node["bots"].getElems().mapIt(it.getStr()) + of "round_ended": + var rr = RoundResult(round: node["round"].getInt()) + for r in node["results"].getElems(): + rr.results.add BotRoundResult( + name: r["name"].getStr(), + score: r["score"].getInt(), + rank: r["rank"].getInt(), + survived: r["survived"].getBool() + ) + result.rounds.add rr + of "battle_ended": + for r in node["results"].getElems(): + result.results.add BotResult( + name: r["name"].getStr(), + totalScore: r["totalScore"].getInt(), + rank: r["rank"].getInt(), + firstPlaces: r["firstPlaces"].getInt(), + survivalCount: r["survivalCount"].getInt() + ) + result.winners = result.results.filterIt(it.rank == 1).mapIt(it.name) + else: discard + +when isMainModule: + const sample = """ +{"event":"game_started","bots":["Alpha","Beta"]} +{"event":"round_ended","round":1,"results":[{"name":"Alpha","score":150,"rank":1,"survived":true},{"name":"Beta","score":80,"rank":2,"survived":false}]} +{"event":"round_ended","round":2,"results":[{"name":"Beta","score":200,"rank":1,"survived":true},{"name":"Alpha","score":60,"rank":2,"survived":false}]} +{"event":"battle_ended","results":[{"name":"Alpha","totalScore":500,"rank":1,"firstPlaces":3,"survivalCount":5},{"name":"Beta","totalScore":300,"rank":2,"firstPlaces":1,"survivalCount":2}]} + +not json, should be ignored +""" + let br = parseServerOutput(sample) + assert br.bots == @["Alpha", "Beta"] + assert br.rounds.len == 2 + assert br.rounds[0].round == 1 + assert br.rounds[0].results[0].name == "Alpha" + assert br.rounds[0].results[0].score == 150 + assert br.rounds[0].results[0].rank == 1 + assert br.rounds[0].results[0].survived == true + assert br.rounds[1].results[0].name == "Beta" + assert br.rounds[1].results[1].survived == false + assert br.results.len == 2 + assert br.results[0].name == "Alpha" + assert br.results[0].totalScore == 500 + assert br.results[0].firstPlaces == 3 + assert br.results[0].survivalCount == 5 + assert br.winners == @["Alpha"] + echo "all assertions passed" diff --git a/common_libs/test_framework/bot_compiler.nim b/common_libs/test_framework/bot_compiler.nim new file mode 100644 index 0000000..50c4e89 --- /dev/null +++ b/common_libs/test_framework/bot_compiler.nim @@ -0,0 +1,48 @@ +## Compiles Nim bots before battle tests. +## config.nims in each bot dir already sets --path and --outdir, so we just +## run `nim c src/BotName.nim` from the bot dir and let config.nims do the rest. + +import std/[os, osproc, streams, strformat, strutils, sequtils] + +proc compileBots*(botDirs: seq[string]): seq[string] = + ## Compiles each bot dir, returns absolute paths to compiled binaries. + ## Raises on first compilation failure, message includes compiler output. + for dir in botDirs: + let absDir = absolutePath(dir) + # Infer main source: src/.nim where stem = last path component + # minus optional _garage suffix (e.g. OscillatorBot_garage → OscillatorBot) + var stem = lastPathPart(absDir) + if stem.endsWith("_garage"): stem = stem[0 ..< stem.len - "_garage".len] + + let srcFile = absDir / "src" / stem & ".nim" + if not fileExists(srcFile): + # Fall back: pick the first .nim in src/ + let candidates = toSeq(walkFiles(absDir / "src" / "*.nim")) + if candidates.len == 0: + raise newException(IOError, fmt"No .nim source found in {absDir}/src/") + stem = splitFile(candidates[0]).name + + # ponytail: startProcess+streams for timeout; execCmdEx has no timeout param + let p = startProcess("nim", workingDir = absDir, + args = ["c", fmt"src/{stem}.nim"], + options = {poUsePath, poStdErrToStdOut}) + let exitCode = waitForExit(p, timeout = 30_000) + let output = p.outputStream.readAll() + p.close() + if exitCode != 0: + raise newException(OSError, + fmt"Compilation failed for {absDir}:{'\n'}{output}") + + result.add absDir / "out" / stem + +when isMainModule: + let botDir = currentSourcePath().parentDir().parentDir().parentDir() / + "OscillatorBot_garage" + echo "Compiling OscillatorBot from: ", botDir + try: + let bins = compileBots(@[botDir]) + echo "OK — binary: ", bins[0] + doAssert fileExists(bins[0]), "binary not found after compile" + except CatchableError as e: + echo "FAIL: ", e.msg + quit(1) diff --git a/common_libs/test_framework/runner_process.nim b/common_libs/test_framework/runner_process.nim new file mode 100644 index 0000000..84d785a --- /dev/null +++ b/common_libs/test_framework/runner_process.nim @@ -0,0 +1,61 @@ +## BattleRunner Java subprocess — capture stdout JSON-lines output. + +import std/[osproc, os, strutils, streams, times, net] + +const + DefaultRunnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar" + ScriptDir = currentSourcePath().parentDir() / ".." / ".." / "tools" / "battle_runner" + +proc runBattleRunner*(serverUrl: string, botDirs: seq[string], rounds: int, + timeout: int = 30000): string = + ## Starts TestBattleRunner as a subprocess. Returns captured stdout on success. + ## Raises IOError on non-zero exit or TimeoutError on timeout. + let jar = getEnv("TR_BATTLE_RUNNER", DefaultRunnerJar) + let classDir = getEnv("TR_BATTLE_RUNNER_DIR", ScriptDir) + + let args = @[ + "-cp", classDir & ":" & jar, + "TestBattleRunner", + "--server-url", serverUrl, + "--rounds", $rounds, + "--bot-dirs", botDirs.join(",") + ] + + let p = startProcess("java", args = args, + options = {poUsePath, poStdErrToStdOut}) + defer: + p.close() + + let deadline = epochTime() + timeout.float / 1000.0 + var stdout = "" + var line: string + while epochTime() < deadline: + if p.peekExitCode() != -1: + # Process finished; drain remaining output + while p.outputStream.readLine(line): + stdout.add(line & "\n") + break + if p.outputStream.readLine(line): + stdout.add(line & "\n") + + if epochTime() >= deadline and p.peekExitCode() == -1: + p.terminate() + raise newException(TimeoutError, + "BattleRunner timed out after " & $timeout & "ms") + + let code = p.waitForExit() + if code != 0: + raise newException(IOError, + "BattleRunner exited with code " & $code & "\nOutput:\n" & stdout) + + result = stdout + +when isMainModule: + echo "runner_process.nim — usage:" + echo " runBattleRunner(serverUrl, botDirs, rounds, timeout)" + echo "" + echo "Env vars:" + echo " TR_BATTLE_RUNNER JAR path (default: " & DefaultRunnerJar & ")" + echo " TR_BATTLE_RUNNER_DIR class dir (default: tools/battle_runner/)" + echo "" + echo "Cannot self-test without a running TR server. Start one first." diff --git a/common_libs/test_framework/server_manager.nim b/common_libs/test_framework/server_manager.nim new file mode 100644 index 0000000..31548d8 --- /dev/null +++ b/common_libs/test_framework/server_manager.nim @@ -0,0 +1,67 @@ +## TR server lifecycle manager — lazy singleton for integration tests. +## Start with ensureServer(), query with getServerUrl(). + +import std/[osproc, net, os, strutils, times, streams, exitprocs] + +const + DefaultJar = "/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar" + ReadySignal = "Starting server on port" + StartupTimeout = 15.0 # seconds + +var + serverProc: Process = nil + serverPort: int = 0 + +proc findFreePort(): int = + let s = newSocket() + s.bindAddr(Port(0)) + let (_, port) = s.getLocalAddr() + result = port.int + s.close() + +proc ensureServer*() = + if serverProc != nil: return + + let jar = getEnv("TR_SERVER_JAR", DefaultJar) + if not fileExists(jar): + raise newException(IOError, "TR server JAR not found: " & jar & + "\nSet TR_SERVER_JAR env var to override.") + + serverPort = findFreePort() + serverProc = startProcess("java", + args = @["-jar", jar, "--port", $serverPort, "--enable-initial-position"], + options = {poUsePath, poStdErrToStdOut}) + + exitprocs.addExitProc(proc() = + if serverProc != nil: + serverProc.terminate() + serverProc.close() + ) + + # Wait for ready signal + let deadline = epochTime() + StartupTimeout + var ready = false + while epochTime() < deadline: + var line: string + discard serverProc.outputStream.readLine(line) + if ReadySignal in line: + ready = true + break + if not ready: + serverProc.terminate() + serverProc.close() + serverProc = nil + raise newException(TimeoutError, "TR server did not start within " & $StartupTimeout & "s") + +proc getServerUrl*(): string = + if serverPort == 0: + raise newException(CatchableError, "Server not started — call ensureServer() first") + "ws://localhost:" & $serverPort + +when isMainModule: + echo "Starting TR server..." + ensureServer() + echo "Server ready at: ", getServerUrl() + echo "Sleeping 2s then exiting (addExitProc will kill server)..." + os.sleep(2000) + echo "Done." diff --git a/common_libs/test_framework/test_framework.nim b/common_libs/test_framework/test_framework.nim new file mode 100644 index 0000000..341a3a6 --- /dev/null +++ b/common_libs/test_framework/test_framework.nim @@ -0,0 +1,32 @@ +## Public API for the TR integration test framework. +## Import this module only; it re-exports all types you need. + +import ./server_manager +import ./bot_compiler +import ./battle_result +import ./runner_process + +export battle_result.BattleResult +export battle_result.BotResult +export battle_result.RoundResult +export battle_result.BotRoundResult + +proc runBattle*(botDirs: seq[string], rounds: int = 10, + timeout: int = 120000): BattleResult = + ensureServer() + discard compileBots(botDirs) + let stdout = runBattleRunner(getServerUrl(), botDirs, rounds, timeout) + result = parseServerOutput(stdout) + +when isMainModule: + echo "test_framework.nim — public API, not a standalone binary." + echo "Requires a TR server JAR. Usage from your test file:" + echo "" + echo " import common_libs/test_framework/test_framework" + echo " let r = runBattle(@[\"path/to/BotA\", \"path/to/BotB\"])" + echo " echo r.winners" + echo "" + echo "Env vars forwarded to sub-modules:" + echo " TR_SERVER_JAR TR server JAR path" + echo " TR_BATTLE_RUNNER BattleRunner JAR path" + echo " TR_BATTLE_RUNNER_DIR BattleRunner class dir" diff --git a/tools/battle_runner/TestBattleRunner.java b/tools/battle_runner/TestBattleRunner.java new file mode 100644 index 0000000..994bc48 --- /dev/null +++ b/tools/battle_runner/TestBattleRunner.java @@ -0,0 +1,85 @@ +import dev.robocode.tankroyale.runner.*; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Generic battle runner that connects to an external Tank Royale server and + * emits JSON-lines to stdout for downstream parsing. + * + * Usage: + * ./run_test_battle.sh --server-url ws://localhost:7654 --rounds 5 --bot-dirs /path/bot1,/path/bot2 + */ +public class TestBattleRunner { + + public static void main(String[] args) { + Logger.getLogger("dev.robocode.tankroyale").setLevel(Level.WARNING); + + String serverUrl = null; + int rounds = 1; + List botDirs = List.of(); + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--server-url" -> serverUrl = args[++i]; + case "--rounds" -> rounds = Integer.parseInt(args[++i]); + case "--bot-dirs" -> botDirs = Arrays.asList(args[++i].split(",")); + default -> { System.err.println("Unknown arg: " + args[i]); System.exit(1); } + } + } + + if (serverUrl == null) { System.err.println("--server-url required"); System.exit(1); } + if (botDirs.isEmpty()) { System.err.println("--bot-dirs required"); System.exit(1); } + + final String url = serverUrl; + final List bots = botDirs.stream().map(BotEntry::of).collect(Collectors.toList()); + final int numRounds = rounds; + final var setup = BattleSetup.classic(s -> s.setNumberOfRounds(numRounds)); + + try (var runner = BattleRunner.create(b -> b.externalServer(url).suppressServerOutput())) { + var owner = new Object(); + try (var handle = runner.startBattleAsync(setup, bots)) { + + handle.getOnGameStarted().on(owner, event -> { + var names = event.getParticipants().stream() + .map(p -> "\"" + escape(p.getName()) + "\"") + .collect(Collectors.joining(",")); + System.out.println("{\"event\":\"game_started\",\"bots\":[" + names + "]}"); + System.out.flush(); + }); + + handle.getOnRoundEnded().on(owner, event -> { + var results = event.getResults().stream().map(r -> + "{\"name\":\"" + escape(r.getName()) + "\"" + + ",\"score\":" + r.getTotalScore() + + ",\"rank\":" + r.getRank() + + ",\"survived\":" + (r.getSurvival() > 0) + "}" + ).collect(Collectors.joining(",")); + System.out.println("{\"event\":\"round_ended\",\"round\":" + event.getRoundNumber() + ",\"results\":[" + results + "]}"); + System.out.flush(); + }); + + var final_ = handle.awaitResults(); + var results = final_.getResults().stream().map(r -> + "{\"name\":\"" + escape(r.getName()) + "\"" + + ",\"totalScore\":" + r.getTotalScore() + + ",\"rank\":" + r.getRank() + + ",\"firstPlaces\":" + r.getFirstPlaces() + + ",\"survivalCount\":" + r.getSurvival() + "}" + ).collect(Collectors.joining(",")); + System.out.println("{\"event\":\"battle_ended\",\"results\":[" + results + "]}"); + System.out.flush(); + } + } catch (Exception e) { + System.err.println("Battle failed: " + e.getMessage()); + System.exit(1); + } + } + + // ponytail: minimal JSON escaping — only backslash and double-quote; extend if bot names use control chars + static String escape(String s) { + return s.replace("\\", "\\\\").replace("\"", "\\\""); + } +} diff --git a/tools/battle_runner/run_test_battle.sh b/tools/battle_runner/run_test_battle.sh new file mode 100755 index 0000000..9c0c868 --- /dev/null +++ b/tools/battle_runner/run_test_battle.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +JAR="${TANK_ROYALE_JAR:-/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar}" + +cd "$SCRIPT_DIR" + +if [ ! -f TestBattleRunner.class ] || [ TestBattleRunner.java -nt TestBattleRunner.class ]; then + echo ">>> Compiling TestBattleRunner.java..." >&2 + javac -cp "$JAR" TestBattleRunner.java +fi + +exec java -cp ".:$JAR" TestBattleRunner "$@"