diff --git a/ModularBot_garage/ModularBot b/ModularBot_garage/ModularBot index 170ac0c..20a43aa 100755 Binary files a/ModularBot_garage/ModularBot and b/ModularBot_garage/ModularBot differ diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index ac4907f..b8fd347 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -1,12 +1,13 @@ ## ModularBot — plugin gun architecture tracer bullet. ## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), AntiSurferGun (6), WallBounceGun (7), AccelGun (8), StopShotGun (9), DisplacementGun (10), AveragedLeadGun (11), DecayGFGun (12) via GunHarness. -## Radar: RadarLockModule via radar harness. +## Radar: RadarLockModule (1v1) / MeleeScanModule (2+ enemies), auto-switched per tick. ## Movement: OscillatorModule (perpendicular strafing). -import std/[math, os, strformat] +import std/[math, os, strformat, tables] import robocode_tankroyale_botapi import radar_harness/radar_interface import radars/radar_lock_module +import radars/melee_scan import gun_harness/gun_interface import gun_harness/virtual_bullets as vb import gun_harness/selector @@ -27,6 +28,8 @@ import guns/knn_gun import movements/phantom_meteor import movement_harness/virtual_bodies as mvb import movement_harness/bullet_shadows +import targeting/enemy_tracker +import targeting/target_selector const botJsonPath = currentSourcePath().parentDir / "ModularBot.json" const DebugVBullets = false @@ -48,6 +51,10 @@ type enemyBearing: float lastState: WorldState radar: RadarLockModule + meleeScan: MeleeScanModule + enemyCount: int + initialEnemyCount: int + radarMode: int # 0 = lock, 1 = melee tracker: VirtualTracker headOn: HeadOnGun linear: LinearGun @@ -69,12 +76,16 @@ type virtualMiss: int tick: int currentGun: int + currentTargetId: int + targetSwitchTick: int + enemyTracker: EnemyTracker 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 - echo CLR_ROUND & "[config]" & CLR_RST & " " & gunColor & "gun=" & gunName & CLR_RST & " | " & CLR_MOVE & "move=phantom_meteor" & CLR_RST & " | " & radarColor & "radar=radar_lock" & CLR_RST + let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan" + echo CLR_ROUND & "[config]" & CLR_RST & " " & gunColor & "gun=" & gunName & CLR_RST & " | " & CLR_MOVE & "move=phantom_meteor" & CLR_RST & " | " & radarColor & "radar=" & radarName & CLR_RST & " | enemies=" & $bot.enemyTracker.allAlive().len & " target=#" & $bot.currentTargetId proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): WorldState = WorldState( @@ -95,142 +106,8 @@ proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): Worl ) method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) = - bot.enemyBearing = directionTo(getX(), getY(), e.x, e.y) - bot.hasContact = true - bot.lastState = bot.buildState(e.x, e.y, e.speed, e.direction, e.energy) - - # Collect predictions for all power bins from all guns - var headsUp: array[len(PowerBins), GunPrediction] - var linPreds: array[len(PowerBins), GunPrediction] - var tmPreds: array[len(PowerBins), GunPrediction] - var circPreds: array[len(PowerBins), GunPrediction] - var gfPreds: array[len(PowerBins), GunPrediction] - var pmPreds: array[len(PowerBins), GunPrediction] - var asPreds: array[len(PowerBins), GunPrediction] - var wbPreds: array[len(PowerBins), GunPrediction] - var acPreds: array[len(PowerBins), GunPrediction] - var ssPreds: array[len(PowerBins), GunPrediction] - var dsPreds: array[len(PowerBins), GunPrediction] - var alPreds: array[len(PowerBins), GunPrediction] - var dgPreds: array[len(PowerBins), GunPrediction] - var knnPreds: array[len(PowerBins), GunPrediction] - for i in 0.. 0: bot.virtualHits.float / total.float * 100.0 else: 0.0 - echo fmt"[vbullet] gun={gunId} bin={binIdx} miss={fe.missDistance:.1f}px hit={fe.hit} | total hits={bot.virtualHits}/{total} ({pct:.1f}%)" - ) - - # Gun selection + fire - let (selectedGun, _, power) = selectShot(bot.tracker) - - # Log gun switch + update turret/bullet colors - if selectedGun != bot.currentGun: - bot.currentGun = selectedGun - bot.printConfig("gun") - case selectedGun - of 0: setTurretColor("#FF3333"); setBulletColor("#FF6666") # HeadOn: warm red - of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF") # Linear: electric blue - of 2: setTurretColor("#9933FF"); setBulletColor("#CC66FF") # Tsetlin: violet - of 3: setTurretColor("#33CC33"); setBulletColor("#66FF66") # Circular: emerald - of 4: setTurretColor("#FFCC00"); setBulletColor("#FFE066") # GuessFactor: yellow - of 5: setTurretColor("#FF6600"); setBulletColor("#FF9944") # Pattern: orange - of 6: setTurretColor("#00CCFF"); setBulletColor("#66DDFF") # AntiSurf: cyan - of 7: setTurretColor("#CCCCCC"); setBulletColor("#EEEEEE") # WallBounce: silver/white - of 8: setTurretColor("#FF00FF"); setBulletColor("#FF66FF") # Accel: magenta - of 9: setTurretColor("#990000"); setBulletColor("#CC3333") # StopShot: dark red - of 10: setTurretColor("#006600"); setBulletColor("#009900") # Displace: dark green - of 11: setTurretColor("#996633"); setBulletColor("#CC9966") # AvgLead: brown - of 12: setTurretColor("#008888"); setBulletColor("#00AAAA") # DecayGF: dark cyan - of 13: setTurretColor("#CC00CC"); setBulletColor("#FF44FF") # KNN: purple/magenta - else: discard - - let pred = case selectedGun - of 1: bot.linear.predict(bot.lastState, bulletSpeed(power)) - of 2: bot.tsetlin.predict(bot.lastState, bulletSpeed(power)) - of 3: bot.circular.predict(bot.lastState, bulletSpeed(power)) - of 4: bot.guessFactor.predict(bot.lastState, bulletSpeed(power)) - of 5: bot.patternMatcher.predict(bot.lastState, bulletSpeed(power)) - of 6: bot.antiSurfer.predict(bot.lastState, bulletSpeed(power)) - of 7: bot.wallBounce.predict(bot.lastState, bulletSpeed(power)) - of 8: bot.accelGun.predict(bot.lastState, bulletSpeed(power)) - of 9: bot.stopShot.predict(bot.lastState, bulletSpeed(power)) - of 10: bot.displace.predict(bot.lastState, bulletSpeed(power)) - of 11: bot.avgLead.predict(bot.lastState, bulletSpeed(power)) - of 12: bot.decayGF.predict(bot.lastState, bulletSpeed(power)) - of 13: bot.knnGun.predict(bot.lastState, bulletSpeed(power)) - else: bot.headOn.predict(bot.lastState, bulletSpeed(power)) - let target = aimAngle(getX(), getY(), pred.x, pred.y) - - let gunDir = getGunDirection() - let gunHeat = getGunHeat() - let gunDelta = (target - gunDir) mod 360.0 - var normDelta = gunDelta - if normDelta > 180.0: normDelta -= 360.0 - elif normDelta < -180.0: normDelta += 360.0 - - if shouldFire(gunDir, target, gunHeat): - discard setFire(power) - - # Turn gun toward predicted point every scan - setGunTurnRate(normDelta) + bot.enemyTracker.update(e.scannedBotId, e.x, e.y, e.direction, e.speed, e.energy, bot.tick) + bot.hasContact = true method onBulletFired*(bot: ModularBot, e: BulletFiredEvent) = @@ -256,26 +133,66 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) = setAdjustGunForBodyTurn(true) setAdjustRadarForBodyTurn(true) setAdjustRadarForGunTurn(true) - setRadarColor("#004444") # RadarLock: dark teal, faint without alpha - setScanColor("#0D4D4D") # RadarLock scan arc: very dark teal, faint without alpha setBodyColor("#FF9900") # PhantomMeteor: deep orange/gold bot.radar.init() bot.mover.resetRound() - bot.hasContact = false - bot.tick = 0 - bot.currentGun = -1 + bot.enemyTracker.resetRound() + bot.hasContact = false + bot.tick = 0 + bot.currentGun = -1 + bot.currentTargetId = -1 + bot.targetSwitchTick = 0 + # Defensive fallback: if onGameStarted hasn't run, use live enemy count from API + if bot.initialEnemyCount == 0: + bot.initialEnemyCount = getEnemyCount() + bot.enemyCount = bot.initialEnemyCount + # ponytail: default to melee until confirmed 1v1 via scans, avoids stale enemyCount issue + bot.radarMode = 1 # Start in melee_scan; will switch to radar_lock (0) if tracker confirms 1v1 bot.moveTracker.resetRound(getX(), getY(), getDirection(), getSpeed()) bot.currentGun = 0 + # Apply radar colors for initial mode + if bot.radarMode == 0: + setRadarColor("#004444") + setScanColor("#0D4D4D") + else: + setRadarColor("#443300") + setScanColor("#4D3D0D") bot.printConfig("all") # tracker fitness persists across rounds (rolling window carries over) +method onBotDeath*(bot: ModularBot, e: BotDeathEvent) = + bot.enemyTracker.markDead(e.victimId) + if bot.enemyCount > 0: + dec bot.enemyCount + method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) = - discard + # minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1 + bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1 + +proc shouldSwitchTarget(bot: ModularBot, candidateId: int): bool = + ## Hysteresis: only switch when there is a clear reason. + if bot.currentTargetId < 0: return true + let cur = bot.enemyTracker.enemies.getOrDefault(bot.currentTargetId) + if not cur.alive: return true + if bot.tick - cur.lastSeenTick >= 20: return true # stale + if bot.tick - bot.targetSwitchTick < 10: return false # cooldown + if candidateId == bot.currentTargetId: return false + let cand = bot.enemyTracker.enemies.getOrDefault(candidateId) + let sx = getX(); let sy = getY() + let curDist = hypot(cur.x - sx, cur.y - sy) + let candDist = hypot(cand.x - sx, cand.y - sy) + if candDist < curDist * 0.7: return true # significantly closer + if cand.energy < 10.0 and cand.energy < getEnergy(): return true # finisher opportunity + return false 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 + if not bot.hasContact: setTargetSpeed(0.0) setTurnRate(0.0) @@ -286,9 +203,177 @@ method run*(bot: ModularBot) = let (spd, tr) = bot.mover.computeMove(bot.lastState) setTargetSpeed(spd) setTurnRate(tr) - setRadarTurnRate(bot.radar.computeScan(bot.lastState)) + + # Auto-switch radar based on live enemy count from tracker (ground truth from scans) + let liveEnemyCount = bot.enemyTracker.allAlive().len + let targetMode = if liveEnemyCount == 1: 0 else: 1 + if targetMode != bot.radarMode: + bot.radarMode = targetMode + if bot.radarMode == 0: + bot.radar.init() # re-lock onto survivor + setRadarColor("#004444") + setScanColor("#0D4D4D") + else: + setRadarColor("#443300") + setScanColor("#4D3D0D") + bot.printConfig("radar") + + let radarRate = if bot.radarMode == 0: + bot.radar.computeScan(bot.lastState) + else: + bot.meleeScan.computeScan(bot.lastState) + setRadarTurnRate(radarRate) go() + # --- 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") + + # --- 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] + var tmPreds: array[len(PowerBins), GunPrediction] + var circPreds: array[len(PowerBins), GunPrediction] + var gfPreds: array[len(PowerBins), GunPrediction] + var pmPreds: array[len(PowerBins), GunPrediction] + var asPreds: array[len(PowerBins), GunPrediction] + var wbPreds: array[len(PowerBins), GunPrediction] + var acPreds: array[len(PowerBins), GunPrediction] + var ssPreds: array[len(PowerBins), GunPrediction] + var dsPreds: array[len(PowerBins), GunPrediction] + var alPreds: array[len(PowerBins), GunPrediction] + var dgPreds: array[len(PowerBins), GunPrediction] + var knnPreds: array[len(PowerBins), GunPrediction] + for i in 0.. 0: bot.virtualHits.float / total.float * 100.0 else: 0.0 + echo fmt"[vbullet] gun={gunId} bin={binIdx} miss={fe.missDistance:.1f}px hit={fe.hit} | total hits={bot.virtualHits}/{total} ({pct:.1f}%)" + ) + + # Gun selection + fire + 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") + of 2: setTurretColor("#9933FF"); setBulletColor("#CC66FF") + of 3: setTurretColor("#33CC33"); setBulletColor("#66FF66") + of 4: setTurretColor("#FFCC00"); setBulletColor("#FFE066") + of 5: setTurretColor("#FF6600"); setBulletColor("#FF9944") + of 6: setTurretColor("#00CCFF"); setBulletColor("#66DDFF") + of 7: setTurretColor("#CCCCCC"); setBulletColor("#EEEEEE") + of 8: setTurretColor("#FF00FF"); setBulletColor("#FF66FF") + of 9: setTurretColor("#990000"); setBulletColor("#CC3333") + of 10: setTurretColor("#006600"); setBulletColor("#009900") + of 11: setTurretColor("#996633"); setBulletColor("#CC9966") + of 12: setTurretColor("#008888"); setBulletColor("#00AAAA") + of 13: setTurretColor("#CC00CC"); setBulletColor("#FF44FF") + else: discard + + let pred = case selectedGun + of 1: bot.linear.predict(bot.lastState, bulletSpeed(power)) + of 2: bot.tsetlin.predict(bot.lastState, bulletSpeed(power)) + of 3: bot.circular.predict(bot.lastState, bulletSpeed(power)) + of 4: bot.guessFactor.predict(bot.lastState, bulletSpeed(power)) + of 5: bot.patternMatcher.predict(bot.lastState, bulletSpeed(power)) + of 6: bot.antiSurfer.predict(bot.lastState, bulletSpeed(power)) + of 7: bot.wallBounce.predict(bot.lastState, bulletSpeed(power)) + of 8: bot.accelGun.predict(bot.lastState, bulletSpeed(power)) + of 9: bot.stopShot.predict(bot.lastState, bulletSpeed(power)) + of 10: bot.displace.predict(bot.lastState, bulletSpeed(power)) + of 11: bot.avgLead.predict(bot.lastState, bulletSpeed(power)) + of 12: bot.decayGF.predict(bot.lastState, bulletSpeed(power)) + of 13: bot.knnGun.predict(bot.lastState, bulletSpeed(power)) + else: bot.headOn.predict(bot.lastState, bulletSpeed(power)) + let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y) + + let gunDir = getGunDirection() + let gunHeat = getGunHeat() + let gunDelta = (aimTarget - gunDir) mod 360.0 + var normDelta = gunDelta + if normDelta > 180.0: normDelta -= 360.0 + elif normDelta < -180.0: normDelta += 360.0 + + if shouldFire(gunDir, aimTarget, gunHeat): + discard setFire(power) + + setGunTurnRate(normDelta) + when isMainModule: var bot = ModularBot( tracker: vb.initTracker(14), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: AntiSurf, 7: WallBounce, 8: Accel, 9: StopShot, 10: Displace, 11: AvgLead, 12: DecayGF, 13: KNN @@ -307,6 +392,7 @@ when isMainModule: decayGF: initDecayGFGun(), knnGun: initKNNGun(), radar: RadarLockModule(), + meleeScan: initMeleeScan(), mover: initPhantomMeteor(), moveTracker: mvb.initVirtualBodyTracker(1), currentGun: -1, diff --git a/common_libs/gun_harness/selector.nim b/common_libs/gun_harness/selector.nim index 77217b0..9d10489 100644 --- a/common_libs/gun_harness/selector.nim +++ b/common_libs/gun_harness/selector.nim @@ -19,8 +19,9 @@ proc shouldFire*(currentGunDir, targetAngle, gunHeat: float): bool = elif delta < -180.0: delta += 360.0 abs(delta) <= AimThresholdDeg and gunHeat <= 0.0 -proc selectShot*(t: VirtualTracker): (GunId, int, float) = +proc selectShot*(t: VirtualTracker, targetId: int = -1): (GunId, int, float) = ## Returns (gunId, powerBinIdx, power) — the shot to take this tick. - let gunId = t.bestGun() - let (binIdx, power) = t.bestPower(gunId) + ## Pass targetId to pick the best gun for that specific enemy. + let gunId = t.bestGun(targetId) + let (binIdx, power) = t.bestPower(gunId, targetId) result = (gunId, binIdx, power) diff --git a/common_libs/gun_harness/virtual_bullets.nim b/common_libs/gun_harness/virtual_bullets.nim index 389224b..81842d3 100644 --- a/common_libs/gun_harness/virtual_bullets.nim +++ b/common_libs/gun_harness/virtual_bullets.nim @@ -4,6 +4,7 @@ ## Calls onResult() on the owning gun when a bullet resolves. import std/math +import std/tables import gun_interface const @@ -19,6 +20,7 @@ type VirtualBullet* = object gunId*: GunId powerBin*: int ## index into PowerBins + targetId*: int ## enemy bot ID this bullet was aimed at fireX*, fireY*: float aimX*, aimY*: float ## predicted target (absolute) bulletSpeed*: float @@ -36,12 +38,13 @@ type bins*: array[len(PowerBins), FitnessWindow] VirtualTracker* = object - bullets*: array[MaxBullets, VirtualBullet] - head*: int ## ring buffer head - fitness*: seq[GunFitness] ## indexed by GunId + bullets*: array[MaxBullets, VirtualBullet] + head*: int ## ring buffer head + numGuns*: int + fitness*: Table[int, seq[GunFitness]] ## keyed by enemy bot ID, indexed by GunId proc initTracker*(numGuns: int): VirtualTracker = - result.fitness = newSeq[GunFitness](numGuns) + result.numGuns = numGuns proc hitRate*(fw: FitnessWindow): float = ## Returns fraction of hits in the rolling window. 0.0 when no data. @@ -58,8 +61,11 @@ proc record(fw: var FitnessWindow, hit: bool) = proc spawnBullets*(t: var VirtualTracker, gunId: GunId, predictions: array[len(PowerBins), GunPrediction], - state: WorldState) = + state: WorldState, targetId: int) = ## Call once per gun per tick with predictions for all power bins. + ## Lazily creates fitness entry for targetId on first spawn. + if targetId notin t.fitness: + t.fitness[targetId] = newSeq[GunFitness](t.numGuns) for binIdx in 0.. StaleTicks). for i in 0.. StaleTicks: + b.active = false + continue + ex = e.x; ey = e.y + else: + # No data for this target — fall back to selected enemy in state + ex = state.enemyX; ey = state.enemyY + let dx = b.aimX - b.fireX let dy = b.aimY - b.fireY let dist = hypot(dx, dy) @@ -98,15 +120,16 @@ proc tickBullets*(t: var VirtualTracker, state: WorldState, if dist < 1e-6: (b.aimX, b.aimY) else: (b.fireX + dx / dist * b.travelDist, b.fireY + dy / dist * b.travelDist) - let missDist = hypot(bx - state.enemyX, by - state.enemyY) + let missDist = hypot(bx - ex, by - ey) let hit = missDist < BotRadius - t.fitness[b.gunId].bins[b.powerBin].record(hit) + if b.targetId in t.fitness: + t.fitness[b.targetId][b.gunId].bins[b.powerBin].record(hit) let fe = FeedbackEvent( prediction: GunPrediction(x: b.aimX, y: b.aimY), - actualX: state.enemyX, - actualY: state.enemyY, + actualX: ex, + actualY: ey, bulletPower: PowerBins[b.powerBin], missDistance: missDist, hit: hit, @@ -114,41 +137,59 @@ proc tickBullets*(t: var VirtualTracker, state: WorldState, onResolved(b.gunId, b.powerBin, fe) b.active = false -proc bestPower*(t: VirtualTracker, gunId: GunId): (int, float) = +proc fitnessFor(t: VirtualTracker, targetId: int): seq[GunFitness] = + ## Returns fitness seq for targetId, or merges all enemies as fallback. + ## ponytail: merge is O(enemies*guns*bins), fine for small counts + if targetId >= 0 and targetId in t.fitness: + return t.fitness[targetId] + # Aggregate across all enemies + result = newSeq[GunFitness](t.numGuns) + for perEnemy in t.fitness.values: + for gunId in 0..= MinHitRate. ## Falls back to lowest power bin if nothing qualifies yet. + ## Uses per-enemy fitness when targetId >= 0 and data exists; else aggregate. + let fit = t.fitnessFor(targetId) result = (0, PowerBins[0]) for binIdx in countdown(len(PowerBins) - 1, 0): - let rate = t.fitness[gunId].bins[binIdx].hitRate() - if rate >= MinHitRate or t.fitness[gunId].bins[binIdx].count == 0: + let rate = fit[gunId].bins[binIdx].hitRate() + if rate >= MinHitRate or fit[gunId].bins[binIdx].count == 0: return (binIdx, PowerBins[binIdx]) -proc bestGun*(t: VirtualTracker): GunId = +proc bestGun*(t: VirtualTracker, targetId: int = -1): GunId = ## Pick gun with highest hit rate across all power bins. - ## Guns with fewer than MinObsBeforeCompete observations across all bins are - ## skipped unless every gun is below threshold (then fall back to best of all). + ## Guns with fewer than MinObsBeforeCompete observations are skipped + ## unless every gun is below threshold (then fall back to best of all). + ## Uses per-enemy fitness when targetId >= 0 and data exists; else aggregate. ## ponytail: O(n*bins), fine for small gun counts - proc bestAmong(t: VirtualTracker, requireMin: bool): GunId = + let fit = t.fitnessFor(targetId) + proc bestAmong(fit: seq[GunFitness], requireMin: bool): GunId = var bestRate = -1.0 result = 0 - for gunId in 0.. bestRate: bestRate = r result = gunId - # Try gated selection first; fall back to ungated if nothing qualifies var anyQualifies = false - for gunId in 0..= MinObsBeforeCompete: + if fit[gunId].bins[binIdx].count >= MinObsBeforeCompete: anyQualifies = true break if anyQualifies: break - result = if anyQualifies: t.bestAmong(true) else: t.bestAmong(false) + result = if anyQualifies: fit.bestAmong(true) else: fit.bestAmong(false) diff --git a/common_libs/targeting/enemy_tracker.nim b/common_libs/targeting/enemy_tracker.nim new file mode 100644 index 0000000..619b708 --- /dev/null +++ b/common_libs/targeting/enemy_tracker.nim @@ -0,0 +1,54 @@ +## Per-enemy state table. Keyed by bot ID. +## Tank Royale: 0° = East. + +import std/tables +import std/math + +type + EnemyState* = object + id*: int + x*, y*: float + heading*: float ## degrees + speed*: float + energy*: float + lastSeenTick*: int + alive*: bool + + EnemyTracker* = object + enemies*: Table[int, EnemyState] + +proc update*(et: var EnemyTracker, botId: int, x, y, heading, speed, energy: float, tick: int) = + et.enemies[botId] = EnemyState(id: botId, x: x, y: y, heading: heading, + speed: speed, energy: energy, + lastSeenTick: tick, alive: true) + +proc markDead*(et: var EnemyTracker, botId: int) = + if botId in et.enemies: + et.enemies[botId].alive = false + +proc resetRound*(et: var EnemyTracker) = + et.enemies.clear() + +proc getEnemy*(et: EnemyTracker, botId: int): EnemyState = + et.enemies[botId] + +proc allAlive*(et: EnemyTracker): seq[EnemyState] = + for s in et.enemies.values: + if s.alive: result.add(s) + +proc closestTo*(et: EnemyTracker, x, y: float): EnemyState = + var bestDist = Inf + for s in et.enemies.values: + if not s.alive: continue + let d = hypot(s.x - x, s.y - y) + if d < bestDist: + bestDist = d + result = s + +proc weakest*(et: EnemyTracker): EnemyState = + var bestEnergy = Inf + for s in et.enemies.values: + if not s.alive: continue + if s.energy < bestEnergy: + bestEnergy = s.energy + result = s diff --git a/common_libs/targeting/target_selector.nim b/common_libs/targeting/target_selector.nim new file mode 100644 index 0000000..1146a50 --- /dev/null +++ b/common_libs/targeting/target_selector.nim @@ -0,0 +1,19 @@ +## Target selector: picks which enemy to aim at. + +import enemy_tracker + +type TargetMode* = enum + tmClosest, ## nearest enemy + tmWeakest, ## lowest energy (finisher) + tmBestGun ## future — alias to closest for now + +proc selectTarget*(et: EnemyTracker, selfX, selfY: float, + mode: TargetMode = tmClosest): int = + ## Returns enemy bot ID. Falls back to 1v1 naturally when 1 alive. + let alive = et.allAlive() + if alive.len == 0: return -1 + if alive.len == 1: return alive[0].id + let chosen = case mode + of tmWeakest: et.weakest() + else: et.closestTo(selfX, selfY) # tmClosest + tmBestGun + result = chosen.id diff --git a/common_libs/test_framework/adversaries/PatternMover/PatternMover b/common_libs/test_framework/adversaries/PatternMover/PatternMover index d93b3f8..d7b7b35 100755 Binary files a/common_libs/test_framework/adversaries/PatternMover/PatternMover and b/common_libs/test_framework/adversaries/PatternMover/PatternMover differ diff --git a/common_libs/test_framework/adversaries/RandomMover/RandomMover b/common_libs/test_framework/adversaries/RandomMover/RandomMover index 66ff8f4..ab3f2da 100755 Binary files a/common_libs/test_framework/adversaries/RandomMover/RandomMover and b/common_libs/test_framework/adversaries/RandomMover/RandomMover differ diff --git a/common_libs/test_framework/adversaries/WaveSurfer/WaveSurfer b/common_libs/test_framework/adversaries/WaveSurfer/WaveSurfer index 6d14b06..a457a83 100755 Binary files a/common_libs/test_framework/adversaries/WaveSurfer/WaveSurfer and b/common_libs/test_framework/adversaries/WaveSurfer/WaveSurfer differ