From c091bf3c34dbe592061622129b4a2e22375a0d2e Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 21 Sep 2026 22:41:06 +0200 Subject: [PATCH] harness: upgrade to server 1.3.1, keep 0.35.5 selectable, re-baseline All prior measurements ran on server 0.35.5. The default is now the current 1.3.1 jar, with the legacy jar kept and switchable via TR_SERVER_JAR (no code edit). test_gauntlet_5bots.nim no longer clobbers a caller's TR_SERVER_JAR - it used to putEnv() unconditionally, so an override was silently ignored. RE-BASELINE (controlled RulesProbe battle, stationary bot, powers 0.1/0.5/1/2/3): dimension 1.3.1 0.35.5 verdict bullet damage per hit 0.4/2/4/10/16 identical SAME bullet speed (20-3p) within noise within noise SAME post-fire gun heat (1+p/5) identical identical SAME cooling 0.1/tick 0.1/tick SAME bulletDamage SCORE exactly 100/round 104..113/round DIFFERENT bulletKillBonus (20%) 20/round 20..23/round DIFFERENT LOUD FINDING - a SCORING rule changed, physics did not: 0.35.5 credits OVERKILL to bulletDamage (the killing bullet's full damage even past 0 energy); 1.3.1 caps it at the energy actually removed. Every 0.35.5 score is therefore inflated ~5-6%, and bulletKillBonus inherits the inflation. Gauntlet totals shift accordingly (SittingDuck 1936 -> 1800, WaveSurfer 1886 -> 1669). Consequence: score-based numbers recorded on 0.35.5 are NOT comparable to 1.3.1. Our gun A/Bs used real HIT RATE, not score, so those conclusions stand. Runner 1.0.2 (unchanged, no newer one on the box) is measured compatible with the 1.3.1 server. Note TrBattleCapture uses the runner's EMBEDDED server, which is 1.0.2 - so the capture path still runs an older engine than the gauntlet. Also re-ran acceptance_offline_vs_online on the new default: 12/12. --- .../tests/test_gauntlet_5bots.nim | 10 +- common_libs/test_framework/README.md | 22 ++- common_libs/test_framework/server_manager.nim | 9 +- .../tests/acceptance_offline_vs_online.nim | 10 +- common_libs/tests/measure_bridge_battle.nim | 41 +++++ common_libs/tests/measure_physics.nim | 32 ++++ .../probe_bots/RulesProbe/RulesProbe.json | 9 ++ .../tests/probe_bots/RulesProbe/RulesProbe.sh | 3 + .../tests/probe_bots/RulesProbe/config.nims | 1 + .../probe_bots/RulesProbe/src/RulesProbe.json | 9 ++ .../probe_bots/RulesProbe/src/RulesProbe.nim | 145 ++++++++++++++++++ 11 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 common_libs/tests/measure_bridge_battle.nim create mode 100644 common_libs/tests/measure_physics.nim create mode 100644 common_libs/tests/probe_bots/RulesProbe/RulesProbe.json create mode 100755 common_libs/tests/probe_bots/RulesProbe/RulesProbe.sh create mode 100644 common_libs/tests/probe_bots/RulesProbe/config.nims create mode 100644 common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.json create mode 100644 common_libs/tests/probe_bots/RulesProbe/src/RulesProbe.nim 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)