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