fix(adversaries): repair four of the five sparring bots

The user suspected these were bugged. They were, and the verdicts are not
uniform - three genuinely broken, one merely sloppy, one fine:

- WaveSurfer: GENUINELY BUGGED, worst of the five. (a) The enemy velocity
  decomposition was sin/cos SWAPPED - enemyVx used sin and enemyVy used cos,
  while Tank Royale is 0 deg = East, CCW+, so it must be cos for X and sin for
  Y. Its linear-prediction gun was aiming at a reflected position. (b) The wall
  escape flipped strafeDir on EVERY tick the bot was inside the wall margin,
  so instead of turning away it flip-flopped in place: measured standing still
  (speed < 0.5) for 96.2% of ticks with a longest continuous standstill of 1398
  ticks. Fixed with a hysteretic wall-escape selection plus a corner escape,
  dead enemyLastDir removed, and per-round state reset.
  AFTER, measured through the booter: rest 12.3%, mean speed 6.69, full speed
  79.7%, longest zero run 18, perpendicular 0.845 / radial 0.012 - it now
  actually strafes. Gun sanity: lead error 1.0 px vs 106 px for head-on on a
  constant-velocity target; lead gun 45.8% hits vs 29.3% for head-on.
- PatternMover: GENUINELY BUGGED. Real deadlock - it decremented its step
  counter by the REQUESTED amount while issuing setTargetSpeed(8), so against a
  wall the counter never reached 0, advanceStep never ran and it was stuck
  forever (309-tick standstill). Now counts down by ACTUAL distance/turn with a
  STALL_LIMIT watchdog and steers toward the arena centre. Standstill 309 -> 19
  ticks; full-speed ticks 10.0% -> 28.4%.
- OscillatorBot: GENUINELY BUGGED, milder. No wall handling at all, so it
  ground along walls 53.4% of ticks and could pin in a corner. Added wall
  steering that preserves the fixed 25-tick reversal cadence. Wall-band 53.4%
  -> 18.6%, mean wall distance 72 -> 119.
- RandomMover: merely sloppy, not broken. Its turn intent saturated against the
  speed-dependent limit (18.4% of moving ticks clamped) and the fire gate was a
  very loose 10 deg. Now clamps to calcMaxTurnRate and fires within 3 deg.
  Saturation 18.4% -> 3.9%.
- SittingDuck: FINE. Speed 0 for 100% of ticks, zero shots. Left untouched -
  it is a duck by design.

Adds test_wavesurfer_velocity.nim, a direct assertion that the decomposition is
cos/sin and explicitly NOT the swapped form (7 cases).

KNOWN ISSUE, not fixed: RandomMover/PatternMover/WaveSurfer import
tankroyale_botapi 1.0.1 and intermittently SIGSEGV in
tankroyale_botapi/event_queue.nim:89 addEvent, freezing the bot for the rest of
the battle. It reproduces on old and new code and never occurs for SittingDuck/
OscillatorBot, which import robocode_tankroyale_botapi 1.0.7. Migrating the
three to 1.0.7 would likely fix it and is worth doing - it is a real
reliability risk for these as sparring partners.
This commit is contained in:
2026-09-21 08:21:47 +02:00
parent e6653199bb
commit c214abcfa8
9 changed files with 206 additions and 36 deletions
@@ -17,18 +17,29 @@ const STEPS: array[6, Step] = [
( 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.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)
@@ -46,27 +57,52 @@ proc advanceStep(bot: PatternMover) =
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 -= rate
if bot.remaining <= 0.0:
advanceStep(bot)
bot.remaining -= turned
if turned < 0.05: inc bot.stallTicks else: bot.stallTicks = 0
else:
let rate = min(bot.remaining, 8.0)
setTargetSpeed(8.0)
setTurnRate(0.0)
bot.remaining -= rate
if bot.remaining <= 0.0:
advanceStep(bot)
# 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()