fix(ModularBot): ram loop prevention, dead-target guards, cleaner logging

- 30-tick cooldown after ghost-stuck/timeout ram exit prevents re-entry loop
- enemy_tracker.update() skips dead bots to prevent same-tick scan resurrection
- TFIL graphics cleared when ramming is active movement
- [config] logs: white base with green-highlighted changes only
- [ram:enter] logs trigger reason and key values on false→true transition
- [death] and [target-invalid] logs retained for diagnostics
This commit is contained in:
2026-09-20 20:44:45 +02:00
parent 1f8574db1d
commit 2cc2a3bd87
30 changed files with 927 additions and 161 deletions
+49 -38
View File
@@ -146,16 +146,17 @@ proc computeForces(eng: GravityEngine, botPos: Vec2, enemies: openArray[Vec2], a
hasClose = true
total = total + normalize(botPos - ph.pos) * (KBullet * (1.0 - d/250.0))
# Priority 2: wall escape (additive — blends with phantom forces)
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 wm = sqrt(ex*ex + ey*ey)
if wm > 0.1: total = total + normalize(vec2(ex, ey)) * 500.0
# Priority 2: wall escape — scales as inverse-square so it dominates bullets near walls
var ex = 0.0; var ey = 0.0
let dLeft = botPos.x
let dRight = arenaW - botPos.x
let dBot = botPos.y
let dTop = arenaH - botPos.y
if dLeft < WallMinDist: ex += KWall / (dLeft * dLeft)
if dRight < WallMinDist: ex -= KWall / (dRight * dRight)
if dBot < WallMinDist: ey += KWall / (dBot * dBot)
if dTop < WallMinDist: ey -= KWall / (dTop * dTop)
total = total + vec2(ex, ey)
# Priority 3: repulsion from ALL enemies; attraction only toward nearest.
# ponytail: linear ramp per enemy, attraction only toward nearest to avoid being pulled into a crowd
@@ -181,23 +182,17 @@ proc computeForces(eng: GravityEngine, botPos: Vec2, enemies: openArray[Vec2], a
# Too far from nearest only → mild attraction
total = total + normalize(ep - botPos) * (KEnemy * t)
# 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
gunheat: GunheatTracker
engine: GravityEngine
gunheat: GunheatTracker
debugGraphics*: bool
proc initPhantomMeteor*(): PhantomMeteorModule =
PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker())
PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker(), debugGraphics: false)
proc resetRound*(m: var PhantomMeteorModule) =
## Clear per-round transients (phantoms, waves, energy baseline), keep histogram.
@@ -264,27 +259,43 @@ proc computeMove*(m: var PhantomMeteorModule, ws: WorldState): MoveCommand =
if force.magnitude < 1e-9:
return (speed: 0.0, turnRate: 0.0)
# Threat centroid (inverse-distance weighted) for perpendicular direction.
# With 1 enemy this equals the single enemy bearing — identical to prior behavior.
var cx = 0.0; var cy = 0.0; var wsum = 0.0
for ep in enemyVecs:
let d = max(dist(ep, botPos), 1.0)
let w = 1.0 / d
cx += ep.x * w; cy += ep.y * w; wsum += w
let centroid = if wsum > 1e-9: vec2(cx/wsum, cy/wsum) else: enemyPos
let centroidBearingRad = arctan2(centroid.y - botPos.y, centroid.x - botPos.x)
# Corner detection: near 2 walls simultaneously → hard escape override.
# ponytail: WallMinDist threshold reused; increase if bot still traps at higher speeds.
let nearL = botPos.x < WallMinDist
let nearR = ws.arenaWidth - botPos.x < WallMinDist
let nearB = botPos.y < WallMinDist
let nearT = ws.arenaHeight - botPos.y < WallMinDist
let wallCount = (if nearL: 1 else: 0) + (if nearR: 1 else: 0) +
(if nearB: 1 else: 0) + (if nearT: 1 else: 0)
let cornered = wallCount >= 2
# Two perpendicular directions to centroid bearing (±90°)
let perpCCW = vec2(-sin(centroidBearingRad), cos(centroidBearingRad)) # +90°
let perpCW = vec2( sin(centroidBearingRad), -cos(centroidBearingRad)) # -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))
if cornered:
# Escape direction = inward wall normals averaged; ignore bullet/enemy pulls.
# Enemy perp is irrelevant here — any direction away from corner is correct.
var escX = 0.0; var escY = 0.0
if nearL: escX += 1.0
if nearR: escX -= 1.0
if nearB: escY += 1.0
if nearT: escY -= 1.0
let escDir = normalize(vec2(escX, escY))
radToDeg(arctan2(escDir.y, escDir.x))
else:
radToDeg(arctan2(perpCW.y, perpCW.x))
# Normal open-field: snap to perp that best aligns with force vector.
var cx = 0.0; var cy = 0.0; var wsum = 0.0
for ep in enemyVecs:
let d = max(dist(ep, botPos), 1.0)
let w = 1.0 / d
cx += ep.x * w; cy += ep.y * w; wsum += w
let centroid = if wsum > 1e-9: vec2(cx/wsum, cy/wsum) else: enemyPos
let centroidBearingRad = arctan2(centroid.y - botPos.y, centroid.x - botPos.x)
let perpCCW = vec2(-sin(centroidBearingRad), cos(centroidBearingRad))
let perpCW = vec2( sin(centroidBearingRad), -cos(centroidBearingRad))
let fn = force.normalize
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