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:
Binary file not shown.
@@ -27,6 +27,7 @@ import guns/decay_gf
|
|||||||
import guns/knn_gun
|
import guns/knn_gun
|
||||||
import movements/phantom_meteor
|
import movements/phantom_meteor
|
||||||
import movements/rammer
|
import movements/rammer
|
||||||
|
import movements/the_floor_is_lava
|
||||||
import movement_harness/virtual_bodies as mvb
|
import movement_harness/virtual_bodies as mvb
|
||||||
import movement_harness/bullet_shadows
|
import movement_harness/bullet_shadows
|
||||||
import targeting/enemy_tracker
|
import targeting/enemy_tracker
|
||||||
@@ -71,9 +72,12 @@ type
|
|||||||
avgLead: AveragedLeadGun
|
avgLead: AveragedLeadGun
|
||||||
decayGF: DecayGFGun
|
decayGF: DecayGFGun
|
||||||
knnGun: KNNGun
|
knnGun: KNNGun
|
||||||
mover: PhantomMeteorModule
|
mover: TFILModule
|
||||||
rammer: RammerModule
|
rammer: RammerModule
|
||||||
isRamming: bool
|
isRamming: bool
|
||||||
|
ramStuckTicks: int
|
||||||
|
ramDurationTicks: int
|
||||||
|
ramCooldownTicks: int
|
||||||
moveTracker: VirtualBodyTracker
|
moveTracker: VirtualBodyTracker
|
||||||
virtualHits: int
|
virtualHits: int
|
||||||
virtualMiss: int
|
virtualMiss: int
|
||||||
@@ -82,14 +86,26 @@ type
|
|||||||
currentTargetId: int
|
currentTargetId: int
|
||||||
targetSwitchTick: int
|
targetSwitchTick: int
|
||||||
enemyTracker: EnemyTracker
|
enemyTracker: EnemyTracker
|
||||||
|
prevGun: int
|
||||||
|
prevRadar: int
|
||||||
|
prevTarget: int
|
||||||
|
|
||||||
proc printConfig(bot: ModularBot, changed: string = "") =
|
proc printConfig(bot: ModularBot, changed: string = "") =
|
||||||
let gunName = GunNames[bot.currentGun]
|
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 radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan"
|
let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan"
|
||||||
let moveName = if bot.isRamming: "rammer" else: "phantom_meteor"
|
let moveName = if bot.isRamming: "rammer" else: "tfil"
|
||||||
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 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 =
|
proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): WorldState =
|
||||||
var ei: seq[EnemyInfo]
|
var ei: seq[EnemyInfo]
|
||||||
@@ -128,9 +144,11 @@ method onBulletHitBot*(bot: ModularBot, e: BulletHitBotEvent) =
|
|||||||
|
|
||||||
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
|
||||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
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) =
|
method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) =
|
||||||
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
|
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) =
|
method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
|
||||||
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
|
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.mover.resetRound()
|
||||||
bot.enemyTracker.resetRound()
|
bot.enemyTracker.resetRound()
|
||||||
bot.isRamming = false
|
bot.isRamming = false
|
||||||
|
bot.ramStuckTicks = 0
|
||||||
|
bot.ramDurationTicks = 0
|
||||||
|
bot.ramCooldownTicks = 0
|
||||||
bot.hasContact = false
|
bot.hasContact = false
|
||||||
bot.tick = 0
|
bot.tick = 0
|
||||||
bot.currentGun = -1
|
bot.currentGun = -1
|
||||||
@@ -171,11 +192,15 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
|||||||
# tracker fitness persists across rounds (rolling window carries over)
|
# tracker fitness persists across rounds (rolling window carries over)
|
||||||
|
|
||||||
method onBotDeath*(bot: ModularBot, e: BotDeathEvent) =
|
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)
|
bot.enemyTracker.markDead(e.victimId)
|
||||||
if bot.enemyCount > 0:
|
if bot.enemyCount > 0:
|
||||||
dec bot.enemyCount
|
dec bot.enemyCount
|
||||||
if e.victimId == bot.currentTargetId:
|
if e.victimId == bot.currentTargetId:
|
||||||
bot.isRamming = false
|
bot.isRamming = false
|
||||||
|
bot.currentTargetId = -1
|
||||||
|
|
||||||
method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||||
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
||||||
@@ -201,9 +226,18 @@ method run*(bot: ModularBot) =
|
|||||||
while isRunning():
|
while isRunning():
|
||||||
inc bot.tick
|
inc bot.tick
|
||||||
|
|
||||||
# Debug: print enemy state on first tick
|
# Ram cooldown countdown
|
||||||
if bot.tick == 1:
|
if bot.ramCooldownTicks > 0:
|
||||||
echo "[debug] enemyCount=" & $bot.enemyCount & " tracker=" & $bot.enemyTracker.allAlive().len & " radarMode=" & $bot.radarMode
|
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:
|
if not bot.hasContact:
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
@@ -212,25 +246,65 @@ method run*(bot: ModularBot) =
|
|||||||
go()
|
go()
|
||||||
continue
|
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 ws = bot.lastState
|
||||||
let ramDist = hypot(ws.enemyX - ws.selfX, ws.enemyY - ws.selfY)
|
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 ramFinisher = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and 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 ramOpportunity = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ramDist < 50.0 and ws.selfEnergy > ws.enemyEnergy + 30.0
|
||||||
let ramDesperation = ws.selfEnergy < 5.0 and ws.enemyEnergy < 5.0 and ramDist < 150.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 = ramFinisher or ramOpportunity or ramDesperation
|
let shouldRam = bot.currentTargetId >= 0 and
|
||||||
|
(ramFinisher or ramOpportunity or ramDesperation)
|
||||||
|
|
||||||
if shouldRam and not bot.isRamming:
|
if shouldRam and not bot.isRamming:
|
||||||
bot.isRamming = true
|
bot.isRamming = true
|
||||||
let tag = if ramDesperation: "desperation" elif ramFinisher: "finisher" else: "opportunity"
|
bot.ramStuckTicks = 0
|
||||||
echo CLR_MOVE & "[ram:" & tag & "] dist=" & $int(ramDist) &
|
bot.ramDurationTicks = 0
|
||||||
" selfE=" & $int(ws.selfEnergy) & " enemyE=" & $int(ws.enemyEnergy) & CLR_RST
|
if ramFinisher:
|
||||||
bot.printConfig("move")
|
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:
|
elif not shouldRam and bot.isRamming:
|
||||||
bot.isRamming = false
|
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:
|
let (spd, tr) = if shouldRam:
|
||||||
|
bot.mover.clearGraphics()
|
||||||
bot.rammer.computeMove(ws)
|
bot.rammer.computeMove(ws)
|
||||||
else:
|
else:
|
||||||
bot.mover.computeMove(ws)
|
bot.mover.computeMove(ws)
|
||||||
@@ -258,23 +332,9 @@ method run*(bot: ModularBot) =
|
|||||||
setRadarTurnRate(radarRate)
|
setRadarTurnRate(radarRate)
|
||||||
go()
|
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) ---
|
# --- Aim + fire from tracked state (not scan event) ---
|
||||||
let tid = bot.currentTargetId
|
|
||||||
if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and
|
if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and
|
||||||
bot.enemyTracker.enemies[tid].alive:
|
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
|
# Spawn virtual bullets for all guns
|
||||||
var headsUp: array[len(PowerBins), GunPrediction]
|
var headsUp: array[len(PowerBins), GunPrediction]
|
||||||
var linPreds: 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)
|
let (selectedGun, _, power) = selectShot(bot.tracker, tid)
|
||||||
if selectedGun != bot.currentGun:
|
if selectedGun != bot.currentGun:
|
||||||
bot.currentGun = selectedGun
|
bot.currentGun = selectedGun
|
||||||
bot.printConfig("gun")
|
|
||||||
case selectedGun
|
case selectedGun
|
||||||
of 0: setTurretColor("#FF3333"); setBulletColor("#FF6666")
|
of 0: setTurretColor("#FF3333"); setBulletColor("#FF6666")
|
||||||
of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF")
|
of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF")
|
||||||
@@ -427,7 +486,7 @@ when isMainModule:
|
|||||||
knnGun: initKNNGun(),
|
knnGun: initKNNGun(),
|
||||||
radar: RadarLockModule(),
|
radar: RadarLockModule(),
|
||||||
meleeScan: initMeleeScan(),
|
meleeScan: initMeleeScan(),
|
||||||
mover: initPhantomMeteor(),
|
mover: TFILModule(debugGraphics: true),
|
||||||
rammer: initRammer(),
|
rammer: initRammer(),
|
||||||
moveTracker: mvb.initVirtualBodyTracker(1),
|
moveTracker: mvb.initVirtualBodyTracker(1),
|
||||||
currentGun: -1,
|
currentGun: -1,
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ import gun_harness/gun_interface
|
|||||||
const MaxSpeed = 8.0 ## TR bot max speed (px/tick)
|
const MaxSpeed = 8.0 ## TR bot max speed (px/tick)
|
||||||
|
|
||||||
type AccelGun* = object
|
type AccelGun* = object
|
||||||
prevSpeed: float
|
prevSpeed: float
|
||||||
prevHeading: float
|
prevHeading: float
|
||||||
prevPrevSpeed: float
|
prevPrevSpeed: float
|
||||||
prevTick: int
|
prevTick: int
|
||||||
frames: int ## how many observations we have
|
frames: int ## how many observations we have
|
||||||
cachedAccel: float ## accel (px/tick²) cached per tick
|
cachedAccel: float ## accel (px/tick²) cached per tick
|
||||||
cachedTurnRate: float ## turn rate (deg/tick) cached per tick
|
cachedTurnRate: float ## turn rate (deg/tick) cached per tick
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initAccelGun*(): AccelGun =
|
proc initAccelGun*(): AccelGun =
|
||||||
AccelGun()
|
AccelGun(debugGraphics: false)
|
||||||
|
|
||||||
proc predict*(g: var AccelGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var AccelGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
if bulletSpeed <= 0.0:
|
if bulletSpeed <= 0.0:
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ type
|
|||||||
waves: seq[ASWave]
|
waves: seq[ASWave]
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedWaveStored: bool
|
cachedWaveStored: bool
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initAntiSurferGun*(): AntiSurferGun =
|
proc initAntiSurferGun*(): AntiSurferGun =
|
||||||
result.cachedTick = -1
|
result.cachedTick = -1
|
||||||
|
result.debugGraphics = false
|
||||||
# Same triangular head-on prior as GFGun — cold-start aims head-on.
|
# Same triangular head-on prior as GFGun — cold-start aims head-on.
|
||||||
let center = (ASBins - 1) div 2 # = 15
|
let center = (ASBins - 1) div 2 # = 15
|
||||||
for i in 0..<ASBins:
|
for i in 0..<ASBins:
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import guns/circular
|
|||||||
import guns/wall_bounce
|
import guns/wall_bounce
|
||||||
|
|
||||||
type AveragedLeadGun* = object
|
type AveragedLeadGun* = object
|
||||||
linear: LinearGun
|
linear: LinearGun
|
||||||
circular: CircularGun
|
circular: CircularGun
|
||||||
wallBounce: WallBounceGun
|
wallBounce: WallBounceGun
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedPred: GunPrediction # per-tick cache; ponytail: single cache, extend if multi-power needed
|
cachedPred: GunPrediction # per-tick cache; ponytail: single cache, extend if multi-power needed
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initAveragedLeadGun*(): AveragedLeadGun =
|
proc initAveragedLeadGun*(): AveragedLeadGun =
|
||||||
AveragedLeadGun(wallBounce: initWallBounceGun())
|
AveragedLeadGun(wallBounce: initWallBounceGun(), debugGraphics: false)
|
||||||
|
|
||||||
proc predict*(g: var AveragedLeadGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var AveragedLeadGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
if state.tick == g.cachedTick:
|
if state.tick == g.cachedTick:
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import std/math
|
|||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
|
|
||||||
type CircularGun* = object
|
type CircularGun* = object
|
||||||
prevHeading: float ## enemy heading from the tick before current
|
prevHeading: float ## enemy heading from the tick before current
|
||||||
prevTick: int ## tick of prevHeading observation
|
prevTick: int ## tick of prevHeading observation
|
||||||
hasPrev: bool
|
hasPrev: bool
|
||||||
cachedOmega: float ## omega (rad/tick) computed on first call this tick
|
cachedOmega: float ## omega (rad/tick) computed on first call this tick
|
||||||
cachedTick: int ## tick for which cachedOmega was computed
|
cachedTick: int ## tick for which cachedOmega was computed
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc predict*(g: var CircularGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var CircularGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
if bulletSpeed <= 0.0:
|
if bulletSpeed <= 0.0:
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ type
|
|||||||
waves: seq[DWave]
|
waves: seq[DWave]
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedWaveStored: bool
|
cachedWaveStored: bool
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initDecayGFGun*(): DecayGFGun =
|
proc initDecayGFGun*(): DecayGFGun =
|
||||||
result.cachedTick = -1
|
result.cachedTick = -1
|
||||||
|
result.debugGraphics = false
|
||||||
let center = (GFBins - 1) div 2
|
let center = (GFBins - 1) div 2
|
||||||
for i in 0..<GFBins:
|
for i in 0..<GFBins:
|
||||||
let d = abs(i - center)
|
let d = abs(i - center)
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ const WindowSize = 15 # ticks; N+1 frames stored
|
|||||||
|
|
||||||
type
|
type
|
||||||
DisplacementGun* = object
|
DisplacementGun* = object
|
||||||
posX: array[WindowSize + 1, float]
|
posX: array[WindowSize + 1, float]
|
||||||
posY: array[WindowSize + 1, float]
|
posY: array[WindowSize + 1, float]
|
||||||
count: int # frames collected so far
|
count: int # frames collected so far
|
||||||
head: int # ring-buffer head
|
head: int # ring-buffer head
|
||||||
lastTick: int # for per-tick cache
|
lastTick: int # for per-tick cache
|
||||||
cacheSpeed: float
|
cacheSpeed: float
|
||||||
cachePred: GunPrediction
|
cachePred: GunPrediction
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc predict*(g: var DisplacementGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var DisplacementGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
# Per-tick cache: same tick + same speed => same prediction
|
# Per-tick cache: same tick + same speed => same prediction
|
||||||
@@ -62,4 +63,4 @@ proc onResult*(g: var DisplacementGun, e: FeedbackEvent) =
|
|||||||
discard # analytical gun — no learning
|
discard # analytical gun — no learning
|
||||||
|
|
||||||
proc initDisplacementGun*(): DisplacementGun =
|
proc initDisplacementGun*(): DisplacementGun =
|
||||||
DisplacementGun(count: 0, head: 0, lastTick: -1)
|
DisplacementGun(count: 0, head: 0, lastTick: -1, debugGraphics: false)
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ type
|
|||||||
# mea not stored — recomputed from FeedbackEvent.bulletPower at resolution time
|
# mea not stored — recomputed from FeedbackEvent.bulletPower at resolution time
|
||||||
|
|
||||||
GFGun* = object
|
GFGun* = object
|
||||||
bins: array[GFBins, float]
|
bins: array[GFBins, float]
|
||||||
waves: seq[Wave] # pending unresolved waves
|
waves: seq[Wave] # pending unresolved waves
|
||||||
# per-tick cache: store wave only once across multiple power-bin calls
|
# per-tick cache: store wave only once across multiple power-bin calls
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedWaveStored: bool
|
cachedWaveStored: bool
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initGFGun*(): GFGun =
|
proc initGFGun*(): GFGun =
|
||||||
result.cachedTick = -1
|
result.cachedTick = -1
|
||||||
|
result.debugGraphics = false
|
||||||
# Seed with a head-on prior: triangular bump at bin 15 (GF=0).
|
# Seed with a head-on prior: triangular bump at bin 15 (GF=0).
|
||||||
# Prevents the cold-start tie-break to GF=-1 (bin 0) that poisons early fitness.
|
# Prevents the cold-start tie-break to GF=-1 (bin 0) that poisons early fitness.
|
||||||
let center = (GFBins - 1) div 2 # = 15
|
let center = (GFBins - 1) div 2 # = 15
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
|
|
||||||
type HeadOnGun* = object
|
type HeadOnGun* = object
|
||||||
discard
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc predict*(g: var HeadOnGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var HeadOnGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
GunPrediction(x: state.enemyX, y: state.enemyY)
|
GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||||
|
|||||||
@@ -23,23 +23,25 @@ type
|
|||||||
feat: array[7, float]
|
feat: array[7, float]
|
||||||
|
|
||||||
KNNGun* = object
|
KNNGun* = object
|
||||||
obs: seq[Obs]
|
obs: seq[Obs]
|
||||||
obsHead: int # ring-buffer write index
|
obsHead: int # ring-buffer write index
|
||||||
waves: seq[KNNWave]
|
waves: seq[KNNWave]
|
||||||
# per-tick cache
|
# per-tick cache
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedWaveStored: bool
|
cachedWaveStored: bool
|
||||||
# rolling normalization ranges
|
# rolling normalization ranges
|
||||||
featMin: array[7, float]
|
featMin: array[7, float]
|
||||||
featMax: array[7, float]
|
featMax: array[7, float]
|
||||||
# state for feature extraction
|
# state for feature extraction
|
||||||
lastSpeed: float
|
lastSpeed: float
|
||||||
lastDirection: float # +1 or -1
|
lastDirection: float # +1 or -1
|
||||||
timeSinceDirChange: int
|
timeSinceDirChange: int
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initKNNGun*(): KNNGun =
|
proc initKNNGun*(): KNNGun =
|
||||||
result.cachedTick = -1
|
result.cachedTick = -1
|
||||||
result.lastDirection = 1.0
|
result.lastDirection = 1.0
|
||||||
|
result.debugGraphics = false
|
||||||
for i in 0..6:
|
for i in 0..6:
|
||||||
result.featMin[i] = 1e18
|
result.featMin[i] = 1e18
|
||||||
result.featMax[i] = -1e18
|
result.featMax[i] = -1e18
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import std/math
|
|||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
|
|
||||||
type LinearGun* = object
|
type LinearGun* = object
|
||||||
discard
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc predict*(g: var LinearGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var LinearGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||||
|
|||||||
@@ -16,18 +16,19 @@ type
|
|||||||
headingDelta: float ## heading change in radians this tick
|
headingDelta: float ## heading change in radians this tick
|
||||||
|
|
||||||
PatternMatcherGun* = object
|
PatternMatcherGun* = object
|
||||||
buf: array[HistorySize, MoveTick]
|
buf: array[HistorySize, MoveTick]
|
||||||
head: int ## next write index (circular)
|
head: int ## next write index (circular)
|
||||||
count: int ## filled entries (capped at HistorySize)
|
count: int ## filled entries (capped at HistorySize)
|
||||||
prevHeading: float
|
prevHeading: float
|
||||||
prevSpeed: float
|
prevSpeed: float
|
||||||
prevTick: int
|
prevTick: int
|
||||||
hasPrev: bool
|
hasPrev: bool
|
||||||
# per-tick cache — avoid re-searching for multiple power bins
|
# per-tick cache — avoid re-searching for multiple power bins
|
||||||
cacheTick: int
|
cacheTick: int
|
||||||
cacheX: float
|
cacheX: float
|
||||||
cacheY: float
|
cacheY: float
|
||||||
cacheValid: bool
|
cacheValid: bool
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
# --- circular buffer helpers ---
|
# --- circular buffer helpers ---
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ const BrakeDecel = 2.0 ## same-direction stop
|
|||||||
const CoastDecel = 1.0 ## cross-zero coasting stop
|
const CoastDecel = 1.0 ## cross-zero coasting stop
|
||||||
|
|
||||||
type StopShotGun* = object
|
type StopShotGun* = object
|
||||||
prevSpeed: float
|
prevSpeed: float
|
||||||
prevHeading: float
|
prevHeading: float
|
||||||
prevTick: int
|
prevTick: int
|
||||||
frames: int
|
frames: int
|
||||||
cachedTick: int
|
cachedTick: int
|
||||||
cachedPredX: float
|
cachedPredX: float
|
||||||
cachedPredY: float
|
cachedPredY: float
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initStopShotGun*(): StopShotGun = StopShotGun()
|
proc initStopShotGun*(): StopShotGun = StopShotGun(debugGraphics: false)
|
||||||
|
|
||||||
proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||||
if bulletSpeed <= 0.0:
|
if bulletSpeed <= 0.0:
|
||||||
|
|||||||
@@ -200,17 +200,19 @@ type
|
|||||||
alive: bool
|
alive: bool
|
||||||
|
|
||||||
TsetlinGun* = object
|
TsetlinGun* = object
|
||||||
net: TmNet
|
net: TmNet
|
||||||
frameBuffer: array[TM_WINDOW_SIZE, TmFrameEncoded]
|
frameBuffer: array[TM_WINDOW_SIZE, TmFrameEncoded]
|
||||||
bufferCount: int
|
bufferCount: int
|
||||||
traces: array[TM_TRACE_SLOTS, TmTrace]
|
traces: array[TM_TRACE_SLOTS, TmTrace]
|
||||||
traceHead: int
|
traceHead: int
|
||||||
shotCount: int ## total onResult calls received
|
shotCount: int ## total onResult calls received
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initTsetlinGun*(): TsetlinGun =
|
proc initTsetlinGun*(): TsetlinGun =
|
||||||
# states init at 0 (boundary); one Type I step crosses into Include
|
# states init at 0 (boundary); one Type I step crosses into Include
|
||||||
for s in result.net.states.mitems: s = 0'i16
|
for s in result.net.states.mitems: s = 0'i16
|
||||||
randomize()
|
randomize()
|
||||||
|
result.debugGraphics = false
|
||||||
|
|
||||||
proc isWarmedUp*(g: TsetlinGun): bool {.inline.} =
|
proc isWarmedUp*(g: TsetlinGun): bool {.inline.} =
|
||||||
g.bufferCount >= TM_WINDOW_SIZE
|
g.bufferCount >= TM_WINDOW_SIZE
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import std/math
|
|||||||
import gun_harness/gun_interface
|
import gun_harness/gun_interface
|
||||||
|
|
||||||
type WallBounceGun* = object
|
type WallBounceGun* = object
|
||||||
discard
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initWallBounceGun*(): WallBounceGun = WallBounceGun()
|
proc initWallBounceGun*(): WallBounceGun = WallBounceGun(debugGraphics: false)
|
||||||
|
|
||||||
proc simulateBounce(ex, ey, heading, speed: float, ticks: int,
|
proc simulateBounce(ex, ey, heading, speed: float, ticks: int,
|
||||||
arenaW, arenaH: float): (float, float) =
|
arenaW, arenaH: float): (float, float) =
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ type MinimumRiskModule* = object
|
|||||||
targetX, targetY: float
|
targetX, targetY: float
|
||||||
ticksSinceCalc: int
|
ticksSinceCalc: int
|
||||||
hasTarget: bool
|
hasTarget: bool
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initMinimumRisk*(): MinimumRiskModule =
|
proc initMinimumRisk*(): MinimumRiskModule =
|
||||||
MinimumRiskModule(hasTarget: false, ticksSinceCalc: RecalcInterval)
|
MinimumRiskModule(hasTarget: false, ticksSinceCalc: RecalcInterval, debugGraphics: false)
|
||||||
|
|
||||||
proc wallRisk(p: Vec2, w, h: float): float {.inline.} =
|
proc wallRisk(p: Vec2, w, h: float): float {.inline.} =
|
||||||
## Linear penalty that ramps up inside WallMargin.
|
## Linear penalty that ramps up inside WallMargin.
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ const
|
|||||||
WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires
|
WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires
|
||||||
|
|
||||||
type OscillatorModule* = object
|
type OscillatorModule* = object
|
||||||
sign: float ## +1 or -1, forward/backward relative to perp heading
|
sign: float ## +1 or -1, forward/backward relative to perp heading
|
||||||
elapsed: int ## ticks since last reversal
|
elapsed: int ## ticks since last reversal
|
||||||
wallLockout: int ## remaining ticks where wall-reversal is suppressed
|
wallLockout: int ## remaining ticks where wall-reversal is suppressed
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initOscillator*(): OscillatorModule =
|
proc initOscillator*(): OscillatorModule =
|
||||||
OscillatorModule(sign: 1.0, elapsed: 0, wallLockout: 0)
|
OscillatorModule(sign: 1.0, elapsed: 0, wallLockout: 0, debugGraphics: false)
|
||||||
|
|
||||||
proc computeMove*(m: var OscillatorModule, ws: WorldState): MoveCommand =
|
proc computeMove*(m: var OscillatorModule, ws: WorldState): MoveCommand =
|
||||||
inc m.elapsed
|
inc m.elapsed
|
||||||
|
|||||||
@@ -146,16 +146,17 @@ proc computeForces(eng: GravityEngine, botPos: Vec2, enemies: openArray[Vec2], a
|
|||||||
hasClose = true
|
hasClose = true
|
||||||
total = total + normalize(botPos - ph.pos) * (KBullet * (1.0 - d/250.0))
|
total = total + normalize(botPos - ph.pos) * (KBullet * (1.0 - d/250.0))
|
||||||
|
|
||||||
# Priority 2: wall escape (additive — blends with phantom forces)
|
# Priority 2: wall escape — scales as inverse-square so it dominates bullets near walls
|
||||||
let minW = min(min(botPos.x, arenaW-botPos.x), min(botPos.y, arenaH-botPos.y))
|
var ex = 0.0; var ey = 0.0
|
||||||
if minW < VeryClose:
|
let dLeft = botPos.x
|
||||||
var ex = 0.0; var ey = 0.0
|
let dRight = arenaW - botPos.x
|
||||||
if botPos.x < VeryClose: ex = 1.0
|
let dBot = botPos.y
|
||||||
if arenaW - botPos.x < VeryClose: ex = -1.0
|
let dTop = arenaH - botPos.y
|
||||||
if botPos.y < VeryClose: ey = 1.0
|
if dLeft < WallMinDist: ex += KWall / (dLeft * dLeft)
|
||||||
if arenaH - botPos.y < VeryClose: ey = -1.0
|
if dRight < WallMinDist: ex -= KWall / (dRight * dRight)
|
||||||
let wm = sqrt(ex*ex + ey*ey)
|
if dBot < WallMinDist: ey += KWall / (dBot * dBot)
|
||||||
if wm > 0.1: total = total + normalize(vec2(ex, ey)) * 500.0
|
if dTop < WallMinDist: ey -= KWall / (dTop * dTop)
|
||||||
|
total = total + vec2(ex, ey)
|
||||||
|
|
||||||
# Priority 3: repulsion from ALL enemies; attraction only toward nearest.
|
# 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
|
# 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
|
# Too far from nearest only → mild attraction
|
||||||
total = total + normalize(ep - botPos) * (KEnemy * t)
|
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
|
total
|
||||||
|
|
||||||
# ── MovementModule wrapper ────────────────────────────────────────────────────
|
# ── MovementModule wrapper ────────────────────────────────────────────────────
|
||||||
|
|
||||||
type PhantomMeteorModule* = object
|
type PhantomMeteorModule* = object
|
||||||
engine: GravityEngine
|
engine: GravityEngine
|
||||||
gunheat: GunheatTracker
|
gunheat: GunheatTracker
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initPhantomMeteor*(): PhantomMeteorModule =
|
proc initPhantomMeteor*(): PhantomMeteorModule =
|
||||||
PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker())
|
PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker(), debugGraphics: false)
|
||||||
|
|
||||||
proc resetRound*(m: var PhantomMeteorModule) =
|
proc resetRound*(m: var PhantomMeteorModule) =
|
||||||
## Clear per-round transients (phantoms, waves, energy baseline), keep histogram.
|
## 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:
|
if force.magnitude < 1e-9:
|
||||||
return (speed: 0.0, turnRate: 0.0)
|
return (speed: 0.0, turnRate: 0.0)
|
||||||
|
|
||||||
# Threat centroid (inverse-distance weighted) for perpendicular direction.
|
# Corner detection: near 2 walls simultaneously → hard escape override.
|
||||||
# With 1 enemy this equals the single enemy bearing — identical to prior behavior.
|
# ponytail: WallMinDist threshold reused; increase if bot still traps at higher speeds.
|
||||||
var cx = 0.0; var cy = 0.0; var wsum = 0.0
|
let nearL = botPos.x < WallMinDist
|
||||||
for ep in enemyVecs:
|
let nearR = ws.arenaWidth - botPos.x < WallMinDist
|
||||||
let d = max(dist(ep, botPos), 1.0)
|
let nearB = botPos.y < WallMinDist
|
||||||
let w = 1.0 / d
|
let nearT = ws.arenaHeight - botPos.y < WallMinDist
|
||||||
cx += ep.x * w; cy += ep.y * w; wsum += w
|
let wallCount = (if nearL: 1 else: 0) + (if nearR: 1 else: 0) +
|
||||||
let centroid = if wsum > 1e-9: vec2(cx/wsum, cy/wsum) else: enemyPos
|
(if nearB: 1 else: 0) + (if nearT: 1 else: 0)
|
||||||
let centroidBearingRad = arctan2(centroid.y - botPos.y, centroid.x - botPos.x)
|
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 =
|
let desiredDeg =
|
||||||
if fn.x * perpCCW.x + fn.y * perpCCW.y >= fn.x * perpCW.x + fn.y * perpCW.y:
|
if cornered:
|
||||||
radToDeg(arctan2(perpCCW.y, perpCCW.x))
|
# 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:
|
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
|
# Delta from current heading
|
||||||
var delta = desiredDeg - ws.selfHeading
|
var delta = desiredDeg - ws.selfHeading
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import movement_harness/movement_interface
|
|||||||
const MaxSpeed* = 8.0
|
const MaxSpeed* = 8.0
|
||||||
|
|
||||||
type RammerModule* = object
|
type RammerModule* = object
|
||||||
discard
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initRammer*(): RammerModule = RammerModule()
|
proc initRammer*(): RammerModule = RammerModule(debugGraphics: false)
|
||||||
|
|
||||||
proc computeMove*(m: var RammerModule, ws: WorldState): MoveCommand =
|
proc computeMove*(m: var RammerModule, ws: WorldState): MoveCommand =
|
||||||
let dx = ws.enemyX - ws.selfX
|
let dx = ws.enemyX - ws.selfX
|
||||||
|
|||||||
@@ -15,17 +15,18 @@ const
|
|||||||
WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires
|
WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires
|
||||||
|
|
||||||
type RandomOscillatorModule* = object
|
type RandomOscillatorModule* = object
|
||||||
sign: float ## +1 or -1
|
sign: float ## +1 or -1
|
||||||
elapsed: int ## ticks since last reversal
|
elapsed: int ## ticks since last reversal
|
||||||
nextPeriod: int ## random period chosen at last reversal
|
nextPeriod: int ## random period chosen at last reversal
|
||||||
wallLockout: int ## remaining suppression ticks
|
wallLockout: int ## remaining suppression ticks
|
||||||
rng: Rand
|
rng: Rand
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initRandomOscillator*(): RandomOscillatorModule =
|
proc initRandomOscillator*(): RandomOscillatorModule =
|
||||||
var rng = initRand(getTime().toUnix())
|
var rng = initRand(getTime().toUnix())
|
||||||
let period = rng.rand(MinPeriod..MaxPeriod)
|
let period = rng.rand(MinPeriod..MaxPeriod)
|
||||||
RandomOscillatorModule(sign: 1.0, elapsed: 0, nextPeriod: period,
|
RandomOscillatorModule(sign: 1.0, elapsed: 0, nextPeriod: period,
|
||||||
wallLockout: 0, rng: rng)
|
wallLockout: 0, rng: rng, debugGraphics: false)
|
||||||
|
|
||||||
proc computeMove*(m: var RandomOscillatorModule, ws: WorldState): MoveCommand =
|
proc computeMove*(m: var RandomOscillatorModule, ws: WorldState): MoveCommand =
|
||||||
inc m.elapsed
|
inc m.elapsed
|
||||||
|
|||||||
@@ -0,0 +1,672 @@
|
|||||||
|
## TFIL — The Floor Is Lava. Built incrementally.
|
||||||
|
|
||||||
|
import std/math
|
||||||
|
import std/random
|
||||||
|
import gun_harness/gun_interface
|
||||||
|
import movement_harness/movement_interface
|
||||||
|
import robocode_tankroyale_botapi/graphics
|
||||||
|
import robocode_tankroyale_botapi/color
|
||||||
|
|
||||||
|
const GridSize = 36.0
|
||||||
|
const MaxSpeed = 8.0
|
||||||
|
|
||||||
|
const BulletCoreRadiusMin = 9.0 ## core radius at power 0.1
|
||||||
|
const BulletCoreRadiusMax = 54.0 ## core radius at power 3.0
|
||||||
|
const BulletAuraExtMin = 36.0 ## aura extension at power 3.0 (slow)
|
||||||
|
const BulletAuraExtMax = 54.0 ## aura extension at power 0.1 (fast)
|
||||||
|
|
||||||
|
const BulletCore = 10.0 ## lava accumulation per bullet-overlapping tile
|
||||||
|
const BulletAura = 5.0 ## lava accumulation for aura ring tiles
|
||||||
|
|
||||||
|
const EnemyCoreRadius = 18.0 ## half of 36px body
|
||||||
|
const EnemyAuraRadius = 54.0 ## 18 + 36
|
||||||
|
|
||||||
|
const EnemyCore = 40.0 ## lava per tile overlapping enemy body circle
|
||||||
|
const EnemyAura = 10.0 ## lava per tile in enemy aura ring
|
||||||
|
|
||||||
|
const CorridorHeat = 20.0
|
||||||
|
|
||||||
|
const WallHotness = 30.0
|
||||||
|
const WallRadiance = 10.0
|
||||||
|
|
||||||
|
const PillarHotness = 30.0
|
||||||
|
const PillarRadiance = 10.0
|
||||||
|
|
||||||
|
const CommitTicks = 15 ## ticks to commit to a dodge point
|
||||||
|
const MinCommitTicks = 5 ## must commit for this many ticks before danger replan allowed
|
||||||
|
const DangerReplanThreshold = 25.0 ## replan on serious threats only (bullet core), not corridors/auras
|
||||||
|
const CoolestLevels = 2 ## how many distinct lava values count as "cool"
|
||||||
|
const MaxTrackedBullets = 20 ## hard cap on tracked bullets
|
||||||
|
|
||||||
|
proc bulletRadii(power: float): tuple[core, aura: float] =
|
||||||
|
let t = (power - 0.1) / 2.9
|
||||||
|
let core = BulletCoreRadiusMin + t * (BulletCoreRadiusMax - BulletCoreRadiusMin)
|
||||||
|
let auraExt = BulletAuraExtMax - t * (BulletAuraExtMax - BulletAuraExtMin)
|
||||||
|
(core, core + auraExt)
|
||||||
|
|
||||||
|
type
|
||||||
|
TrackedBullet = object
|
||||||
|
originX, originY: float
|
||||||
|
x, y: float
|
||||||
|
velX, velY: float ## speed * cos(heading), speed * sin(heading)
|
||||||
|
power: float
|
||||||
|
alive: bool
|
||||||
|
age: int ## ticks alive; die if > 200
|
||||||
|
|
||||||
|
TFILModule* = object
|
||||||
|
debugGraphics*: bool
|
||||||
|
cols, rows: int
|
||||||
|
marginX, marginY: float
|
||||||
|
arenaWidth, arenaHeight: float
|
||||||
|
lava: seq[float] # flat row-major, index = row*cols + col
|
||||||
|
bullets: seq[TrackedBullet]
|
||||||
|
prevEnergy: seq[tuple[id: int, energy: float]] # enemy id -> last known energy
|
||||||
|
commitTarget: tuple[x, y: float] ## world coords of committed dodge point
|
||||||
|
commitTicks: int ## ticks remaining on commitment
|
||||||
|
commitLava: float ## lava at commit time (for spike detection)
|
||||||
|
blockedTile: tuple[col, row: int; active: bool] ## excluded from next pick after danger replan
|
||||||
|
cachedHull: seq[tuple[x, y: float]]
|
||||||
|
cachedInsideTiles: seq[tuple[col, row: int]]
|
||||||
|
callCount: int ## computeMove call count; 0 = never called
|
||||||
|
lastBotX, lastBotY: float ## bot position at last call; used to detect position jumps
|
||||||
|
lastTileCol, lastTileRow: int ## grid tile at last call; used to detect gradual displacement
|
||||||
|
|
||||||
|
proc initTFIL*(): TFILModule = TFILModule(debugGraphics: false)
|
||||||
|
|
||||||
|
proc removeBulletNear*(m: var TFILModule, x, y: float) =
|
||||||
|
## Mark the tracked bullet closest to (x,y) within GridSize tolerance as dead.
|
||||||
|
var bestIdx = -1
|
||||||
|
var bestD2 = GridSize * GridSize # tolerance²
|
||||||
|
for i, b in m.bullets:
|
||||||
|
let d2 = (b.x - x)*(b.x - x) + (b.y - y)*(b.y - y)
|
||||||
|
if d2 < bestD2:
|
||||||
|
bestD2 = d2
|
||||||
|
bestIdx = i
|
||||||
|
if bestIdx >= 0:
|
||||||
|
m.bullets.del(bestIdx)
|
||||||
|
|
||||||
|
proc prevEnergyGet(m: TFILModule, id: int): float =
|
||||||
|
for e in m.prevEnergy:
|
||||||
|
if e.id == id: return e.energy
|
||||||
|
100.0
|
||||||
|
|
||||||
|
proc prevEnergySet(m: var TFILModule, id: int, energy: float) =
|
||||||
|
for i in 0..<m.prevEnergy.len:
|
||||||
|
if m.prevEnergy[i].id == id:
|
||||||
|
m.prevEnergy[i].energy = energy
|
||||||
|
return
|
||||||
|
m.prevEnergy.add((id: id, energy: energy))
|
||||||
|
|
||||||
|
proc clearGraphics*(m: var TFILModule) =
|
||||||
|
## No-op: the SVG buffer is a module-level global cleared by the framework
|
||||||
|
## after every go(). Exists so callers can signal "TFIL is inactive this tick".
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc resetRound*(m: var TFILModule) =
|
||||||
|
m.bullets = @[]
|
||||||
|
m.prevEnergy = @[]
|
||||||
|
m.commitTicks = 0
|
||||||
|
m.cachedHull = @[]
|
||||||
|
m.cachedInsideTiles = @[]
|
||||||
|
m.blockedTile = (col: 0, row: 0, active: false)
|
||||||
|
m.callCount = 0
|
||||||
|
m.lastBotX = 0.0
|
||||||
|
m.lastBotY = 0.0
|
||||||
|
m.lastTileCol = 0
|
||||||
|
m.lastTileRow = 0
|
||||||
|
|
||||||
|
proc initGrid(m: var TFILModule, arenaWidth, arenaHeight: float) =
|
||||||
|
m.cols = int(arenaWidth / GridSize)
|
||||||
|
m.rows = int(arenaHeight / GridSize)
|
||||||
|
m.marginX = (arenaWidth - m.cols.float * GridSize) / 2.0
|
||||||
|
m.marginY = (arenaHeight - m.rows.float * GridSize) / 2.0
|
||||||
|
m.arenaWidth = arenaWidth
|
||||||
|
m.arenaHeight = arenaHeight
|
||||||
|
m.lava = newSeq[float](m.cols * m.rows) # all 0.0
|
||||||
|
|
||||||
|
proc detectFires(m: var TFILModule, ws: WorldState) =
|
||||||
|
## Check all enemies for energy drops; spawn a tracked bullet per confirmed fire.
|
||||||
|
for ei in ws.enemies:
|
||||||
|
let prev = m.prevEnergyGet(ei.id)
|
||||||
|
let drop = prev - ei.energy
|
||||||
|
m.prevEnergySet(ei.id, ei.energy)
|
||||||
|
if drop >= 0.09 and drop <= 3.01:
|
||||||
|
let speed = 20.0 - 3.0 * drop
|
||||||
|
# Linear prediction: aim at where we will be when the bullet arrives
|
||||||
|
let dist = sqrt((ws.selfX - ei.x)^2 + (ws.selfY - ei.y)^2)
|
||||||
|
let travelTime = dist / speed
|
||||||
|
let predX = ws.selfX + ws.selfSpeed * cos(ws.selfHeading * PI / 180.0) * travelTime
|
||||||
|
let predY = ws.selfY + ws.selfSpeed * sin(ws.selfHeading * PI / 180.0) * travelTime
|
||||||
|
let heading = arctan2(predY - ei.y, predX - ei.x)
|
||||||
|
if m.bullets.len >= MaxTrackedBullets:
|
||||||
|
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
|
||||||
|
m.bullets.add TrackedBullet(
|
||||||
|
originX: ei.x, originY: ei.y,
|
||||||
|
x: ei.x, y: ei.y,
|
||||||
|
velX: speed * cos(heading),
|
||||||
|
velY: speed * sin(heading),
|
||||||
|
power: drop,
|
||||||
|
alive: true,
|
||||||
|
age: 0)
|
||||||
|
|
||||||
|
proc advanceBullets(m: var TFILModule, selfX, selfY: float) =
|
||||||
|
## Advance positions and reap bullets that are: passed us, out of bounds, or too old.
|
||||||
|
var i = 0
|
||||||
|
while i < m.bullets.len:
|
||||||
|
var b = m.bullets[i]
|
||||||
|
b.x += b.velX
|
||||||
|
b.y += b.velY
|
||||||
|
b.age += 1
|
||||||
|
# Death conditions (any triggers removal):
|
||||||
|
# 1. Passed us (dot < 0, moving away)
|
||||||
|
# 2. Out of arena bounds
|
||||||
|
# 3. Too old (>200 ticks)
|
||||||
|
let dx = selfX - b.x
|
||||||
|
let dy = selfY - b.y
|
||||||
|
let dot = b.velX * dx + b.velY * dy
|
||||||
|
let outOfBounds = b.x < 0.0 or b.x > m.arenaWidth or b.y < 0.0 or b.y > m.arenaHeight
|
||||||
|
if dot < 0.0 or outOfBounds or b.age > 200:
|
||||||
|
b.alive = false
|
||||||
|
m.bullets[i] = b
|
||||||
|
if b.alive: inc i
|
||||||
|
else: m.bullets.del(i)
|
||||||
|
|
||||||
|
type CorridorGeom = object
|
||||||
|
dx, dy: float ## unit heading
|
||||||
|
px, py: float ## unit perpendicular
|
||||||
|
tMin: float ## distance to wall
|
||||||
|
bx, by: float ## bullet origin
|
||||||
|
|
||||||
|
proc corridorGeom(b: TrackedBullet, arenaWidth, arenaHeight: float): CorridorGeom =
|
||||||
|
let speed = sqrt(b.velX * b.velX + b.velY * b.velY)
|
||||||
|
if speed < 0.001: return
|
||||||
|
let dx = b.velX / speed
|
||||||
|
let dy = b.velY / speed
|
||||||
|
var tMin = Inf
|
||||||
|
if dx > 0.0: tMin = min(tMin, (arenaWidth - b.x) / dx)
|
||||||
|
elif dx < 0.0: tMin = min(tMin, (0.0 - b.x) / dx)
|
||||||
|
if dy > 0.0: tMin = min(tMin, (arenaHeight - b.y) / dy)
|
||||||
|
elif dy < 0.0: tMin = min(tMin, (0.0 - b.y) / dy)
|
||||||
|
CorridorGeom(dx: dx, dy: dy, px: -dy, py: dx, tMin: tMin, bx: b.x, by: b.y)
|
||||||
|
|
||||||
|
proc lavaAt(m: TFILModule, col, row: int): float =
|
||||||
|
m.lava[row * m.cols + col]
|
||||||
|
|
||||||
|
proc tileAt(m: TFILModule, wx, wy: float): tuple[col, row: int] =
|
||||||
|
(col: clamp(int((wx - m.marginX) / GridSize), 0, m.cols - 1),
|
||||||
|
row: clamp(int((wy - m.marginY) / GridSize), 0, m.rows - 1))
|
||||||
|
|
||||||
|
proc pointInHull(px, py: float, hull: seq[(float, float)]): bool =
|
||||||
|
var inside = false
|
||||||
|
var j = hull.high
|
||||||
|
for i in 0..hull.high:
|
||||||
|
if ((hull[i][1] > py) != (hull[j][1] > py)) and
|
||||||
|
(px < (hull[j][0] - hull[i][0]) * (py - hull[i][1]) / (hull[j][1] - hull[i][1]) + hull[i][0]):
|
||||||
|
inside = not inside
|
||||||
|
j = i
|
||||||
|
inside
|
||||||
|
|
||||||
|
proc computeReachableHull(x0, y0, heading0, speed0,
|
||||||
|
arenaW, arenaH: float, ticks: int = 50): seq[(float, float)] =
|
||||||
|
## Simulate `ticks` ticks at various turn rates / target speeds.
|
||||||
|
## Returns convex hull (gift-wrap) of final positions.
|
||||||
|
const TargetSpeeds = [8.0, 4.0, -4.0, -8.0]
|
||||||
|
const NumRates = 11
|
||||||
|
|
||||||
|
var pts: seq[(float, float)]
|
||||||
|
pts.add (x0, y0) # always reachable: stay
|
||||||
|
|
||||||
|
for tSpeed in TargetSpeeds:
|
||||||
|
# Max turn rate at target speed (approximate; actual varies per tick but close enough)
|
||||||
|
let mtr = 10.0 - 0.75 * abs(tSpeed)
|
||||||
|
for ri in 0..<NumRates:
|
||||||
|
let turnRate = if NumRates == 1: 0.0
|
||||||
|
else: -mtr + (2.0 * mtr / (NumRates - 1).float) * ri.float
|
||||||
|
var x = x0; var y = y0
|
||||||
|
var h = heading0; var spd = speed0
|
||||||
|
for _ in 0..<ticks:
|
||||||
|
# Accelerate toward target
|
||||||
|
if spd < tSpeed: spd = min(spd + 1.0, tSpeed)
|
||||||
|
elif spd > tSpeed: spd = max(spd - 1.0, tSpeed)
|
||||||
|
spd = clamp(spd, -MaxSpeed, MaxSpeed)
|
||||||
|
# Turn (clamp to current max turn rate)
|
||||||
|
let curMtr = 10.0 - 0.75 * abs(spd)
|
||||||
|
let tr = clamp(turnRate, -curMtr, curMtr)
|
||||||
|
h += tr
|
||||||
|
let hr = h * PI / 180.0
|
||||||
|
x = clamp(x + spd * cos(hr), 0.0, arenaW)
|
||||||
|
y = clamp(y + spd * sin(hr), 0.0, arenaH)
|
||||||
|
pts.add (x, y)
|
||||||
|
|
||||||
|
# Gift-wrap convex hull (O(n²), ~45 points — fine)
|
||||||
|
# Find leftmost point as start
|
||||||
|
var startIdx = 0
|
||||||
|
for i in 1..<pts.len:
|
||||||
|
if pts[i][0] < pts[startIdx][0] or
|
||||||
|
(pts[i][0] == pts[startIdx][0] and pts[i][1] < pts[startIdx][1]):
|
||||||
|
startIdx = i
|
||||||
|
|
||||||
|
var hull: seq[(float, float)]
|
||||||
|
var cur = startIdx
|
||||||
|
while true:
|
||||||
|
hull.add pts[cur]
|
||||||
|
var next = 0
|
||||||
|
for i in 1..<pts.len:
|
||||||
|
if next == cur:
|
||||||
|
next = i
|
||||||
|
continue
|
||||||
|
let ax = pts[next][0] - pts[cur][0]
|
||||||
|
let ay = pts[next][1] - pts[cur][1]
|
||||||
|
let bx = pts[i][0] - pts[cur][0]
|
||||||
|
let by = pts[i][1] - pts[cur][1]
|
||||||
|
let cross = ax * by - ay * bx
|
||||||
|
if cross < 0.0: # i is more counterclockwise
|
||||||
|
next = i
|
||||||
|
cur = next
|
||||||
|
if cur == startIdx: break
|
||||||
|
hull
|
||||||
|
|
||||||
|
proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||||
|
if m.cols == 0:
|
||||||
|
m.initGrid(ws.arenaWidth, ws.arenaHeight)
|
||||||
|
|
||||||
|
# Soft reset: detect gap by position jump (rammer moved us across ticks)
|
||||||
|
# 12px threshold: above single-tick max movement (8px) but catches even short ram gaps
|
||||||
|
let jumpDist = sqrt((ws.selfX - m.lastBotX)^2 + (ws.selfY - m.lastBotY)^2)
|
||||||
|
let jumped = (m.callCount > 0) and (jumpDist > 12.0)
|
||||||
|
if jumped:
|
||||||
|
m.commitTicks = 0 # force replan — old target invalid
|
||||||
|
m.cachedHull = @[] # stale position/heading
|
||||||
|
m.cachedInsideTiles = @[]
|
||||||
|
m.bullets = @[] # bullet positions are hopelessly stale
|
||||||
|
m.blockedTile = (col: 0, row: 0, active: false)
|
||||||
|
# Re-snapshot prevEnergy so energy changes during ramming aren't misread as fires
|
||||||
|
m.prevEnergy = @[]
|
||||||
|
for ei in ws.enemies:
|
||||||
|
m.prevEnergySet(ei.id, ei.energy)
|
||||||
|
|
||||||
|
# Tile-change replan: catches gradual displacement that position threshold misses
|
||||||
|
if (not jumped) and (m.callCount > 0) and (m.commitTicks > 0):
|
||||||
|
let curTileCol = clamp(int((ws.selfX - m.marginX) / GridSize), 0, m.cols - 1)
|
||||||
|
let curTileRow = clamp(int((ws.selfY - m.marginY) / GridSize), 0, m.rows - 1)
|
||||||
|
if curTileCol != m.lastTileCol or curTileRow != m.lastTileRow:
|
||||||
|
m.commitTicks = 0
|
||||||
|
m.cachedHull = @[]
|
||||||
|
m.cachedInsideTiles = @[]
|
||||||
|
|
||||||
|
# Per-tick: advance existing bullets, detect new fires
|
||||||
|
m.advanceBullets(ws.selfX, ws.selfY)
|
||||||
|
m.detectFires(ws)
|
||||||
|
|
||||||
|
# Recompute lava from scratch each tick
|
||||||
|
for i in 0..<m.lava.len: m.lava[i] = 0.0
|
||||||
|
for b in m.bullets:
|
||||||
|
let bx = b.x
|
||||||
|
let by = b.y
|
||||||
|
let (coreR, auraR) = bulletRadii(b.power)
|
||||||
|
let colMin = max(0, int(floor((bx - auraR - m.marginX) / GridSize)))
|
||||||
|
let colMax = min(m.cols-1, int(floor((bx + auraR - m.marginX) / GridSize)))
|
||||||
|
let rowMin = max(0, int(floor((by - auraR - m.marginY) / GridSize)))
|
||||||
|
let rowMax = min(m.rows-1, int(floor((by + auraR - m.marginY) / GridSize)))
|
||||||
|
for row in rowMin..rowMax:
|
||||||
|
for col in colMin..colMax:
|
||||||
|
let x0 = m.marginX + col.float * GridSize
|
||||||
|
let y0 = m.marginY + row.float * GridSize
|
||||||
|
let nearX = clamp(bx, x0, x0 + GridSize)
|
||||||
|
let nearY = clamp(by, y0, y0 + GridSize)
|
||||||
|
let dx = nearX - bx
|
||||||
|
let dy = nearY - by
|
||||||
|
let d2 = dx*dx + dy*dy
|
||||||
|
if d2 <= coreR * coreR:
|
||||||
|
m.lava[row * m.cols + col] += BulletCore
|
||||||
|
elif d2 <= auraR * auraR:
|
||||||
|
m.lava[row * m.cols + col] += BulletAura
|
||||||
|
|
||||||
|
# Corridor heat — rotated rectangle from bullet position to arena wall, auraR wide
|
||||||
|
for b in m.bullets:
|
||||||
|
let cg = corridorGeom(b, m.arenaWidth, m.arenaHeight)
|
||||||
|
if cg.tMin == 0.0: continue # zero-speed bullet, skip
|
||||||
|
let (_, auraR) = bulletRadii(b.power)
|
||||||
|
let wx = cg.bx + cg.dx * cg.tMin
|
||||||
|
let wy = cg.by + cg.dy * cg.tMin
|
||||||
|
# Bounding box of the 4 corners
|
||||||
|
let c0x = cg.bx + cg.px * auraR; let c0y = cg.by + cg.py * auraR
|
||||||
|
let c1x = cg.bx - cg.px * auraR; let c1y = cg.by - cg.py * auraR
|
||||||
|
let c2x = wx - cg.px * auraR; let c2y = wy - cg.py * auraR
|
||||||
|
let c3x = wx + cg.px * auraR; let c3y = wy + cg.py * auraR
|
||||||
|
let xMin = min(min(c0x, c1x), min(c2x, c3x))
|
||||||
|
let xMax = max(max(c0x, c1x), max(c2x, c3x))
|
||||||
|
let yMin = min(min(c0y, c1y), min(c2y, c3y))
|
||||||
|
let yMax = max(max(c0y, c1y), max(c2y, c3y))
|
||||||
|
let colMin = max(0, int(floor((xMin - m.marginX) / GridSize)))
|
||||||
|
let colMax = min(m.cols-1, int(floor((xMax - m.marginX) / GridSize)))
|
||||||
|
let rowMin = max(0, int(floor((yMin - m.marginY) / GridSize)))
|
||||||
|
let rowMax = min(m.rows-1, int(floor((yMax - m.marginY) / GridSize)))
|
||||||
|
for row in rowMin..rowMax:
|
||||||
|
for col in colMin..colMax:
|
||||||
|
let cx = m.marginX + (col.float + 0.5) * GridSize
|
||||||
|
let cy = m.marginY + (row.float + 0.5) * GridSize
|
||||||
|
# Project tile center onto heading and perpendicular axes
|
||||||
|
let relX = cx - cg.bx
|
||||||
|
let relY = cy - cg.by
|
||||||
|
let along = relX * cg.dx + relY * cg.dy
|
||||||
|
let perp = relX * cg.px + relY * cg.py
|
||||||
|
if along >= 0.0 and along <= cg.tMin and perp >= -auraR and perp <= auraR:
|
||||||
|
m.lava[row * m.cols + col] += CorridorHeat
|
||||||
|
|
||||||
|
# Enemy heat auras — core (18px) and aura ring (54px), same pattern as bullets
|
||||||
|
for ei in ws.enemies:
|
||||||
|
let ex = ei.x
|
||||||
|
let ey = ei.y
|
||||||
|
let colMin = max(0, int(floor((ex - EnemyAuraRadius - m.marginX) / GridSize)))
|
||||||
|
let colMax = min(m.cols-1, int(floor((ex + EnemyAuraRadius - m.marginX) / GridSize)))
|
||||||
|
let rowMin = max(0, int(floor((ey - EnemyAuraRadius - m.marginY) / GridSize)))
|
||||||
|
let rowMax = min(m.rows-1, int(floor((ey + EnemyAuraRadius - m.marginY) / GridSize)))
|
||||||
|
for row in rowMin..rowMax:
|
||||||
|
for col in colMin..colMax:
|
||||||
|
let x0 = m.marginX + col.float * GridSize
|
||||||
|
let y0 = m.marginY + row.float * GridSize
|
||||||
|
let nearX = clamp(ex, x0, x0 + GridSize)
|
||||||
|
let nearY = clamp(ey, y0, y0 + GridSize)
|
||||||
|
let dx = nearX - ex
|
||||||
|
let dy = nearY - ey
|
||||||
|
let d2 = dx*dx + dy*dy
|
||||||
|
if d2 <= EnemyCoreRadius * EnemyCoreRadius:
|
||||||
|
m.lava[row * m.cols + col] += EnemyCore
|
||||||
|
elif d2 <= EnemyAuraRadius * EnemyAuraRadius:
|
||||||
|
m.lava[row * m.cols + col] += EnemyAura
|
||||||
|
|
||||||
|
# Wall radiance heat — additive with bullet heat
|
||||||
|
for row in 0..<m.rows:
|
||||||
|
for col in 0..<m.cols:
|
||||||
|
let heat = max(0.0, WallHotness - col.float * WallRadiance) +
|
||||||
|
max(0.0, WallHotness - (m.cols-1-col).float * WallRadiance) +
|
||||||
|
max(0.0, WallHotness - row.float * WallRadiance) +
|
||||||
|
max(0.0, WallHotness - (m.rows-1-row).float * WallRadiance)
|
||||||
|
m.lava[row * m.cols + col] += heat
|
||||||
|
|
||||||
|
# Pillar radiance heat — center 1×1, 1×2, 2×1, or 2×2 depending on grid parity
|
||||||
|
let pc0 = if m.cols mod 2 == 1: m.cols div 2 else: m.cols div 2 - 1
|
||||||
|
let pc1 = m.cols div 2 # same as pc0 when odd, pc0+1 when even
|
||||||
|
let pr0 = if m.rows mod 2 == 1: m.rows div 2 else: m.rows div 2 - 1
|
||||||
|
let pr1 = m.rows div 2
|
||||||
|
for row in 0..<m.rows:
|
||||||
|
for col in 0..<m.cols:
|
||||||
|
var minDist = int.high
|
||||||
|
for pcol in pc0..pc1:
|
||||||
|
for prow in pr0..pr1:
|
||||||
|
let d = max(abs(col - pcol), abs(row - prow))
|
||||||
|
if d < minDist: minDist = d
|
||||||
|
m.lava[row * m.cols + col] += max(0.0, PillarHotness - minDist.float * PillarRadiance)
|
||||||
|
|
||||||
|
if m.debugGraphics:
|
||||||
|
# Compute max lava for heat gradient
|
||||||
|
var maxLava = 0.0
|
||||||
|
for v in m.lava:
|
||||||
|
if v > maxLava: maxLava = v
|
||||||
|
|
||||||
|
# Non-zero tiles: colored border + colored value text (yellow→orange→red)
|
||||||
|
setFont("Arial", 10.0)
|
||||||
|
for row in 0..<m.rows:
|
||||||
|
for col in 0..<m.cols:
|
||||||
|
let val = m.lava[row * m.cols + col]
|
||||||
|
if val == 0.0: continue
|
||||||
|
let t = if maxLava > 0.0: val / maxLava else: 0.0
|
||||||
|
let heatColor = fromRgb(255'u8, uint8(255.0 * (1.0 - t)), 0'u8)
|
||||||
|
let x0 = m.marginX + col.float * GridSize
|
||||||
|
let y0 = m.marginY + row.float * GridSize
|
||||||
|
setStrokeColor(heatColor)
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
drawRectangle(x0, y0, GridSize, GridSize)
|
||||||
|
setFillColor(heatColor)
|
||||||
|
drawText($int(val), x0 + 12.0, y0 + 22.0)
|
||||||
|
|
||||||
|
# Draw tracked bullet circles
|
||||||
|
setStrokeColor(RED)
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
setFillColor(RED)
|
||||||
|
for b in m.bullets:
|
||||||
|
let (coreR, auraR) = bulletRadii(b.power)
|
||||||
|
drawCircle(b.x, b.y, coreR)
|
||||||
|
fillCircle(b.x, b.y, 3.0)
|
||||||
|
setStrokeColor(fromHex("#FF8800")) # orange aura
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
drawCircle(b.x, b.y, auraR)
|
||||||
|
setStrokeColor(RED)
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
|
||||||
|
# Danger corridor: rotated rectangle projecting each bullet forward to arena wall
|
||||||
|
setStrokeColor(fromHex("#AAAAAA"))
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
for b in m.bullets:
|
||||||
|
let (_, auraR) = bulletRadii(b.power)
|
||||||
|
let cg = corridorGeom(b, m.arenaWidth, m.arenaHeight)
|
||||||
|
if cg.tMin == 0.0: continue
|
||||||
|
let wx = cg.bx + cg.dx * cg.tMin
|
||||||
|
let wy = cg.by + cg.dy * cg.tMin
|
||||||
|
let corners: seq[(float, float)] = @[
|
||||||
|
(cg.bx + cg.px * auraR, cg.by + cg.py * auraR),
|
||||||
|
(cg.bx - cg.px * auraR, cg.by - cg.py * auraR),
|
||||||
|
(wx - cg.px * auraR, wy - cg.py * auraR),
|
||||||
|
(wx + cg.px * auraR, wy + cg.py * auraR),
|
||||||
|
]
|
||||||
|
drawPolygon(corners)
|
||||||
|
|
||||||
|
# Enemy core (cyan) and aura (green)
|
||||||
|
setStrokeColor(fromHex("#00FFFF")) # cyan core
|
||||||
|
setStrokeWidth(1.5)
|
||||||
|
for ei in ws.enemies:
|
||||||
|
drawCircle(ei.x, ei.y, EnemyCoreRadius)
|
||||||
|
setStrokeColor(fromHex("#00CC00")) # green aura
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
for ei in ws.enemies:
|
||||||
|
drawCircle(ei.x, ei.y, EnemyAuraRadius)
|
||||||
|
|
||||||
|
# ── Tile-based Dodge System ───────────────────────────────────────────────────
|
||||||
|
let botCol = clamp(int((ws.selfX - m.marginX) / GridSize), 0, m.cols - 1)
|
||||||
|
let botRow = clamp(int((ws.selfY - m.marginY) / GridSize), 0, m.rows - 1)
|
||||||
|
|
||||||
|
# Hull + inside-tiles: only recompute on replan tick (commitTicks == 0)
|
||||||
|
type TileRef = tuple[col, row: int]
|
||||||
|
if m.commitTicks == 0:
|
||||||
|
let hull = computeReachableHull(ws.selfX, ws.selfY, ws.selfHeading, ws.selfSpeed,
|
||||||
|
m.arenaWidth, m.arenaHeight, 50)
|
||||||
|
# store as named-field seq to match cachedHull type
|
||||||
|
m.cachedHull = @[]
|
||||||
|
for p in hull: m.cachedHull.add (x: p[0], y: p[1])
|
||||||
|
m.cachedInsideTiles = @[]
|
||||||
|
if hull.len >= 3:
|
||||||
|
for row in 0..<m.rows:
|
||||||
|
for col in 0..<m.cols:
|
||||||
|
let cx = m.marginX + (col.float + 0.5) * GridSize
|
||||||
|
let cy = m.marginY + (row.float + 0.5) * GridSize
|
||||||
|
if pointInHull(cx, cy, hull):
|
||||||
|
m.cachedInsideTiles.add (col: col, row: row)
|
||||||
|
|
||||||
|
let insideTiles = m.cachedInsideTiles
|
||||||
|
|
||||||
|
# Find the CoolestLevels distinct lava values among inside-hull tiles
|
||||||
|
var distinctVals: seq[float]
|
||||||
|
for t in insideTiles:
|
||||||
|
let v = m.lavaAt(t.col, t.row)
|
||||||
|
var found = false
|
||||||
|
for dv in distinctVals:
|
||||||
|
if dv == v: found = true; break
|
||||||
|
if not found: distinctVals.add v
|
||||||
|
# Sort ascending (insertion sort — small N)
|
||||||
|
for i in 1..<distinctVals.len:
|
||||||
|
let key = distinctVals[i]
|
||||||
|
var j = i - 1
|
||||||
|
while j >= 0 and distinctVals[j] > key:
|
||||||
|
distinctVals[j + 1] = distinctVals[j]
|
||||||
|
dec j
|
||||||
|
distinctVals[j + 1] = key
|
||||||
|
|
||||||
|
# Collect tiles matching the CoolestLevels coolest distinct values
|
||||||
|
var coolTiles: seq[TileRef]
|
||||||
|
let numLevels = min(CoolestLevels, distinctVals.len)
|
||||||
|
for t in insideTiles:
|
||||||
|
let v = m.lavaAt(t.col, t.row)
|
||||||
|
for li in 0..<numLevels:
|
||||||
|
if v == distinctVals[li]:
|
||||||
|
coolTiles.add t
|
||||||
|
break
|
||||||
|
|
||||||
|
# Score each cool tile by MAX lava on the straight-line path from bot.
|
||||||
|
# A single hot tile on the path (corridor, bullet core, enemy aura) makes the whole path unsafe.
|
||||||
|
const PathSampleStep = 18.0 # ~half a tile
|
||||||
|
const PathDangerThreshold = 10.0 # max lava on path; above this = unsafe
|
||||||
|
type ScoredTile = tuple[col, row: int; pathMaxHeat: float]
|
||||||
|
var scoredTiles: seq[ScoredTile]
|
||||||
|
for t in coolTiles:
|
||||||
|
let tx = m.marginX + (t.col.float + 0.5) * GridSize
|
||||||
|
let ty = m.marginY + (t.row.float + 0.5) * GridSize
|
||||||
|
let ddx = tx - ws.selfX
|
||||||
|
let ddy = ty - ws.selfY
|
||||||
|
let lineDist = sqrt(ddx*ddx + ddy*ddy)
|
||||||
|
var pathMaxHeat = 0.0
|
||||||
|
if lineDist > 0.1:
|
||||||
|
let steps = max(1, int(lineDist / PathSampleStep))
|
||||||
|
for si in 0..steps:
|
||||||
|
let frac = si.float / steps.float
|
||||||
|
let sx = ws.selfX + ddx * frac
|
||||||
|
let sy = ws.selfY + ddy * frac
|
||||||
|
let (sc, sr) = m.tileAt(sx, sy)
|
||||||
|
pathMaxHeat = max(pathMaxHeat, m.lavaAt(sc, sr))
|
||||||
|
scoredTiles.add (col: t.col, row: t.row, pathMaxHeat: pathMaxHeat)
|
||||||
|
|
||||||
|
# Sort by pathMaxHeat ascending (insertion sort — small N)
|
||||||
|
for i in 1..<scoredTiles.len:
|
||||||
|
let key = scoredTiles[i]
|
||||||
|
var j = i - 1
|
||||||
|
while j >= 0 and scoredTiles[j].pathMaxHeat > key.pathMaxHeat:
|
||||||
|
scoredTiles[j + 1] = scoredTiles[j]
|
||||||
|
dec j
|
||||||
|
scoredTiles[j + 1] = key
|
||||||
|
|
||||||
|
# Absolute threshold filter: safe = path max lava <= PathDangerThreshold.
|
||||||
|
# Fallback: if everything is hot, keep the 2 coolest paths anyway.
|
||||||
|
var safeTiles: seq[ScoredTile]
|
||||||
|
var blockedTiles: seq[ScoredTile]
|
||||||
|
for t in scoredTiles:
|
||||||
|
if t.pathMaxHeat <= PathDangerThreshold: safeTiles.add t
|
||||||
|
else: blockedTiles.add t
|
||||||
|
if safeTiles.len < 2:
|
||||||
|
# Fallback: promote the least-hot blocked tiles until we have 2
|
||||||
|
# ponytail: O(n) scan on already-sorted seq — fine for small N
|
||||||
|
let needed = 2 - safeTiles.len
|
||||||
|
let promote = min(needed, blockedTiles.len)
|
||||||
|
for i in 0..<promote:
|
||||||
|
safeTiles.add blockedTiles[i]
|
||||||
|
blockedTiles = blockedTiles[promote ..< blockedTiles.len]
|
||||||
|
|
||||||
|
# Commitment logic
|
||||||
|
if m.commitTicks > 0:
|
||||||
|
# Only allow danger replan after MinCommitTicks have elapsed
|
||||||
|
let ticksElapsed = CommitTicks - m.commitTicks
|
||||||
|
if ticksElapsed >= MinCommitTicks:
|
||||||
|
let (cc, cr) = m.tileAt(m.commitTarget.x, m.commitTarget.y)
|
||||||
|
let curLava = m.lavaAt(cc, cr)
|
||||||
|
if curLava > m.commitLava + DangerReplanThreshold:
|
||||||
|
# Mark committed tile blocked so we don't re-pick it
|
||||||
|
m.blockedTile = (col: cc, row: cr, active: true)
|
||||||
|
m.commitTicks = 0 # replan
|
||||||
|
else:
|
||||||
|
dec m.commitTicks
|
||||||
|
else:
|
||||||
|
dec m.commitTicks
|
||||||
|
|
||||||
|
if m.commitTicks == 0 and safeTiles.len > 0:
|
||||||
|
# Filter out the blocked tile from candidates
|
||||||
|
var candidates: seq[ScoredTile]
|
||||||
|
for t in safeTiles:
|
||||||
|
if m.blockedTile.active and t.col == m.blockedTile.col and t.row == m.blockedTile.row:
|
||||||
|
continue
|
||||||
|
candidates.add t
|
||||||
|
if candidates.len == 0: candidates = safeTiles # all blocked → ignore block
|
||||||
|
let chosen = rand(candidates.high)
|
||||||
|
let ct = candidates[chosen]
|
||||||
|
m.commitTarget = (x: m.marginX + (ct.col.float + 0.5) * GridSize,
|
||||||
|
y: m.marginY + (ct.row.float + 0.5) * GridSize)
|
||||||
|
m.commitTicks = CommitTicks
|
||||||
|
m.commitLava = m.lavaAt(ct.col, ct.row)
|
||||||
|
m.blockedTile.active = false # clear after successful pick
|
||||||
|
|
||||||
|
if m.debugGraphics:
|
||||||
|
# Reachable hull perimeter (darker blue)
|
||||||
|
if m.cachedHull.len >= 3:
|
||||||
|
let hullPairs: seq[(float, float)] = block:
|
||||||
|
var s: seq[(float, float)]
|
||||||
|
for p in m.cachedHull: s.add (p.x, p.y)
|
||||||
|
s
|
||||||
|
setStrokeColor(fromHex("#336699"))
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
drawPolygon(hullPairs)
|
||||||
|
|
||||||
|
# Dim (dark cyan) for path-blocked cool tiles
|
||||||
|
setStrokeColor(fromHex("#006666"))
|
||||||
|
setStrokeWidth(1.0)
|
||||||
|
for t in blockedTiles:
|
||||||
|
let x0 = m.marginX + t.col.float * GridSize
|
||||||
|
let y0 = m.marginY + t.row.float * GridSize
|
||||||
|
drawRectangle(x0, y0, GridSize, GridSize)
|
||||||
|
|
||||||
|
# Bright cyan borders on safe-to-reach tiles
|
||||||
|
setStrokeColor(fromHex("#00FFFF"))
|
||||||
|
setStrokeWidth(2.0)
|
||||||
|
for t in safeTiles:
|
||||||
|
let x0 = m.marginX + t.col.float * GridSize
|
||||||
|
let y0 = m.marginY + t.row.float * GridSize
|
||||||
|
drawRectangle(x0, y0, GridSize, GridSize)
|
||||||
|
|
||||||
|
# Green on chosen tile
|
||||||
|
let (chosenCol, chosenRow) = m.tileAt(m.commitTarget.x, m.commitTarget.y)
|
||||||
|
let gx0 = m.marginX + chosenCol.float * GridSize
|
||||||
|
let gy0 = m.marginY + chosenRow.float * GridSize
|
||||||
|
setStrokeColor(fromHex("#00FF00"))
|
||||||
|
setStrokeWidth(2.5)
|
||||||
|
drawRectangle(gx0, gy0, GridSize, GridSize)
|
||||||
|
|
||||||
|
# Blue on bot tile
|
||||||
|
let bx0 = m.marginX + botCol.float * GridSize
|
||||||
|
let by0 = m.marginY + botRow.float * GridSize
|
||||||
|
setStrokeColor(fromHex("#0088FF"))
|
||||||
|
setStrokeWidth(2.5)
|
||||||
|
drawRectangle(bx0, by0, GridSize, GridSize)
|
||||||
|
|
||||||
|
# Committed target line
|
||||||
|
setStrokeColor(fromHex("#00FF00"))
|
||||||
|
setStrokeWidth(1.5)
|
||||||
|
drawLine(ws.selfX, ws.selfY, m.commitTarget.x, m.commitTarget.y)
|
||||||
|
|
||||||
|
# Update position snapshot and call counter for next gap detection
|
||||||
|
m.lastBotX = ws.selfX
|
||||||
|
m.lastBotY = ws.selfY
|
||||||
|
m.lastTileCol = clamp(int((ws.selfX - m.marginX) / GridSize), 0, m.cols - 1)
|
||||||
|
m.lastTileRow = clamp(int((ws.selfY - m.marginY) / GridSize), 0, m.rows - 1)
|
||||||
|
m.callCount += 1
|
||||||
|
|
||||||
|
# ── Steering ─────────────────────────────────────────────────────────────────
|
||||||
|
let stepDx = m.commitTarget.x - ws.selfX
|
||||||
|
let stepDy = m.commitTarget.y - ws.selfY
|
||||||
|
let dist2 = stepDx*stepDx + stepDy*stepDy
|
||||||
|
if dist2 < 324.0: # already at target (18px radius)
|
||||||
|
return (speed: 0.0, turnRate: 0.0)
|
||||||
|
|
||||||
|
let targetBearing = arctan2(stepDy, stepDx) * 180.0 / PI
|
||||||
|
var delta = targetBearing - ws.selfHeading
|
||||||
|
while delta > 180.0: delta -= 360.0
|
||||||
|
while delta < -180.0: delta += 360.0
|
||||||
|
|
||||||
|
let maxTurnRate = 10.0 - 0.75 * abs(ws.selfSpeed)
|
||||||
|
var speed: float
|
||||||
|
var turnRate: float
|
||||||
|
if abs(delta) <= 90.0:
|
||||||
|
speed = MaxSpeed
|
||||||
|
turnRate = clamp(delta, -maxTurnRate, maxTurnRate)
|
||||||
|
else:
|
||||||
|
let flipped = if delta > 0.0: delta - 180.0 else: delta + 180.0
|
||||||
|
speed = -MaxSpeed
|
||||||
|
turnRate = clamp(flipped, -maxTurnRate, maxTurnRate)
|
||||||
|
|
||||||
|
result = (speed: speed, turnRate: turnRate)
|
||||||
@@ -23,13 +23,14 @@ type
|
|||||||
startDist: float64
|
startDist: float64
|
||||||
|
|
||||||
WaveSurferModule* = object
|
WaveSurferModule* = object
|
||||||
bins: array[WS_BINS, float64]
|
bins: array[WS_BINS, float64]
|
||||||
waves: seq[WSWave]
|
waves: seq[WSWave]
|
||||||
prevEnergy: float64
|
prevEnergy: float64
|
||||||
strafeDir: float64 ## +1.0 or -1.0
|
strafeDir: float64 ## +1.0 or -1.0
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initWaveSurfer*(): WaveSurferModule =
|
proc initWaveSurfer*(): WaveSurferModule =
|
||||||
var m = WaveSurferModule(prevEnergy: 100.0, strafeDir: 1.0)
|
var m = WaveSurferModule(prevEnergy: 100.0, strafeDir: 1.0, debugGraphics: false)
|
||||||
for i in 0..<WS_BINS: m.bins[i] = 1.0
|
for i in 0..<WS_BINS: m.bins[i] = 1.0
|
||||||
m
|
m
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ export radar_interface
|
|||||||
const MaxRadarTurnRate* = 45.0 ## deg/tick — Tank Royale hard limit
|
const MaxRadarTurnRate* = 45.0 ## deg/tick — Tank Royale hard limit
|
||||||
|
|
||||||
type MeleeScanModule* = object
|
type MeleeScanModule* = object
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc initMeleeScan*(): MeleeScanModule = MeleeScanModule()
|
proc initMeleeScan*(): MeleeScanModule = MeleeScanModule(debugGraphics: false)
|
||||||
|
|
||||||
proc computeScan*(m: MeleeScanModule, state: WorldState): float =
|
proc computeScan*(m: MeleeScanModule, state: WorldState): float =
|
||||||
## Always spin at max rate — guarantees full arena coverage every 8 ticks.
|
## Always spin at max rate — guarantees full arena coverage every 8 ticks.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ../radar_lock/radar_lock as radar_lock_impl
|
|||||||
export radar_interface
|
export radar_interface
|
||||||
|
|
||||||
type RadarLockModule* = object
|
type RadarLockModule* = object
|
||||||
|
debugGraphics*: bool
|
||||||
|
|
||||||
proc computeScan*(m: RadarLockModule, state: WorldState): float =
|
proc computeScan*(m: RadarLockModule, state: WorldState): float =
|
||||||
## Returns radar turn rate (deg/tick) to lock onto enemy.
|
## Returns radar turn rate (deg/tick) to lock onto enemy.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ type
|
|||||||
enemies*: Table[int, EnemyState]
|
enemies*: Table[int, EnemyState]
|
||||||
|
|
||||||
proc update*(et: var EnemyTracker, botId: int, x, y, heading, speed, energy: float, tick: int) =
|
proc update*(et: var EnemyTracker, botId: int, x, y, heading, speed, energy: float, tick: int) =
|
||||||
|
if botId in et.enemies and not et.enemies[botId].alive:
|
||||||
|
return # don't resurrect dead bots
|
||||||
et.enemies[botId] = EnemyState(id: botId, x: x, y: y, heading: heading,
|
et.enemies[botId] = EnemyState(id: botId, x: x, y: y, heading: heading,
|
||||||
speed: speed, energy: energy,
|
speed: speed, energy: energy,
|
||||||
lastSeenTick: tick, alive: true)
|
lastSeenTick: tick, alive: true)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user