d3b3c28cdf
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.
112 lines
3.6 KiB
Nim
112 lines
3.6 KiB
Nim
# PatternMover — repeats a fixed movement sequence, head-on firing.
|
|
# Purpose: test pattern-matching gun.
|
|
|
|
import std/[math, os]
|
|
import robocode_tankroyale_botapi
|
|
|
|
const botJsonPath = currentSourcePath().parentDir / "PatternMover.json"
|
|
|
|
# Sequence: (distance, turn_deg) pairs
|
|
type Step = tuple[dist: float, turn: float]
|
|
const STEPS: array[6, Step] = [
|
|
(100.0, 0.0),
|
|
( 0.0, 45.0),
|
|
( 50.0, 0.0),
|
|
( 0.0, -90.0),
|
|
( 75.0, 0.0),
|
|
( 0.0, 30.0),
|
|
]
|
|
|
|
const
|
|
MOVE_SPEED = 8.0
|
|
STALL_LIMIT = 20 # ticks without progress before the step is abandoned
|
|
|
|
type PatternMover = ref object of Bot
|
|
stepIdx: int
|
|
remaining: float # distance left in current move step, or turn degrees
|
|
inTurn: bool # true while executing a turn sub-step
|
|
lastX: float
|
|
lastY: float
|
|
lastDir: float
|
|
haveLast: bool
|
|
stallTicks: int
|
|
|
|
method onRoundStarted*(bot: PatternMover, e: RoundStartedEvent) =
|
|
setAdjustGunForBodyTurn(true)
|
|
setAdjustRadarForBodyTurn(true)
|
|
setAdjustRadarForGunTurn(true)
|
|
bot.stepIdx = 0
|
|
bot.remaining = STEPS[0].dist
|
|
bot.inTurn = false
|
|
bot.haveLast = false
|
|
bot.stallTicks = 0
|
|
|
|
method onScannedBot*(bot: PatternMover, 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) < 10.0 and getGunHeat() <= 0.0:
|
|
discard setFire(1.0)
|
|
|
|
proc advanceStep(bot: PatternMover) =
|
|
bot.stepIdx = (bot.stepIdx + 1) mod STEPS.len
|
|
let s = STEPS[bot.stepIdx]
|
|
if s.dist > 0.0:
|
|
bot.inTurn = false
|
|
bot.remaining = s.dist
|
|
else:
|
|
bot.inTurn = true
|
|
bot.remaining = abs(s.turn)
|
|
bot.stallTicks = 0
|
|
|
|
method run*(bot: PatternMover) =
|
|
while isRunning():
|
|
setRadarTurnRate(45.0)
|
|
|
|
# Progress is measured from real movement / real turning, never from the
|
|
# requested intent, so a blocked bot cannot run its step counter down.
|
|
var moved = 0.0
|
|
var turned = 0.0
|
|
if bot.haveLast:
|
|
moved = distanceTo(bot.lastX, bot.lastY, getX(), getY())
|
|
turned = abs(normalizeRelativeAngle(getDirection() - bot.lastDir))
|
|
|
|
if bot.inTurn:
|
|
let s = STEPS[bot.stepIdx]
|
|
let sign = if s.turn >= 0: 1.0 else: -1.0
|
|
let rate = min(bot.remaining, MAX_TURN_RATE)
|
|
setTurnRate(sign * rate)
|
|
setTargetSpeed(0.0)
|
|
bot.remaining -= turned
|
|
if turned < 0.05: inc bot.stallTicks else: bot.stallTicks = 0
|
|
else:
|
|
# If the forward move is actually blocked (e.g. against a wall), steer
|
|
# towards the arena centre until it can move again, so the pattern makes
|
|
# real progress instead of deadlocking against the wall.
|
|
var headingCorrection = 0.0
|
|
if bot.stallTicks >= 2:
|
|
let arenaW = getArenaWidth().float
|
|
let arenaH = getArenaHeight().float
|
|
let toCenter = directionTo(getX(), getY(), arenaW * 0.5, arenaH * 0.5)
|
|
headingCorrection = normalizeRelativeAngle(toCenter - getDirection())
|
|
setTurnRate(headingCorrection.clamp(-MAX_TURN_RATE, MAX_TURN_RATE))
|
|
setTargetSpeed(MOVE_SPEED)
|
|
bot.remaining -= moved
|
|
if moved < 0.05: inc bot.stallTicks else: bot.stallTicks = 0
|
|
|
|
bot.lastX = getX()
|
|
bot.lastY = getY()
|
|
bot.lastDir = getDirection()
|
|
bot.haveLast = true
|
|
|
|
# Advance on completion OR after a bounded stall (e.g. pinned against a
|
|
# wall), so the repeating pattern can never deadlock.
|
|
if bot.remaining <= 0.5 or bot.stallTicks > STALL_LIMIT:
|
|
advanceStep(bot)
|
|
|
|
go()
|
|
|
|
when isMainModule:
|
|
var bot = PatternMover()
|
|
start(bot, botJsonPath)
|