feat(ModularBot): virtual body movement selector — wave-based scoring replaces EMA damage

Implements VirtualBodyTracker (wave-based hit/miss scoring) instead of EMA damage accumulation. Movement switching now happens every tick, not every 3 rounds. Also refactors radar colors to dark teal (#004444/#0D4D4D) for faint visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 10:44:49 +02:00
parent d2a2ebceb9
commit c0abd4c960
2 changed files with 176 additions and 30 deletions
+24 -30
View File
@@ -25,6 +25,7 @@ import guns/averaged_lead
import guns/decay_gf
import movements/phantom_meteor
import movements/wave_surfer
import movement_harness/virtual_bodies as mvb
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
const DebugVBullets = false
@@ -54,10 +55,7 @@ type
mover: PhantomMeteorModule
waveMover: WaveSurferModule
movementId: int ## 0=phantom, 1=waveSurf
dmgPhantom: float ## rolling damage taken under phantom
dmgWaveSurf: float ## rolling damage taken under waveSurf
roundDmg: float ## damage this round
roundCount: int
moveTracker: VirtualBodyTracker
virtualHits: int
virtualMiss: int
tick: int
@@ -215,35 +213,25 @@ method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
bot.roundDmg += e.damage
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
# Accumulate damage into active movement bucket (exponential decay, α=0.5)
# ponytail: simple EMA; switch to longer window if thrashing becomes a problem
let decay = 0.5
if bot.movementId == 0:
bot.dmgPhantom = decay * bot.dmgPhantom + (1.0 - decay) * bot.roundDmg
else:
bot.dmgWaveSurf = decay * bot.dmgWaveSurf + (1.0 - decay) * bot.roundDmg
echo "[move] round dmg=", bot.roundDmg, " phantom_ema=", bot.dmgPhantom, " wave_ema=", bot.dmgWaveSurf
let newId = bot.moveTracker.bestMovement()
if newId != bot.movementId:
echo "[move] switching: ", (if newId == 0: "phantom_meteor" else: "wave_surfer")
bot.movementId = newId
method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
setRadarColor("#00CCCC") # RadarLock: cyan/teal
setScanColor("#33FFFF") # RadarLock scan arc: bright teal
setRadarColor("#004444") # RadarLock: dark teal, faint without alpha
setScanColor("#0D4D4D") # RadarLock scan arc: very dark teal, faint without alpha
bot.radar.init()
bot.hasContact = false
bot.tick = 0
bot.currentGun = -1
bot.roundDmg = 0.0
inc bot.roundCount
# Every 3 rounds: switch to lower-damage movement (probe every 3rd round)
# ponytail: simple 3-round probe; add hysteresis if switching thrashes
if bot.roundCount > 2:
bot.movementId = if bot.dmgPhantom <= bot.dmgWaveSurf: 0 else: 1
bot.moveTracker.resetRound(getX(), getY(), getDirection(), getSpeed())
if bot.movementId == 0:
bot.mover.resetRound()
@@ -271,10 +259,15 @@ method run*(bot: ModularBot) =
go()
continue
# Movement harness: dispatch to active mover
let (spd, tr) =
if bot.movementId == 0: bot.mover.computeMove(bot.lastState)
else: bot.waveMover.computeMove(bot.lastState)
# Movement harness: compute all, feed tracker, apply active
let phantomCmd = bot.mover.computeMove(bot.lastState)
let waveSurfCmd = bot.waveMover.computeMove(bot.lastState)
bot.moveTracker.tick(bot.lastState, [phantomCmd, waveSurfCmd])
let activeId = bot.moveTracker.bestMovement()
if activeId != bot.movementId:
bot.movementId = activeId
echo "[move] switched to: ", (if activeId == 0: "phantom_meteor" else: "wave_surfer")
let (spd, tr) = if bot.movementId == 0: phantomCmd else: waveSurfCmd
setTargetSpeed(spd)
setTurnRate(tr)
@@ -297,9 +290,10 @@ when isMainModule:
displace: initDisplacementGun(),
avgLead: initAveragedLeadGun(),
decayGF: initDecayGFGun(),
radar: RadarLockModule(),
mover: initPhantomMeteor(),
waveMover: initWaveSurfer(),
currentGun: -1,
radar: RadarLockModule(),
mover: initPhantomMeteor(),
waveMover: initWaveSurfer(),
moveTracker: mvb.initVirtualBodyTracker(2), # 0=phantom, 1=waveSurf
currentGun: -1,
)
start(bot, botJsonPath)