Files
SirRoboGarage/common_libs/test_framework/adversaries/RandomMover/src/RandomMover.nim
T
SirStone d3b3c28cdf fix(adversaries): migrate to bot-api 1.0.7 - kills an intermittent crash that corrupted measurements
Four of the five adversaries imported the OLD package (tankroyale_botapi
1.0.1); only SittingDuck used robocode_tankroyale_botapi 1.0.7, which is what
the rest of the repo requires. A previous report claimed OscillatorBot was
already on 1.0.7 - that was WRONG, and OscillatorBot turned out to crash the
MOST (8 SIGSEGVs in the first reproduction, 15 in its historical /tmp logs).

THE CRASH, reproduced with an identical stack in every case:
  botThreadEntry -> run -> adversary run -> go -> dispatchPendingEvents ->
  tankroyale_botapi-1.0.1/event_queue.nim(89) addEvent -> realloc/rawDealloc ->
  SIGSEGV
Counts, old API: 60 melee battles x 8 rounds gave RandomMover 1, PatternMover 3,
WaveSurfer 0, OscillatorBot 8; 6 battles x 6 rounds vs SittingDuck gave 4/2/0/3.

ROOT CAUSE: the main->bot event hand-off. 1.0.1 passes a lock-protected
seq[BotEvent] (signalTick writes gPendingEvents, dispatchPendingEvents copies it
under lock). 1.0.7 uses a Channel[seq[BotEvent]] (send(move(pending)) /
tryRecv). The old path copied string-bearing BotEvent payloads across threads
every tick, churning ORC refcounts on the shared heap until the freelist was
corrupted. 1.0.7's own source documents this as the gdb-confirmed fix.

WHY IT MATTERED MORE THAN IT LOOKED: the crash silently corrupted measurements.
Against a stationary duck, crash contamination inflated WaveSurfer's rest
fraction from 12.4% (clean) to 20.7%; in a focused run the server logged
'Bot left: OscillatorBot' while the game continued and its score stopped
growing. So every gauntlet run tonight was fighting adversaries that were
partially dead - which is a second, independent reason the user's instinct that
these bots were bugged was correct, and why they should not be used as a
measurement baseline. (The per-gun REAL hit rates are unaffected: those came
from DrussGT battles.)

FIX: all four migrated to robocode_tankroyale_botapi 1.0.7. NO API adaptations
were needed beyond the module rename - every symbol these bots use is identical
in 1.0.7, verified by diffing the two packages (constants/utils/json_parse/
schemas semantically identical; the movement and intent procs in bot.nim are
byte-identical). The .nimble files now require robocode_tankroyale_botapi.

VERIFIED: 120 melee battles x 8 rounds plus 6x6 vs SittingDuck -> 0 SIGSEGV in
all four stderr logs (0 bytes). Behaviour unchanged: sub-1% absolute drift in
mean speed, rest fraction, reversal rate, mean range and perpendicular fraction,
all within run-to-run spread; the one >=3-sigma flag (WaveSurfer perpendicular
relative to DrussGT) was isolated against a stationary opponent and shown to be
the chaotic closed loop, not the migration. test_wavesurfer_velocity passes 7/7.

NOT migrated, reported only: GotoTest_garage, OscillatorBot_garage (archived
copy), PPO_Bot_garage, QBot_garage, SAC_LSTM_Bot_garage - older experiment
garages, left alone deliberately.
2026-09-21 09:14:47 +02:00

51 lines
1.6 KiB
Nim

# RandomMover — random speed/turn with persistence, head-on firing.
# Purpose: test guns against unpredictable movement.
import std/[math, os, random]
import robocode_tankroyale_botapi
const botJsonPath = currentSourcePath().parentDir / "RandomMover.json"
type RandomMover = ref object of Bot
currentSpeed: float
currentTurn: float
ticksUntilChange: int
proc newDirection(bot: RandomMover) =
bot.currentSpeed = rand(-8.0..8.0)
# The server caps body turn at calcMaxTurnRate(speed) = 10 - 0.75*|speed|
# (~4 deg/tick at full speed); keep the random intent inside that limit so it
# is not silently saturated by the server.
let maxTurn = calcMaxTurnRate(bot.currentSpeed)
bot.currentTurn = rand(-maxTurn..maxTurn)
bot.ticksUntilChange = rand(10..20)
method onRoundStarted*(bot: RandomMover, e: RoundStartedEvent) =
randomize()
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
newDirection(bot)
method onScannedBot*(bot: RandomMover, e: ScannedBotEvent) =
let bearing = directionTo(getX(), getY(), e.x, e.y)
let gunDelta = normalizeRelativeAngle(bearing - getGunDirection())
setGunTurnRate(gunDelta.clamp(-MAX_GUN_TURN_RATE, MAX_GUN_TURN_RATE))
if abs(gunDelta) < 3.0 and getGunHeat() <= 0.0:
discard setFire(1.0)
method run*(bot: RandomMover) =
while isRunning():
dec bot.ticksUntilChange
if bot.ticksUntilChange <= 0:
newDirection(bot)
setTargetSpeed(bot.currentSpeed)
setTurnRate(bot.currentTurn)
setRadarTurnRate(45.0)
go()
when isMainModule:
var bot = RandomMover()
start(bot, botJsonPath)