Files
SirRoboGarage/common_libs/tests/acceptance_offline_vs_online.nim
T
SirStone c091bf3c34 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.
2026-09-21 22:41:06 +02:00

133 lines
5.1 KiB
Nim

## Task 3: acceptance test — prove the offline range reproduces the live
## virtual-bullet metric, or the range is worthless.
##
## Steps:
## 1. run ONE live ModularBot vs OscillatorBot round with the ModularBot
## recorder enabled for this battle only (the test exports
## TR_RECORD_WORLDSTATE=1, which the bot reads at RUNTIME; ordinary
## builds leave it unset and write no fixture),
## 2. read the online per-gun virtual fitness from /tmp/gun_stats.jsonl,
## 3. replay the recorded WorldState fixture offline through the same guns,
## 4. compare.
##
## Tsetlin is stochastic (tmLearnOne calls rand(); its constructor calls
## randomize()), so byte-identical replay is impossible for it. The 12
## deterministic guns must match EXACTLY; Tsetlin is reported separately.
##
## Run with:
## nim c -r common_libs/tests/acceptance_offline_vs_online.nim
##
## Requires TR_SERVER_JAR / TR_BATTLE_RUNNER (or the default dev paths below).
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
const
repoRoot = currentSourcePath().parentDir.parentDir.parentDir
modularBotDir = repoRoot / "ModularBot_garage"
adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "OscillatorBot"
statsPath = "/tmp/gun_stats.jsonl"
recordPath = "/tmp/worldstate_record.jsonl"
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)
proc isStochastic(id: int): bool = id == TsetlinId or id == TmSelectorId
proc lastOnlineRound(path: string): JsonNode =
result = nil
for line in lines(path):
let s = line.strip()
if s.len == 0: continue
let node = parseJson(s)
if node.hasKey("guns"): result = node
proc main() =
if not fileExists(serverJar) or not fileExists(runnerJar):
echo "Skipping: TR JARs not found (server=", serverJar, ", runner=", runnerJar, ")"
quit(0)
if not fileExists(modularBotDir / "src" / "ModularBot.nim"):
echo "Skipping: ModularBot source not found"
quit(0)
for p in [statsPath, recordPath]:
if fileExists(p): removeFile(p)
# Enable the ModularBot's runtime world-state recorder for THIS battle only.
# The env var is inherited by the battle-runner process and then by the bot
# processes it spawns, so a single invocation of this test is self-contained.
putEnv("TR_RECORD_WORLDSTATE", "1")
defer: delEnv("TR_RECORD_WORLDSTATE")
echo "=== live battle: ModularBot vs OscillatorBot, 1 round, max speed ==="
let battle = runBattle(@[modularBotDir, adversaryDir], rounds = 1,
timeout = 240000, maxSpeed = true)
for res in battle.results:
echo fmt" {res.name:<14} rank={res.rank} score={res.totalScore}"
if not fileExists(recordPath):
echo "FAIL: recorder produced no fixture (TR_RECORD_WORLDSTATE not inherited?)"
quit(1)
if not fileExists(statsPath):
echo "FAIL: no /tmp/gun_stats.jsonl"
quit(1)
let online = lastOnlineRound(statsPath)
let fx = loadFixture(recordPath)
let reports = replayFixture(fx, buildAllGunDrivers(enableTmSelector = false), liveActual = true)
# Map online stats by gun id.
var onShots: array[14, int]
var onHits: array[14, int]
var onNames: array[14, string]
for g in online["guns"]:
let id = g["id"].getInt()
if id >= 0 and id < 14:
onShots[id] = g["vShots"].getInt()
onHits[id] = g["vHits"].getInt()
onNames[id] = g["name"].getStr()
echo ""
echo fmt"fixture: {recordPath} ticks={fx.states.len} enemyId={fx.enemyId} enemyDied={fx.enemyDied}"
echo "online stats: /tmp/gun_stats.jsonl round ", online["round"].getInt()
echo ""
echo "gun online(vHits/vShots) offline(hits/shots) verdict"
echo "-----------------------------------------------------------------------"
var matches = 0
var deterministic = 0
for id in 0..<14:
let r = reports[id]
let match = r.hits == onHits[id] and r.shots == onShots[id]
var verdict: string
if isStochastic(id):
verdict = if match: "MATCH (stochastic)" else: "differs (stochastic, expected)"
else:
inc deterministic
if match:
inc matches
verdict = "OK"
else:
verdict = "MISMATCH"
echo fmt"{r.name:<12} {onHits[id]:>5}/{onShots[id]:<5} {r.hits:>5}/{r.shots:<5} {verdict}"
echo ""
echo fmt"deterministic guns matching exactly: {matches}/{deterministic}"
if matches != deterministic:
echo "VERDICT: FAIL — offline range does NOT reproduce the live metric."
quit(1)
echo "VERDICT: PASS — offline == online for all 12 deterministic guns."
echo "(Tsetlin and TMSelect are stochastic and are allowed to differ.)"
when isMainModule:
main()