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
+94 -35
View File
@@ -27,6 +27,7 @@ import guns/decay_gf
import guns/knn_gun
import movements/phantom_meteor
import movements/rammer
import movements/the_floor_is_lava
import movement_harness/virtual_bodies as mvb
import movement_harness/bullet_shadows
import targeting/enemy_tracker
@@ -71,9 +72,12 @@ type
avgLead: AveragedLeadGun
decayGF: DecayGFGun
knnGun: KNNGun
mover: PhantomMeteorModule
mover: TFILModule
rammer: RammerModule
isRamming: bool
ramStuckTicks: int
ramDurationTicks: int
ramCooldownTicks: int
moveTracker: VirtualBodyTracker
virtualHits: int
virtualMiss: int
@@ -82,14 +86,26 @@ type
currentTargetId: int
targetSwitchTick: int
enemyTracker: EnemyTracker
prevGun: int
prevRadar: int
prevTarget: int
proc printConfig(bot: ModularBot, changed: string = "") =
let gunName = GunNames[bot.currentGun]
let gunColor = if changed == "gun" or changed == "all": CLR_CHANGE else: CLR_RST
let radarColor = if changed == "radar" or changed == "all": CLR_CHANGE else: CLR_RST
let gunName = GunNames[bot.currentGun]
let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan"
let moveName = if bot.isRamming: "rammer" else: "phantom_meteor"
echo CLR_ROUND & "[config]" & CLR_RST & " " & gunColor & "gun=" & gunName & CLR_RST & " | " & CLR_MOVE & "move=" & moveName & CLR_RST & " | " & radarColor & "radar=" & radarName & CLR_RST & " | enemies=" & $bot.enemyTracker.allAlive().len & " target=#" & $bot.currentTargetId
let moveName = if bot.isRamming: "rammer" else: "tfil"
let gc = if bot.currentGun != bot.prevGun or changed == "all": CLR_CHANGE else: ""
let rc = if bot.radarMode != bot.prevRadar or changed == "all": CLR_CHANGE else: ""
let tc = if bot.currentTargetId != bot.prevTarget or changed == "all": CLR_CHANGE else: ""
let rst = CLR_RST
echo "[config] " & gc & "gun=" & gunName & (if gc != "": rst else: "") &
" | move=" & moveName &
" | " & rc & "radar=" & radarName & (if rc != "": rst else: "") &
" | enemies=" & $bot.enemyTracker.allAlive().len &
" target=" & tc & "#" & $bot.currentTargetId & (if tc != "": rst else: "")
bot.prevGun = bot.currentGun
bot.prevRadar = bot.radarMode
bot.prevTarget = bot.currentTargetId
proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): WorldState =
var ei: seq[EnemyInfo]
@@ -128,9 +144,11 @@ method onBulletHitBot*(bot: ModularBot, e: BulletHitBotEvent) =
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
bot.mover.removeBulletNear(e.bullet.x, e.bullet.y)
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
@@ -147,6 +165,9 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
bot.mover.resetRound()
bot.enemyTracker.resetRound()
bot.isRamming = false
bot.ramStuckTicks = 0
bot.ramDurationTicks = 0
bot.ramCooldownTicks = 0
bot.hasContact = false
bot.tick = 0
bot.currentGun = -1
@@ -171,11 +192,15 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
# tracker fitness persists across rounds (rolling window carries over)
method onBotDeath*(bot: ModularBot, e: BotDeathEvent) =
echo "[death] victimId=", e.victimId, " wasTarget=", (e.victimId == bot.currentTargetId),
" trackerAlive=", (if bot.enemyTracker.enemies.contains(e.victimId): $bot.enemyTracker.enemies[e.victimId].alive else: "notInTracker")
echo CLR_MOVE & "[death] victimId=" & $e.victimId & " wasTarget=" & $(e.victimId == bot.currentTargetId) & CLR_RST
bot.enemyTracker.markDead(e.victimId)
if bot.enemyCount > 0:
dec bot.enemyCount
if e.victimId == bot.currentTargetId:
bot.isRamming = false
bot.currentTargetId = -1
method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
@@ -201,9 +226,18 @@ method run*(bot: ModularBot) =
while isRunning():
inc bot.tick
# Debug: print enemy state on first tick
if bot.tick == 1:
echo "[debug] enemyCount=" & $bot.enemyCount & " tracker=" & $bot.enemyTracker.allAlive().len & " radarMode=" & $bot.radarMode
# Ram cooldown countdown
if bot.ramCooldownTicks > 0:
dec bot.ramCooldownTicks
# Safety: reset invalid target before any logic
if bot.currentTargetId >= 0:
if not bot.enemyTracker.enemies.contains(bot.currentTargetId) or
not bot.enemyTracker.enemies[bot.currentTargetId].alive:
echo CLR_MOVE & "[target-invalid] id=" & $bot.currentTargetId & " resetting" & CLR_RST
bot.currentTargetId = -1
bot.isRamming = false
if not bot.hasContact:
setTargetSpeed(0.0)
@@ -212,25 +246,65 @@ method run*(bot: ModularBot) =
go()
continue
# Ram decision — harness decides, not the movement module
# --- Target selection (sticky) ---
let candidateId = selectTarget(bot.enemyTracker, getX(), getY(), tmClosest)
if bot.shouldSwitchTarget(candidateId):
if candidateId != bot.currentTargetId:
bot.currentTargetId = candidateId
bot.targetSwitchTick = bot.tick
bot.printConfig("target")
# --- Refresh target state FIRST, before ram decision ---
let tid = bot.currentTargetId
if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and
bot.enemyTracker.enemies[tid].alive:
let tgt = bot.enemyTracker.getEnemy(tid)
bot.enemyBearing = directionTo(getX(), getY(), tgt.x, tgt.y)
bot.lastState = bot.buildState(tgt.x, tgt.y, tgt.speed, tgt.heading, tgt.energy)
# Ram decision — harness decides, not the movement module (uses fresh lastState)
let ws = bot.lastState
let ramDist = hypot(ws.enemyX - ws.selfX, ws.enemyY - ws.selfY)
let ramFinisher = ramDist < 300.0 and ws.enemyEnergy < 20.0 and ws.selfEnergy > ws.enemyEnergy
let ramOpportunity = ramDist < 80.0 and ws.selfEnergy > ws.enemyEnergy + 10.0
let ramDesperation = ws.selfEnergy < 5.0 and ws.enemyEnergy < 5.0 and ramDist < 150.0
let shouldRam = ramFinisher or ramOpportunity or ramDesperation
let ramFinisher = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ramDist < 300.0 and ws.enemyEnergy < 20.0 and ws.selfEnergy > ws.enemyEnergy
let ramOpportunity = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ramDist < 50.0 and ws.selfEnergy > ws.enemyEnergy + 30.0
let ramDesperation = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ws.selfEnergy < 5.0 and ws.enemyEnergy < 5.0 and ramDist < 150.0
let shouldRam = bot.currentTargetId >= 0 and
(ramFinisher or ramOpportunity or ramDesperation)
if shouldRam and not bot.isRamming:
bot.isRamming = true
let tag = if ramDesperation: "desperation" elif ramFinisher: "finisher" else: "opportunity"
echo CLR_MOVE & "[ram:" & tag & "] dist=" & $int(ramDist) &
" selfE=" & $int(ws.selfEnergy) & " enemyE=" & $int(ws.enemyEnergy) & CLR_RST
bot.printConfig("move")
bot.ramStuckTicks = 0
bot.ramDurationTicks = 0
if ramFinisher:
echo "[ram:enter] reason=finisher dist=", ramDist.int, " selfEnergy=", ws.selfEnergy.int, " enemyEnergy=", ws.enemyEnergy.int
elif ramOpportunity:
echo "[ram:enter] reason=opportunity dist=", ramDist.int, " energyAdv=", (ws.selfEnergy - ws.enemyEnergy).int
elif ramDesperation:
echo "[ram:enter] reason=desperation selfEnergy=", ws.selfEnergy.int
elif not shouldRam and bot.isRamming:
bot.isRamming = false
bot.printConfig("move")
if bot.isRamming:
inc bot.ramDurationTicks
if ramDist < 5.0:
inc bot.ramStuckTicks
else:
bot.ramStuckTicks = 0
if bot.ramStuckTicks > 10:
bot.isRamming = false
bot.currentTargetId = -1
bot.ramStuckTicks = 0
bot.ramDurationTicks = 0
bot.ramCooldownTicks = 30
elif bot.ramDurationTicks > 60:
bot.isRamming = false
bot.currentTargetId = -1
bot.ramStuckTicks = 0
bot.ramDurationTicks = 0
bot.ramCooldownTicks = 30
let (spd, tr) = if shouldRam:
bot.mover.clearGraphics()
bot.rammer.computeMove(ws)
else:
bot.mover.computeMove(ws)
@@ -258,23 +332,9 @@ method run*(bot: ModularBot) =
setRadarTurnRate(radarRate)
go()
# --- Target selection (sticky) --- lock target during ram to prevent dangerous switch
if not bot.isRamming:
let candidateId = selectTarget(bot.enemyTracker, getX(), getY(), tmClosest)
if bot.shouldSwitchTarget(candidateId):
if candidateId != bot.currentTargetId:
bot.currentTargetId = candidateId
bot.targetSwitchTick = bot.tick
bot.printConfig("target")
# --- Aim + fire from tracked state (not scan event) ---
let tid = bot.currentTargetId
if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and
bot.enemyTracker.enemies[tid].alive:
let tgt = bot.enemyTracker.getEnemy(tid)
bot.enemyBearing = directionTo(getX(), getY(), tgt.x, tgt.y)
bot.lastState = bot.buildState(tgt.x, tgt.y, tgt.speed, tgt.heading, tgt.energy)
# Spawn virtual bullets for all guns
var headsUp: array[len(PowerBins), GunPrediction]
var linPreds: array[len(PowerBins), GunPrediction]
@@ -361,7 +421,6 @@ method run*(bot: ModularBot) =
let (selectedGun, _, power) = selectShot(bot.tracker, tid)
if selectedGun != bot.currentGun:
bot.currentGun = selectedGun
bot.printConfig("gun")
case selectedGun
of 0: setTurretColor("#FF3333"); setBulletColor("#FF6666")
of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF")
@@ -427,7 +486,7 @@ when isMainModule:
knnGun: initKNNGun(),
radar: RadarLockModule(),
meleeScan: initMeleeScan(),
mover: initPhantomMeteor(),
mover: TFILModule(debugGraphics: true),
rammer: initRammer(),
moveTracker: mvb.initVirtualBodyTracker(1),
currentGun: -1,