# 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)