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.
This commit is contained in:
2026-09-21 22:41:06 +02:00
parent 18f778056b
commit c091bf3c34
11 changed files with 283 additions and 8 deletions
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./out/RulesProbe 2>> /tmp/rulesprobe_stderr.log
@@ -0,0 +1 @@
switch("outdir", "out")
@@ -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"
}
@@ -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)