feat(ModularBot): pluggable bot with 4 guns, phantom meteor movement, radar harness
- Gun harness: virtual bullet tracker, rolling fitness, auto-selector - Guns: head-on, linear (extrapolation), circular (integrated formula), tsetlin machine (learning) - Movement: phantom meteor gravity engine (danger histograms, phantom bullets, fire detection) - Radar: harness + radar_lock adapter - Color-coded modules: turret/bullet color per gun, body per movement, scan per radar - Beats Target, SpinBot, Crazy, TrackFire in 10-round battles
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
## Oscillator movement — perpendicular strafing relative to enemy bearing.
|
||||
## Reverses direction every PERIOD ticks; wall proximity triggers one reversal
|
||||
## then locks out further wall-reversals for WALL_LOCKOUT ticks to prevent
|
||||
## sign-flip every tick (which would zero net movement and park the bot).
|
||||
|
||||
import std/math
|
||||
import gun_harness/gun_interface
|
||||
import movement_harness/movement_interface
|
||||
|
||||
const
|
||||
MaxSpeed* = 8.0 ## Tank Royale max speed
|
||||
Period* = 40 ## ticks between reversals; ponytail: fixed, tune if evasion feels predictable
|
||||
WallMargin* = 80.0 ## px from wall to trigger early reversal
|
||||
WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires
|
||||
|
||||
type OscillatorModule* = object
|
||||
sign: float ## +1 or -1, forward/backward relative to perp heading
|
||||
elapsed: int ## ticks since last reversal
|
||||
wallLockout: int ## remaining ticks where wall-reversal is suppressed
|
||||
|
||||
proc initOscillator*(): OscillatorModule =
|
||||
OscillatorModule(sign: 1.0, elapsed: 0, wallLockout: 0)
|
||||
|
||||
proc computeMove*(m: var OscillatorModule, ws: WorldState): MoveCommand =
|
||||
inc m.elapsed
|
||||
if m.wallLockout > 0: dec m.wallLockout
|
||||
|
||||
# Perpendicular heading to enemy: enemy bearing + 90°
|
||||
let enemyBearing = arctan2(ws.enemyY - ws.selfY, ws.enemyX - ws.selfX) * (180.0 / PI)
|
||||
let perpHeading = (enemyBearing + 90.0) mod 360.0
|
||||
|
||||
let nearWall = ws.selfX < WallMargin or ws.selfX > ws.arenaWidth - WallMargin or
|
||||
ws.selfY < WallMargin or ws.selfY > ws.arenaHeight - WallMargin
|
||||
|
||||
if m.elapsed >= Period or (nearWall and m.wallLockout == 0):
|
||||
m.sign *= -1.0
|
||||
m.elapsed = 0
|
||||
if nearWall: m.wallLockout = WallLockout
|
||||
|
||||
# Turn rate = delta from current heading toward perpendicular
|
||||
var delta = perpHeading - ws.selfHeading
|
||||
while delta > 180.0: delta -= 360.0
|
||||
while delta < -180.0: delta += 360.0
|
||||
|
||||
# When moving backward, flip the turn to keep 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))
|
||||
@@ -0,0 +1,232 @@
|
||||
## phantom_meteor.nim — PhantomMeteor gravity engine as a MovementModule.
|
||||
## Wraps gravity.nim (GravityEngine) to satisfy the MovementModule concept.
|
||||
## Fire detection, phantom bullets, waves, and danger histogram are all internal.
|
||||
## All angles in radians internally; interface outputs degrees for bot API.
|
||||
|
||||
import std/math
|
||||
import gun_harness/gun_interface
|
||||
import movement_harness/movement_interface
|
||||
|
||||
# Inline the gravity engine types and logic here to keep the module self-contained.
|
||||
# We re-export nothing from gravity.nim — it's not on the common_libs path.
|
||||
# ponytail: copy instead of import; if gravity.nim moves to common_libs, collapse.
|
||||
|
||||
# ── Vec2 (local, unexported) ──────────────────────────────────────────────────
|
||||
|
||||
type
|
||||
Vec2 = object
|
||||
x, y: float64
|
||||
|
||||
proc vec2(x, y: float64): Vec2 {.inline.} = Vec2(x: x, y: y)
|
||||
proc `+`(a, b: Vec2): Vec2 {.inline.} = vec2(a.x+b.x, a.y+b.y)
|
||||
proc `-`(a, b: Vec2): Vec2 {.inline.} = vec2(a.x-b.x, a.y-b.y)
|
||||
proc `*`(a: Vec2, s: float64): Vec2 {.inline.} = vec2(a.x*s, a.y*s)
|
||||
proc magnitude(v: Vec2): float64 {.inline.} = sqrt(v.x*v.x + v.y*v.y)
|
||||
proc normalize(v: Vec2): Vec2 =
|
||||
let m = v.magnitude
|
||||
if m < 1e-9: vec2(0.0, 0.0) else: vec2(v.x/m, v.y/m)
|
||||
proc dist(a, b: Vec2): float64 {.inline.} = (a-b).magnitude
|
||||
|
||||
# ── Gravity engine types ──────────────────────────────────────────────────────
|
||||
|
||||
const NumBins = 41
|
||||
|
||||
type
|
||||
DangerHistogram = object
|
||||
bins: array[NumBins, float64]
|
||||
|
||||
PhantomBullet = object
|
||||
pos, vel: Vec2
|
||||
weight: float64
|
||||
alive: bool
|
||||
ticks: int
|
||||
|
||||
Wave = object
|
||||
origin: Vec2
|
||||
heading: float64
|
||||
speed: float64
|
||||
radius: float64
|
||||
startDist: float64
|
||||
|
||||
GravityEngine = object
|
||||
histogram: DangerHistogram
|
||||
phantoms: seq[PhantomBullet]
|
||||
waves: seq[Wave]
|
||||
prevEnemyEnergy: float64
|
||||
|
||||
# ── Gravity engine internals ──────────────────────────────────────────────────
|
||||
|
||||
const
|
||||
KBullet = 1500.0
|
||||
KWall = 4000.0
|
||||
KEnemy = 300.0
|
||||
PreferredDist = 400.0
|
||||
NumPhantoms = 25
|
||||
MinDist = 20.0
|
||||
WallMinDist = 40.0
|
||||
VeryClose = 40.0
|
||||
|
||||
proc initEngine(): GravityEngine =
|
||||
var h: DangerHistogram
|
||||
for i in 0..<NumBins: h.bins[i] = 1.0
|
||||
GravityEngine(histogram: h, phantoms: @[], waves: @[], prevEnemyEnergy: 100.0)
|
||||
|
||||
proc gfToBin(gf: float64): int =
|
||||
int(((gf.clamp(-1.0,1.0) + 1.0) / 2.0 * float64(NumBins-1)).round).clamp(0, NumBins-1)
|
||||
|
||||
proc mea(speed: float64): float64 = arcsin(min(8.0/speed, 1.0))
|
||||
|
||||
proc detectFire(eng: var GravityEngine, energy: float64): tuple[fired: bool; power: float64] =
|
||||
let drop = eng.prevEnemyEnergy - energy
|
||||
eng.prevEnemyEnergy = energy
|
||||
if drop >= 0.1 and drop <= 3.0: (true, drop) else: (false, 0.0)
|
||||
|
||||
proc spawnPhantoms(eng: var GravityEngine, enemyPos, botPos: Vec2, bspeed: float64) =
|
||||
let base = arctan2(botPos.y - enemyPos.y, botPos.x - enemyPos.x)
|
||||
let maxA = mea(bspeed)
|
||||
for i in 0..<NumPhantoms:
|
||||
let gf = if NumPhantoms == 1: 0.0 else: -1.0 + float64(i)/float64(NumPhantoms-1)*2.0
|
||||
let angle = base + gf * maxA
|
||||
let weight = eng.histogram.bins[gfToBin(gf)]
|
||||
eng.phantoms.add PhantomBullet(
|
||||
pos: enemyPos, vel: vec2(bspeed*cos(angle), bspeed*sin(angle)),
|
||||
weight: weight, alive: true, ticks: 0)
|
||||
|
||||
proc tickPhantoms(eng: var GravityEngine) =
|
||||
for i in 0..<eng.phantoms.len:
|
||||
if not eng.phantoms[i].alive: continue
|
||||
eng.phantoms[i].pos = eng.phantoms[i].pos + eng.phantoms[i].vel
|
||||
inc eng.phantoms[i].ticks
|
||||
if eng.phantoms[i].ticks >= 50: eng.phantoms[i].alive = false
|
||||
if eng.phantoms.len > 200:
|
||||
var live: seq[PhantomBullet]
|
||||
for p in eng.phantoms:
|
||||
if p.alive: live.add p
|
||||
eng.phantoms = live
|
||||
|
||||
proc spawnWave(eng: var GravityEngine, enemyPos, botPos: Vec2, bspeed: float64) =
|
||||
eng.waves.add Wave(
|
||||
origin: enemyPos,
|
||||
heading: arctan2(botPos.y - enemyPos.y, botPos.x - enemyPos.x),
|
||||
speed: bspeed, radius: 0.0, startDist: dist(enemyPos, botPos))
|
||||
|
||||
proc tickWaves(eng: var GravityEngine, botPos: Vec2) =
|
||||
var i = 0
|
||||
while i < eng.waves.len:
|
||||
eng.waves[i].radius += eng.waves[i].speed
|
||||
if eng.waves[i].radius >= eng.waves[i].startDist:
|
||||
let toBot = arctan2(botPos.y - eng.waves[i].origin.y,
|
||||
botPos.x - eng.waves[i].origin.x)
|
||||
var off = toBot - eng.waves[i].heading
|
||||
while off > PI: off -= 2.0*PI
|
||||
while off < -PI: off += 2.0*PI
|
||||
let maxA = mea(eng.waves[i].speed)
|
||||
if maxA >= 1e-9:
|
||||
let gf = (off / maxA).clamp(-1.0, 1.0)
|
||||
eng.histogram.bins[gfToBin(gf)] += 1.0
|
||||
eng.waves.del(i)
|
||||
else:
|
||||
inc i
|
||||
|
||||
proc computeForces(eng: GravityEngine, botPos, enemyPos: Vec2, arenaW, arenaH: float64): Vec2 =
|
||||
var total = vec2(0.0, 0.0)
|
||||
|
||||
# Priority 1: nearby phantoms
|
||||
var hasClose = false
|
||||
for ph in eng.phantoms:
|
||||
if ph.alive:
|
||||
let d = dist(ph.pos, botPos)
|
||||
if d < 150.0:
|
||||
hasClose = true
|
||||
total = total + normalize(botPos - ph.pos) * (KBullet * (1.0 - d/150.0))
|
||||
|
||||
# Priority 2: wall escape (hard override)
|
||||
let minW = min(min(botPos.x, arenaW-botPos.x), min(botPos.y, arenaH-botPos.y))
|
||||
if minW < VeryClose:
|
||||
var ex = 0.0; var ey = 0.0
|
||||
if botPos.x < VeryClose: ex = 1.0
|
||||
if arenaW - botPos.x < VeryClose: ex = -1.0
|
||||
if botPos.y < VeryClose: ey = 1.0
|
||||
if arenaH - botPos.y < VeryClose: ey = -1.0
|
||||
let m = sqrt(ex*ex + ey*ey)
|
||||
if m > 0.1: return normalize(vec2(ex,ey)) * 500.0
|
||||
|
||||
# Priority 3: distance to enemy
|
||||
let de = dist(enemyPos, botPos)
|
||||
if not hasClose and de > 100.0:
|
||||
if de < PreferredDist:
|
||||
total = total + normalize(botPos - enemyPos) * KEnemy
|
||||
elif de > PreferredDist + 100.0:
|
||||
total = total + normalize(enemyPos - botPos) * (KEnemy * 0.3)
|
||||
|
||||
# Priority 4: weak wall repulsion
|
||||
let dL = max(botPos.x, WallMinDist)
|
||||
let dR = max(arenaW - botPos.x, WallMinDist)
|
||||
let dB = max(botPos.y, WallMinDist)
|
||||
let dT = max(arenaH - botPos.y, WallMinDist)
|
||||
total = total + vec2(KWall*0.5/(dL*dL) - KWall*0.5/(dR*dR),
|
||||
KWall*0.5/(dB*dB) - KWall*0.5/(dT*dT))
|
||||
total
|
||||
|
||||
# ── MovementModule wrapper ────────────────────────────────────────────────────
|
||||
|
||||
type PhantomMeteorModule* = object
|
||||
engine: GravityEngine
|
||||
|
||||
proc initPhantomMeteor*(): PhantomMeteorModule =
|
||||
PhantomMeteorModule(engine: initEngine())
|
||||
|
||||
proc resetRound*(m: var PhantomMeteorModule) =
|
||||
## Clear per-round transients (phantoms, waves, energy baseline), keep histogram.
|
||||
m.engine.phantoms = @[]
|
||||
m.engine.waves = @[]
|
||||
m.engine.prevEnemyEnergy = 100.0
|
||||
|
||||
proc computeMove*(m: var PhantomMeteorModule, ws: WorldState): MoveCommand =
|
||||
let botPos = vec2(ws.selfX, ws.selfY)
|
||||
let enemyPos = vec2(ws.enemyX, ws.enemyY)
|
||||
|
||||
# Advance simulation
|
||||
m.engine.tickPhantoms()
|
||||
m.engine.tickWaves(botPos)
|
||||
|
||||
# Fire detection → spawn phantoms + wave
|
||||
let (fired, power) = m.engine.detectFire(ws.enemyEnergy)
|
||||
if fired:
|
||||
let bspeed = 20.0 - 3.0 * power
|
||||
m.engine.spawnPhantoms(enemyPos, botPos, bspeed)
|
||||
m.engine.spawnWave(enemyPos, botPos, bspeed)
|
||||
|
||||
# Compute force vector
|
||||
let force = m.engine.computeForces(botPos, enemyPos, ws.arenaWidth, ws.arenaHeight)
|
||||
|
||||
if force.magnitude < 1e-9:
|
||||
return (speed: 0.0, turnRate: 0.0)
|
||||
|
||||
# Enemy bearing (radians, math convention: 0=East, CCW+)
|
||||
let enemyBearingRad = arctan2(enemyPos.y - botPos.y, enemyPos.x - botPos.x)
|
||||
|
||||
# Two perpendicular directions to enemy bearing (±90°)
|
||||
let perpCCW = vec2(-sin(enemyBearingRad), cos(enemyBearingRad)) # +90°
|
||||
let perpCW = vec2( sin(enemyBearingRad), -cos(enemyBearingRad)) # -90°
|
||||
|
||||
# Pick perpendicular direction that aligns with force vector
|
||||
let fn = force.normalize
|
||||
let desiredDeg =
|
||||
if fn.x * perpCCW.x + fn.y * perpCCW.y >= fn.x * perpCW.x + fn.y * perpCW.y:
|
||||
radToDeg(arctan2(perpCCW.y, perpCCW.x))
|
||||
else:
|
||||
radToDeg(arctan2(perpCW.y, perpCW.x))
|
||||
|
||||
# Delta from current heading
|
||||
var delta = desiredDeg - ws.selfHeading
|
||||
while delta > 180.0: delta -= 360.0
|
||||
while delta < -180.0: delta += 360.0
|
||||
|
||||
# Dot-product trick: if |delta| > 90 → reverse, less turning
|
||||
let goForward = abs(delta) <= 90.0
|
||||
if not goForward:
|
||||
delta = if delta >= 0.0: delta - 180.0 else: delta + 180.0
|
||||
|
||||
(speed: if goForward: 8.0 else: -8.0,
|
||||
turnRate: delta.clamp(-10.0, 10.0))
|
||||
Reference in New Issue
Block a user