diff --git a/ModularBot_garage/tests/test_gauntlet_5bots.nim b/ModularBot_garage/tests/test_gauntlet_5bots.nim index d6ec964..57d355f 100644 --- a/ModularBot_garage/tests/test_gauntlet_5bots.nim +++ b/ModularBot_garage/tests/test_gauntlet_5bots.nim @@ -11,8 +11,14 @@ import test_framework/battle_result const modularBotDir = currentSourcePath().parentDir.parentDir adversariesDir = "/home/davide/Projects/SirRoboGarage/common_libs/test_framework/adversaries" - serverJar = "/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar" - runnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar" + defaultRunnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar" + +# Respect an EXISTING TR_SERVER_JAR / TR_BATTLE_RUNNER so either server version +# (1.3.1 default, or the 0.35.5 legacy via server_manager.LegacyServerJar) can be +# selected from the environment. Only fill in the default when the user left the +# variable unset. This file must not clobber a caller's choice. +let serverJar = getEnv("TR_SERVER_JAR", DefaultServerJar) +let runnerJar = getEnv("TR_BATTLE_RUNNER", defaultRunnerJar) if not fileExists(serverJar): echo "SKIP: server JAR not found at " & serverJar diff --git a/common_libs/test_framework/README.md b/common_libs/test_framework/README.md index f241977..fb2434b 100644 --- a/common_libs/test_framework/README.md +++ b/common_libs/test_framework/README.md @@ -14,12 +14,30 @@ Integration test framework for Tank Royale bots. Spins up a TR server JAR on a r | 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_SERVER_JAR` | Path to the TR server JAR | `DefaultServerJar` in `server_manager.nim` (current: server 1.3.1) | +| `TR_BATTLE_RUNNER` | Path to the TR runner JAR | hardcoded dev path in `runner_process.nim` (1.0.2) | | `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. +### Switching server versions + +The framework defaults to the current server (`DefaultServerJar`, 1.3.1). The +previous 0.35.5 build is kept at `LegacyServerJar` for reproducing older +baselines. Either can be selected from the environment without editing code: + +```sh +# current (default) +nim c -r tests/test_gauntlet_5bots.nim + +# legacy 0.35.5, for old-baseline reproduction +export TR_SERVER_JAR=/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar +nim c -r tests/test_gauntlet_5bots.nim +``` + +Test drivers must not overwrite `TR_SERVER_JAR` when the caller already set it; +`test_gauntlet_5bots.nim` now only fills in the default when the variable is unset. + ### One-time Java compilation ```sh diff --git a/common_libs/test_framework/server_manager.nim b/common_libs/test_framework/server_manager.nim index 3504bc7..95a3d97 100644 --- a/common_libs/test_framework/server_manager.nim +++ b/common_libs/test_framework/server_manager.nim @@ -4,7 +4,12 @@ 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" + DefaultServerJar* = "/home/davide/Downloads/robocode-tankroyale/robocode-tankroyale-server.jar" + ## Current server (Implementation-Version 1.3.1). + LegacyServerJar* = "/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar" + ## Previous server (0.35.5). Every pre-1.3.1 measurement was taken on it; + ## kept so old baselines can be reproduced. Select it with + ## `TR_SERVER_JAR=$LegacyServerJar`, no code edit needed. ReadySignal = "Starting server on port" StartupTimeout = 15.0 # seconds @@ -22,7 +27,7 @@ proc findFreePort(): int = proc ensureServer*() = if serverProc != nil: return - let jar = getEnv("TR_SERVER_JAR", DefaultJar) + let jar = getEnv("TR_SERVER_JAR", DefaultServerJar) if not fileExists(jar): raise newException(IOError, "TR server JAR not found: " & jar & "\nSet TR_SERVER_JAR env var to override.") diff --git a/common_libs/tests/acceptance_offline_vs_online.nim b/common_libs/tests/acceptance_offline_vs_online.nim index c3a405b..008e567 100644 --- a/common_libs/tests/acceptance_offline_vs_online.nim +++ b/common_libs/tests/acceptance_offline_vs_online.nim @@ -21,6 +21,7 @@ import std/[json, os, strformat, strutils, math] import test_framework/test_framework +import test_framework/server_manager import gun_harness/offline_range import range_guns @@ -30,8 +31,13 @@ const adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "OscillatorBot" statsPath = "/tmp/gun_stats.jsonl" recordPath = "/tmp/worldstate_record.jsonl" - serverJar = "/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar" - runnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar" + defaultRunnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar" + +# Honor the caller's TR_SERVER_JAR / TR_BATTLE_RUNNER; otherwise use the +# current defaults (server 1.3.1). The runner uses the same default as +# runner_process.nim so an unset env var is not silently treated as "missing". +let serverJar = getEnv("TR_SERVER_JAR", DefaultServerJar) +let runnerJar = getEnv("TR_BATTLE_RUNNER", defaultRunnerJar) const TsetlinId = 2 const TmSelectorId = 13 ## also stochastic (rand() in Gate choose + TM feedback) diff --git a/common_libs/tests/measure_bridge_battle.nim b/common_libs/tests/measure_bridge_battle.nim new file mode 100644 index 0000000..0cc5117 --- /dev/null +++ b/common_libs/tests/measure_bridge_battle.nim @@ -0,0 +1,41 @@ +## Re-baseline the DrussGT bridge battle on the selected server. +## +## ModularBot (Nim) vs the real DrussGT running through tools/robocode_shim. +## DrussGT is a prebuilt Java bot dir (only .json/.sh), so the framework's +## compileBots() cannot be used for it; we drive the runner directly. +## +## BRIDGE_ROUNDS=5 TR_SERVER_JAR= \ +## nim c -r --path:common_libs common_libs/tests/measure_bridge_battle.nim + +import std/[os, strformat, strutils] +import test_framework/server_manager +import test_framework/bot_compiler +import test_framework/runner_process +import test_framework/battle_result + +const + repoRoot = currentSourcePath().parentDir.parentDir.parentDir + modularBotDir = repoRoot / "ModularBot_garage" + +let drussDir = getEnv("DRUSSGT_BOTDIR", "/tmp/tr_bots/DrussGT") +let rounds = parseInt(getEnv("BRIDGE_ROUNDS", "5")) + +if not fileExists(drussDir / "DrussGT.sh"): + echo "SKIP: DrussGT bridge bot dir not found at ", drussDir + quit(0) + +ensureServer() +discard compileBots(@[modularBotDir]) +echo "server jar : ", getEnv("TR_SERVER_JAR", DefaultServerJar) +echo "drussgt dir: ", drussDir +let raw = runBattleRunner(getServerUrl(), @[modularBotDir, drussDir], rounds, 900_000, true) +let r = parseServerOutput(raw) +for b in r.results: + echo fmt" {b.name:<12} totalScore={b.totalScore} rank={b.rank} firstPlaces={b.firstPlaces} survival={b.survivalCount}" +echo "rounds played: ", r.rounds.len +for rr in r.rounds: + var line = " round " & $rr.round & ":" + for b in rr.results: + let st = if b.survived: "surv" else: "dead" + line &= fmt" {b.name}={b.score}({st})" + echo line diff --git a/common_libs/tests/measure_physics.nim b/common_libs/tests/measure_physics.nim new file mode 100644 index 0000000..9d6ed23 --- /dev/null +++ b/common_libs/tests/measure_physics.nim @@ -0,0 +1,32 @@ +## Empirical physics re-baseline driver: run the RulesProbe bot against a +## stationary SittingDuck on whichever server TR_SERVER_JAR selects, and dump +## the raw event log to PROBE_LOG. +## +## PROBE_LOG=/tmp/probe_131.jsonl TR_SERVER_JAR= \ +## nim c -r --path:common_libs common_libs/tests/measure_physics.nim +## +## Compare the two logs with common_libs/tests/analyze_probe_log.nim. + +import std/[os, strformat, strutils] +import test_framework/test_framework + +const + repoRoot = currentSourcePath().parentDir.parentDir.parentDir + probeDir = currentSourcePath().parentDir / "probe_bots" / "RulesProbe" + duckDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck" + +let outPath = getEnv("PROBE_LOG", "/tmp/probe_log.jsonl") +let rounds = parseInt(getEnv("PROBE_ROUNDS", "6")) +let powers = getEnv("PROBE_POWERS", "0.1,0.5,1,2,3") + +if fileExists(outPath): removeFile(outPath) +putEnv("PROBE_LOG", outPath) +putEnv("PROBE_POWERS", powers) + +echo "server jar : ", getEnv("TR_SERVER_JAR", "(default)") +echo "probe log : ", outPath +echo "rounds : ", rounds +let r = runBattle(@[probeDir, duckDir], rounds = rounds, timeout = 300000, maxSpeed = true) +for b in r.results: + echo fmt" {b.name}: totalScore={b.totalScore} rank={b.rank} survival={b.survivalCount}" +echo "rounds played: ", r.rounds.len diff --git a/common_libs/tests/probe_bots/RulesProbe/RulesProbe.json b/common_libs/tests/probe_bots/RulesProbe/RulesProbe.json new file mode 100644 index 0000000..c622286 --- /dev/null +++ b/common_libs/tests/probe_bots/RulesProbe/RulesProbe.json @@ -0,0 +1,9 @@ +{ + "name": "RulesProbe", + "version": "0.1.0", + "authors": ["Test"], + "description": "Physics probe: logs bullet damage/speed/gun heat/score breakdown", + "gameTypes": ["classic", "1v1"], + "platform": "Nim", + "programmingLang": "Nim" +} diff --git a/common_libs/tests/probe_bots/RulesProbe/RulesProbe.sh b/common_libs/tests/probe_bots/RulesProbe/RulesProbe.sh new file mode 100755 index 0000000..c9f2154 --- /dev/null +++ b/common_libs/tests/probe_bots/RulesProbe/RulesProbe.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd "$(dirname "$0")" +exec ./out/RulesProbe 2>> /tmp/rulesprobe_stderr.log diff --git a/common_libs/tests/probe_bots/RulesProbe/config.nims b/common_libs/tests/probe_bots/RulesProbe/config.nims new file mode 100644 index 0000000..3dee76c --- /dev/null +++ b/common_libs/tests/probe_bots/RulesProbe/config.nims @@ -0,0 +1 @@ +switch("outdir", "out") diff --git a/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.json b/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.json new file mode 100644 index 0000000..c622286 --- /dev/null +++ b/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.json @@ -0,0 +1,9 @@ +{ + "name": "RulesProbe", + "version": "0.1.0", + "authors": ["Test"], + "description": "Physics probe: logs bullet damage/speed/gun heat/score breakdown", + "gameTypes": ["classic", "1v1"], + "platform": "Nim", + "programmingLang": "Nim" +} diff --git a/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.nim b/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.nim new file mode 100644 index 0000000..e9913eb --- /dev/null +++ b/common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.nim @@ -0,0 +1,145 @@ +## RulesProbe — measures Tank Royale server physics empirically. +## +## Stationary bot that aims at a stationary adversary and cycles through a list +## of firepowers, logging everything needed to read bullet damage, bullet speed, +## gun heat / cooling rate and the round-result score breakdown straight off the +## server's events. Used to compare server 0.35.5 against 1.3.1. +## +## Env: +## PROBE_LOG output JSONL path (default /tmp/probe_log.jsonl) +## PROBE_POWERS comma-separated firepowers to cycle (default 0.1,0.5,1,2,3) + +import std/[os, strutils, json, math, tables] +import robocode_tankroyale_botapi + +const botJsonPath = currentSourcePath().parentDir / "RulesProbe.json" + +let logPath = getEnv("PROBE_LOG", "/tmp/probe_log.jsonl") +let powerList = + block: + var s: seq[float] + for tok in getEnv("PROBE_POWERS", "0.1,0.5,1,2,3").split(','): + let t = tok.strip() + if t.len > 0: + try: s.add parseFloat(t) + except ValueError: discard + if s.len == 0: s = @[1.0] + s + +var gLog: File +var gReady = false + +proc logRow(row: JsonNode) = + if not gReady: + try: + gLog = open(logPath, fmAppend) + gReady = true + except CatchableError: + return + try: + gLog.writeLine($row) + gLog.flushFile() + except CatchableError: + discard + +type + RulesProbe = ref object of Bot + powerIdx: int + targetX, targetY: float + hasTarget: bool + fireTick: Table[int, int] + fireX, fireY: Table[int, float] + fireDir: Table[int, float] + firePower: Table[int, float] + scanX, scanY: Table[int, float] + +method onScannedBot*(bot: RulesProbe, e: ScannedBotEvent) = + bot.targetX = e.x + bot.targetY = e.y + bot.hasTarget = true + bot.scanX[e.scannedBotId] = e.x + bot.scanY[e.scannedBotId] = e.y + logRow(%*{"ev": "scan", "t": e.turnNumber, "id": e.scannedBotId, + "x": e.x, "y": e.y, "e": e.energy}) + +method onBulletFired*(bot: RulesProbe, e: BulletFiredEvent) = + let b = e.bullet + bot.fireTick[b.bulletId] = e.turnNumber + bot.fireX[b.bulletId] = b.x + bot.fireY[b.bulletId] = b.y + bot.fireDir[b.bulletId] = b.direction + bot.firePower[b.bulletId] = b.power + logRow(%*{"ev": "fire", "t": e.turnNumber, "bid": b.bulletId, "p": b.power, + "x": b.x, "y": b.y, "dir": b.direction}) + +method onBulletHit*(bot: RulesProbe, e: BulletHitBotEvent) = + let b = e.bullet + let dist = if b.bulletId in bot.fireX: + hypot(bot.scanX.getOrDefault(e.victimId, 0.0) - bot.fireX[b.bulletId], + bot.scanY.getOrDefault(e.victimId, 0.0) - bot.fireY[b.bulletId]) + else: -1.0 + logRow(%*{"ev": "hitbot", "t": e.turnNumber, "bid": b.bulletId, "p": b.power, + "dmg": e.damage, "e": e.energy, "victim": e.victimId, + "ft": bot.fireTick.getOrDefault(b.bulletId, -1), "dist": dist}) + +method onBulletHitWall*(bot: RulesProbe, e: BulletHitWallEvent) = + let b = e.bullet + logRow(%*{"ev": "hitwall", "t": e.turnNumber, "bid": b.bulletId, "p": b.power, + "x": b.x, "y": b.y, "dir": b.direction, + "ft": bot.fireTick.getOrDefault(b.bulletId, -1), + "fx": bot.fireX.getOrDefault(b.bulletId, 0.0), + "fy": bot.fireY.getOrDefault(b.bulletId, 0.0), + "fdir": bot.fireDir.getOrDefault(b.bulletId, 0.0)}) + +method onHitByBullet*(bot: RulesProbe, e: HitByBulletEvent) = + logRow(%*{"ev": "hitby", "t": e.turnNumber, "p": e.bullet.power, + "dmg": e.damage, "e": e.energy}) + +method onRoundEnded*(bot: RulesProbe, e: RoundEndedEventForBot) = + let r = e.results + logRow(%*{"ev": "roundend", "t": e.turnNumber, "round": e.roundNumber, + "rank": r.rank, "survival": r.survival, + "lastSurvivorBonus": r.lastSurvivorBonus, + "bulletDamage": r.bulletDamage, "bulletKillBonus": r.bulletKillBonus, + "ramDamage": r.ramDamage, "ramKillBonus": r.ramKillBonus, + "totalScore": r.totalScore, "firstPlaces": r.firstPlaces, + "secondPlaces": r.secondPlaces, "thirdPlaces": r.thirdPlaces}) + +method onGameEnded*(bot: RulesProbe, e: GameEndedEventForBot) = + let r = e.results + logRow(%*{"ev": "gameend", "rounds": e.numberOfRounds, + "rank": r.rank, "survival": r.survival, + "lastSurvivorBonus": r.lastSurvivorBonus, + "bulletDamage": r.bulletDamage, "bulletKillBonus": r.bulletKillBonus, + "ramDamage": r.ramDamage, "ramKillBonus": r.ramKillBonus, + "totalScore": r.totalScore}) + +method run*(bot: RulesProbe) = + setAdjustGunForBodyTurn(true) + setAdjustRadarForBodyTurn(true) + setAdjustRadarForGunTurn(true) + setTargetSpeed(0.0) + setTurnRate(0.0) + logRow(%*{"ev": "start", "turn": getTurn(), "arenaW": getArenaWidth(), + "arenaH": getArenaHeight(), "cooling": getGunCoolingRate(), + "powers": powerList}) + while isRunning(): + let t = getTurn() + logRow(%*{"ev": "tick", "t": t, "heat": getGunHeat(), "e": getEnergy(), + "x": getX(), "y": getY(), "gd": getGunDirection(), + "rd": getRadarDirection()}) + setRadarTurnRate(45.0) + if bot.hasTarget: + let bearing = directionTo(getX(), getY(), bot.targetX, bot.targetY) + let delta = calcDeltaAngle(bearing, getGunDirection()) + setGunTurnRate(delta) + let p = powerList[bot.powerIdx mod powerList.len] + if abs(delta) < 1.0 and getGunHeat() <= 0.0 and getEnergy() > p: + if setFire(p): + logRow(%*{"ev": "firecmd", "t": t, "p": p, "heatBefore": getGunHeat()}) + inc bot.powerIdx + go() + +when isMainModule: + var bot = RulesProbe() + start(bot, botJsonPath)