From d27efd3f9a80e1ae5e1c39018ccb19dd34a1a789 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Sun, 20 Sep 2026 01:39:49 +0200 Subject: [PATCH] feat(ModularBot): random-oscillator movement module + battle tests - Random-oscillator: perpendicular strafe with randomized reversal timing (15-45 ticks) - Not wired (needs movement selector) - Battle-tested 10-gun bot vs RamFire, Corners, VelociBot Co-Authored-By: Claude Sonnet 4.6 --- common_libs/movements/random_oscillator.nim | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 common_libs/movements/random_oscillator.nim diff --git a/common_libs/movements/random_oscillator.nim b/common_libs/movements/random_oscillator.nim new file mode 100644 index 0000000..e8be9fe --- /dev/null +++ b/common_libs/movements/random_oscillator.nim @@ -0,0 +1,56 @@ +## random_oscillator.nim — perpendicular strafing with randomized reversal timing. +## Harder to predict than fixed-period oscillation; same wall-lockout guard as oscillator.nim. + +import std/math +import std/random +import std/times +import gun_harness/gun_interface +import movement_harness/movement_interface + +const + MaxSpeed* = 8.0 ## Tank Royale max speed + MinPeriod* = 15 ## shortest reversal interval (ticks) + MaxPeriod* = 45 ## longest reversal interval (ticks) + WallMargin* = 80.0 ## px from wall to trigger forced reversal + WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires + +type RandomOscillatorModule* = object + sign: float ## +1 or -1 + elapsed: int ## ticks since last reversal + nextPeriod: int ## random period chosen at last reversal + wallLockout: int ## remaining suppression ticks + rng: Rand + +proc initRandomOscillator*(): RandomOscillatorModule = + var rng = initRand(getTime().toUnix()) + let period = rng.rand(MinPeriod..MaxPeriod) + RandomOscillatorModule(sign: 1.0, elapsed: 0, nextPeriod: period, + wallLockout: 0, rng: rng) + +proc computeMove*(m: var RandomOscillatorModule, ws: WorldState): MoveCommand = + inc m.elapsed + if m.wallLockout > 0: dec m.wallLockout + + let nearWall = ws.selfX < WallMargin or ws.selfX > ws.arenaWidth - WallMargin or + ws.selfY < WallMargin or ws.selfY > ws.arenaHeight - WallMargin + + if m.elapsed >= m.nextPeriod or (nearWall and m.wallLockout == 0): + m.sign *= -1.0 + m.elapsed = 0 + m.nextPeriod = m.rng.rand(MinPeriod..MaxPeriod) + if nearWall: m.wallLockout = WallLockout + + # Perpendicular heading to enemy bearing (same dot-product trick as phantom_meteor) + let enemyBearingRad = arctan2(ws.enemyY - ws.selfY, ws.enemyX - ws.selfX) + let perpHeadingDeg = radToDeg(enemyBearingRad) + 90.0 + + var delta = perpHeadingDeg - ws.selfHeading + while delta > 180.0: delta -= 360.0 + while delta < -180.0: delta += 360.0 + + # Dot-product trick: if going backward, flip turn to stay perpendicular + var normDelta = if m.sign < 0: delta - 180.0 else: delta + while normDelta > 180.0: normDelta -= 360.0 + while normDelta < -180.0: normDelta += 360.0 + + (speed: m.sign * MaxSpeed, turnRate: normDelta.clamp(-10.0, 10.0))