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.
148 lines
5.5 KiB
Nim
148 lines
5.5 KiB
Nim
# WaveSurfer — reactive movement adversary with linear prediction gun.
|
|
# Monitors enemy energy drops (bullet fire detection), reverses on detected fire.
|
|
# Strafes perpendicular to enemy, reverses near walls.
|
|
# Purpose: hardest test enemy — tests TM gun's ability to learn reactive movement.
|
|
|
|
import std/[math, os]
|
|
import robocode_tankroyale_botapi
|
|
|
|
const botJsonPath = currentSourcePath().parentDir / "WaveSurfer.json"
|
|
|
|
const
|
|
WALL_MARGIN = 60.0
|
|
STRAFE_SPEED = 8.0
|
|
BULLET_SPEED = 20.0 - 3.0 * 2.0 # for fire power 2.0
|
|
WALL_HYST = 0.05 # dot-product hysteresis for wall escape
|
|
|
|
type WaveSurfer = ref object of Bot
|
|
strafeDir: float # +1 or -1
|
|
lastEnemyEnergy: float
|
|
enemyX, enemyY: float
|
|
enemyVx, enemyVy: float
|
|
enemyDir: float
|
|
enemySpeed: float
|
|
stuckTicks: int # consecutive ticks stopped while touching a wall
|
|
|
|
proc enemyVelocity*(direction, speed: float): tuple[vx, vy: float] =
|
|
## World velocity of a bot from its (heading, signed speed).
|
|
## Tank Royale convention: 0 degrees is EAST and angles increase
|
|
## counter-clockwise, so vx = cos(heading)*speed and vy = sin(heading)*speed
|
|
## (the opposite of classic Robocode).
|
|
(vx: cos(degToRad(direction)) * speed,
|
|
vy: sin(degToRad(direction)) * speed)
|
|
|
|
method onRoundStarted*(bot: WaveSurfer, e: RoundStartedEvent) =
|
|
setAdjustGunForBodyTurn(true)
|
|
setAdjustRadarForBodyTurn(true)
|
|
setAdjustRadarForGunTurn(true)
|
|
bot.strafeDir = 1.0
|
|
bot.lastEnemyEnergy = 100.0
|
|
bot.enemyX = 0.0
|
|
bot.enemyY = 0.0
|
|
bot.enemyVx = 0.0
|
|
bot.enemyVy = 0.0
|
|
bot.enemyDir = 0.0
|
|
bot.enemySpeed = 0.0
|
|
bot.stuckTicks = 0
|
|
|
|
method onScannedBot*(bot: WaveSurfer, e: ScannedBotEvent) =
|
|
# Detect bullet fire: enemy energy dropped by 0.1..3.0
|
|
let energyDrop = bot.lastEnemyEnergy - e.energy
|
|
if energyDrop > 0.1 and energyDrop <= 3.0:
|
|
# Enemy fired — reverse strafe direction immediately
|
|
bot.strafeDir *= -1.0
|
|
bot.lastEnemyEnergy = e.energy
|
|
|
|
# Track enemy position and velocity
|
|
bot.enemyX = e.x
|
|
bot.enemyY = e.y
|
|
bot.enemyDir = e.direction
|
|
bot.enemySpeed = e.speed
|
|
let v = enemyVelocity(e.direction, e.speed)
|
|
bot.enemyVx = v.vx
|
|
bot.enemyVy = v.vy
|
|
|
|
# Linear prediction: estimate where enemy will be when bullet arrives
|
|
let dx = e.x - getX()
|
|
let dy = e.y - getY()
|
|
let dist = sqrt(dx*dx + dy*dy)
|
|
let tof = dist / BULLET_SPEED # ticks of flight
|
|
let predX = e.x + bot.enemyVx * tof
|
|
let predY = e.y + bot.enemyVy * tof
|
|
let bearing = directionTo(getX(), getY(), predX, predY)
|
|
let gunDelta = normalizeRelativeAngle(bearing - getGunDirection())
|
|
setGunTurnRate(gunDelta.clamp(-MAX_GUN_TURN_RATE, MAX_GUN_TURN_RATE))
|
|
if abs(gunDelta) < 5.0 and getGunHeat() <= 0.0:
|
|
discard setFire(2.0)
|
|
|
|
method run*(bot: WaveSurfer) =
|
|
while isRunning():
|
|
setRadarTurnRate(45.0)
|
|
|
|
let x = getX()
|
|
let y = getY()
|
|
let arenaW = getArenaWidth().float
|
|
let arenaH = getArenaHeight().float
|
|
let dl = x
|
|
let dr = arenaW - x
|
|
let db = y
|
|
let dt = arenaH - y
|
|
let nearest = min(min(dl, dr), min(db, dt))
|
|
|
|
# Detect a pin. Tank Royale stops a bot dead on wall contact (no sliding),
|
|
# so in a corner both perpendicular options push into one of the two walls.
|
|
if abs(getSpeed()) < 0.5 and nearest < WALL_MARGIN:
|
|
inc bot.stuckTicks
|
|
else:
|
|
bot.stuckTicks = 0
|
|
|
|
var toEnemy = getDirection()
|
|
let haveEnemy = bot.enemyX != 0.0 or bot.enemyY != 0.0
|
|
if haveEnemy:
|
|
toEnemy = directionTo(x, y, bot.enemyX, bot.enemyY)
|
|
|
|
if bot.stuckTicks > 3:
|
|
# Pinned (usually a corner): head straight for the arena centre until
|
|
# moving again, then resume the perpendicular strafe.
|
|
let escapeDir = directionTo(x, y, arenaW * 0.5, arenaH * 0.5)
|
|
let delta = normalizeRelativeAngle(escapeDir - getDirection())
|
|
setTurnRate(delta.clamp(-MAX_TURN_RATE, MAX_TURN_RATE))
|
|
elif haveEnemy:
|
|
# Strafe perpendicular to enemy
|
|
let perpDir = toEnemy + 90.0 * bot.strafeDir
|
|
let bodyDelta = normalizeRelativeAngle(perpDir - getDirection())
|
|
setTurnRate(bodyDelta.clamp(-MAX_TURN_RATE, MAX_TURN_RATE))
|
|
else:
|
|
setTurnRate(4.0 * bot.strafeDir)
|
|
|
|
setTargetSpeed(STRAFE_SPEED)
|
|
|
|
# Wall escape. Instead of a per-tick flip (which jitters in place) the
|
|
# strafe sign is *selected* every tick to point the perpendicular heading
|
|
# away from the nearest wall. It only changes when the other option is
|
|
# clearly better (hysteresis), so it cannot chatter; and because it is
|
|
# re-asserted each tick it also overrides a fire-triggered reversal that
|
|
# would otherwise drive the bot back into the wall.
|
|
if nearest < WALL_MARGIN and bot.stuckTicks <= 3:
|
|
# Outward wall normal (points from the arena towards that wall).
|
|
var nx = 0.0
|
|
var ny = 0.0
|
|
if nearest == dl: nx = -1.0
|
|
elif nearest == dr: nx = 1.0
|
|
elif nearest == db: ny = -1.0
|
|
else: ny = 1.0
|
|
# Pick the strafe sign whose perpendicular heading escapes the wall
|
|
# (smallest component along the outward normal).
|
|
let candA = toEnemy + 90.0 * bot.strafeDir
|
|
let candB = toEnemy - 90.0 * bot.strafeDir
|
|
let dotA = cos(degToRad(candA)) * nx + sin(degToRad(candA)) * ny
|
|
let dotB = cos(degToRad(candB)) * nx + sin(degToRad(candB)) * ny
|
|
if dotA > dotB + WALL_HYST:
|
|
bot.strafeDir *= -1.0
|
|
|
|
go()
|
|
|
|
when isMainModule:
|
|
var bot = WaveSurfer(strafeDir: 1.0, lastEnemyEnergy: 100.0)
|
|
start(bot, botJsonPath)
|