diff --git a/ModularBot_garage/ModularBot b/ModularBot_garage/ModularBot index 20a43aa..808fdb8 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 bf4c057..75755b2 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -27,6 +27,7 @@ import guns/decay_gf import guns/knn_gun import movements/phantom_meteor import movements/rammer +import movements/the_floor_is_lava import movement_harness/virtual_bodies as mvb import movement_harness/bullet_shadows import targeting/enemy_tracker @@ -71,9 +72,12 @@ type avgLead: AveragedLeadGun decayGF: DecayGFGun knnGun: KNNGun - mover: PhantomMeteorModule + mover: TFILModule rammer: RammerModule isRamming: bool + ramStuckTicks: int + ramDurationTicks: int + ramCooldownTicks: int moveTracker: VirtualBodyTracker virtualHits: int virtualMiss: int @@ -82,14 +86,26 @@ type currentTargetId: int targetSwitchTick: int enemyTracker: EnemyTracker + prevGun: int + prevRadar: int + prevTarget: int proc printConfig(bot: ModularBot, changed: string = "") = - let gunName = GunNames[bot.currentGun] - let gunColor = if changed == "gun" or changed == "all": CLR_CHANGE else: CLR_RST - let radarColor = if changed == "radar" or changed == "all": CLR_CHANGE else: CLR_RST + let gunName = GunNames[bot.currentGun] let radarName = if bot.radarMode == 0: "radar_lock" else: "melee_scan" - let moveName = if bot.isRamming: "rammer" else: "phantom_meteor" - echo CLR_ROUND & "[config]" & CLR_RST & " " & gunColor & "gun=" & gunName & CLR_RST & " | " & CLR_MOVE & "move=" & moveName & CLR_RST & " | " & radarColor & "radar=" & radarName & CLR_RST & " | enemies=" & $bot.enemyTracker.allAlive().len & " target=#" & $bot.currentTargetId + let moveName = if bot.isRamming: "rammer" else: "tfil" + let gc = if bot.currentGun != bot.prevGun or changed == "all": CLR_CHANGE else: "" + let rc = if bot.radarMode != bot.prevRadar or changed == "all": CLR_CHANGE else: "" + let tc = if bot.currentTargetId != bot.prevTarget or changed == "all": CLR_CHANGE else: "" + let rst = CLR_RST + echo "[config] " & gc & "gun=" & gunName & (if gc != "": rst else: "") & + " | move=" & moveName & + " | " & rc & "radar=" & radarName & (if rc != "": rst else: "") & + " | enemies=" & $bot.enemyTracker.allAlive().len & + " target=" & tc & "#" & $bot.currentTargetId & (if tc != "": rst else: "") + bot.prevGun = bot.currentGun + bot.prevRadar = bot.radarMode + bot.prevTarget = bot.currentTargetId proc buildState(bot: ModularBot, ex, ey, espeed, eheading, eenergy: float): WorldState = var ei: seq[EnemyInfo] @@ -128,9 +144,11 @@ method onBulletHitBot*(bot: ModularBot, e: BulletHitBotEvent) = method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) = bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y) + bot.mover.removeBulletNear(e.bullet.x, e.bullet.y) method onBulletHitBullet*(bot: ModularBot, e: BulletHitBulletEvent) = bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y) + bot.mover.removeBulletNear(e.bullet.x, e.bullet.y) method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) = bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY()) @@ -147,6 +165,9 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) = bot.mover.resetRound() bot.enemyTracker.resetRound() bot.isRamming = false + bot.ramStuckTicks = 0 + bot.ramDurationTicks = 0 + bot.ramCooldownTicks = 0 bot.hasContact = false bot.tick = 0 bot.currentGun = -1 @@ -171,11 +192,15 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) = # tracker fitness persists across rounds (rolling window carries over) method onBotDeath*(bot: ModularBot, e: BotDeathEvent) = + echo "[death] victimId=", e.victimId, " wasTarget=", (e.victimId == bot.currentTargetId), + " trackerAlive=", (if bot.enemyTracker.enemies.contains(e.victimId): $bot.enemyTracker.enemies[e.victimId].alive else: "notInTracker") + echo CLR_MOVE & "[death] victimId=" & $e.victimId & " wasTarget=" & $(e.victimId == bot.currentTargetId) & CLR_RST bot.enemyTracker.markDead(e.victimId) if bot.enemyCount > 0: dec bot.enemyCount if e.victimId == bot.currentTargetId: bot.isRamming = false + bot.currentTargetId = -1 method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) = # minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1 @@ -201,9 +226,18 @@ method run*(bot: ModularBot) = while isRunning(): inc bot.tick - # Debug: print enemy state on first tick - if bot.tick == 1: - echo "[debug] enemyCount=" & $bot.enemyCount & " tracker=" & $bot.enemyTracker.allAlive().len & " radarMode=" & $bot.radarMode + # Ram cooldown countdown + if bot.ramCooldownTicks > 0: + dec bot.ramCooldownTicks + + # Safety: reset invalid target before any logic + if bot.currentTargetId >= 0: + if not bot.enemyTracker.enemies.contains(bot.currentTargetId) or + not bot.enemyTracker.enemies[bot.currentTargetId].alive: + echo CLR_MOVE & "[target-invalid] id=" & $bot.currentTargetId & " resetting" & CLR_RST + bot.currentTargetId = -1 + bot.isRamming = false + if not bot.hasContact: setTargetSpeed(0.0) @@ -212,25 +246,65 @@ method run*(bot: ModularBot) = go() continue - # Ram decision — harness decides, not the movement module + # --- Target selection (sticky) --- + let candidateId = selectTarget(bot.enemyTracker, getX(), getY(), tmClosest) + if bot.shouldSwitchTarget(candidateId): + if candidateId != bot.currentTargetId: + bot.currentTargetId = candidateId + bot.targetSwitchTick = bot.tick + bot.printConfig("target") + + # --- Refresh target state FIRST, before ram decision --- + let tid = bot.currentTargetId + if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and + bot.enemyTracker.enemies[tid].alive: + let tgt = bot.enemyTracker.getEnemy(tid) + bot.enemyBearing = directionTo(getX(), getY(), tgt.x, tgt.y) + bot.lastState = bot.buildState(tgt.x, tgt.y, tgt.speed, tgt.heading, tgt.energy) + + # Ram decision — harness decides, not the movement module (uses fresh lastState) let ws = bot.lastState let ramDist = hypot(ws.enemyX - ws.selfX, ws.enemyY - ws.selfY) - let ramFinisher = ramDist < 300.0 and ws.enemyEnergy < 20.0 and ws.selfEnergy > ws.enemyEnergy - let ramOpportunity = ramDist < 80.0 and ws.selfEnergy > ws.enemyEnergy + 10.0 - let ramDesperation = ws.selfEnergy < 5.0 and ws.enemyEnergy < 5.0 and ramDist < 150.0 - let shouldRam = ramFinisher or ramOpportunity or ramDesperation + let ramFinisher = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ramDist < 300.0 and ws.enemyEnergy < 20.0 and ws.selfEnergy > ws.enemyEnergy + let ramOpportunity = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ramDist < 50.0 and ws.selfEnergy > ws.enemyEnergy + 30.0 + let ramDesperation = bot.ramCooldownTicks == 0 and ws.enemyEnergy > 0 and ws.selfEnergy < 5.0 and ws.enemyEnergy < 5.0 and ramDist < 150.0 + let shouldRam = bot.currentTargetId >= 0 and + (ramFinisher or ramOpportunity or ramDesperation) if shouldRam and not bot.isRamming: bot.isRamming = true - let tag = if ramDesperation: "desperation" elif ramFinisher: "finisher" else: "opportunity" - echo CLR_MOVE & "[ram:" & tag & "] dist=" & $int(ramDist) & - " selfE=" & $int(ws.selfEnergy) & " enemyE=" & $int(ws.enemyEnergy) & CLR_RST - bot.printConfig("move") + bot.ramStuckTicks = 0 + bot.ramDurationTicks = 0 + if ramFinisher: + echo "[ram:enter] reason=finisher dist=", ramDist.int, " selfEnergy=", ws.selfEnergy.int, " enemyEnergy=", ws.enemyEnergy.int + elif ramOpportunity: + echo "[ram:enter] reason=opportunity dist=", ramDist.int, " energyAdv=", (ws.selfEnergy - ws.enemyEnergy).int + elif ramDesperation: + echo "[ram:enter] reason=desperation selfEnergy=", ws.selfEnergy.int elif not shouldRam and bot.isRamming: bot.isRamming = false - bot.printConfig("move") + + if bot.isRamming: + inc bot.ramDurationTicks + if ramDist < 5.0: + inc bot.ramStuckTicks + else: + bot.ramStuckTicks = 0 + if bot.ramStuckTicks > 10: + bot.isRamming = false + bot.currentTargetId = -1 + bot.ramStuckTicks = 0 + bot.ramDurationTicks = 0 + bot.ramCooldownTicks = 30 + elif bot.ramDurationTicks > 60: + bot.isRamming = false + bot.currentTargetId = -1 + bot.ramStuckTicks = 0 + bot.ramDurationTicks = 0 + bot.ramCooldownTicks = 30 let (spd, tr) = if shouldRam: + bot.mover.clearGraphics() bot.rammer.computeMove(ws) else: bot.mover.computeMove(ws) @@ -258,23 +332,9 @@ method run*(bot: ModularBot) = setRadarTurnRate(radarRate) go() - # --- Target selection (sticky) --- lock target during ram to prevent dangerous switch - if not bot.isRamming: - let candidateId = selectTarget(bot.enemyTracker, getX(), getY(), tmClosest) - if bot.shouldSwitchTarget(candidateId): - if candidateId != bot.currentTargetId: - bot.currentTargetId = candidateId - bot.targetSwitchTick = bot.tick - bot.printConfig("target") - # --- Aim + fire from tracked state (not scan event) --- - let tid = bot.currentTargetId if tid >= 0 and bot.enemyTracker.enemies.contains(tid) and bot.enemyTracker.enemies[tid].alive: - let tgt = bot.enemyTracker.getEnemy(tid) - bot.enemyBearing = directionTo(getX(), getY(), tgt.x, tgt.y) - bot.lastState = bot.buildState(tgt.x, tgt.y, tgt.speed, tgt.heading, tgt.energy) - # Spawn virtual bullets for all guns var headsUp: array[len(PowerBins), GunPrediction] var linPreds: array[len(PowerBins), GunPrediction] @@ -361,7 +421,6 @@ method run*(bot: ModularBot) = let (selectedGun, _, power) = selectShot(bot.tracker, tid) if selectedGun != bot.currentGun: bot.currentGun = selectedGun - bot.printConfig("gun") case selectedGun of 0: setTurretColor("#FF3333"); setBulletColor("#FF6666") of 1: setTurretColor("#3366FF"); setBulletColor("#6699FF") @@ -427,7 +486,7 @@ when isMainModule: knnGun: initKNNGun(), radar: RadarLockModule(), meleeScan: initMeleeScan(), - mover: initPhantomMeteor(), + mover: TFILModule(debugGraphics: true), rammer: initRammer(), moveTracker: mvb.initVirtualBodyTracker(1), currentGun: -1, diff --git a/common_libs/guns/accel_predictor.nim b/common_libs/guns/accel_predictor.nim index 338f256..1e66071 100644 --- a/common_libs/guns/accel_predictor.nim +++ b/common_libs/guns/accel_predictor.nim @@ -9,17 +9,18 @@ import gun_harness/gun_interface const MaxSpeed = 8.0 ## TR bot max speed (px/tick) type AccelGun* = object - prevSpeed: float - prevHeading: float - prevPrevSpeed: float - prevTick: int - frames: int ## how many observations we have - cachedAccel: float ## accel (px/tick²) cached per tick + prevSpeed: float + prevHeading: float + prevPrevSpeed: float + prevTick: int + frames: int ## how many observations we have + cachedAccel: float ## accel (px/tick²) cached per tick cachedTurnRate: float ## turn rate (deg/tick) cached per tick - cachedTick: int + cachedTick: int + debugGraphics*: bool proc initAccelGun*(): AccelGun = - AccelGun() + AccelGun(debugGraphics: false) proc predict*(g: var AccelGun, state: WorldState, bulletSpeed: float): GunPrediction = if bulletSpeed <= 0.0: diff --git a/common_libs/guns/anti_surfer.nim b/common_libs/guns/anti_surfer.nim index 592b629..2861909 100644 --- a/common_libs/guns/anti_surfer.nim +++ b/common_libs/guns/anti_surfer.nim @@ -21,9 +21,11 @@ type waves: seq[ASWave] cachedTick: int cachedWaveStored: bool + debugGraphics*: bool proc initAntiSurferGun*(): AntiSurferGun = result.cachedTick = -1 + result.debugGraphics = false # Same triangular head-on prior as GFGun — cold-start aims head-on. let center = (ASBins - 1) div 2 # = 15 for i in 0.. same prediction @@ -62,4 +63,4 @@ proc onResult*(g: var DisplacementGun, e: FeedbackEvent) = discard # analytical gun — no learning proc initDisplacementGun*(): DisplacementGun = - DisplacementGun(count: 0, head: 0, lastTick: -1) + DisplacementGun(count: 0, head: 0, lastTick: -1, debugGraphics: false) diff --git a/common_libs/guns/guess_factor.nim b/common_libs/guns/guess_factor.nim index 93dee1d..23eea71 100644 --- a/common_libs/guns/guess_factor.nim +++ b/common_libs/guns/guess_factor.nim @@ -17,14 +17,16 @@ type # mea not stored — recomputed from FeedbackEvent.bulletPower at resolution time GFGun* = object - bins: array[GFBins, float] - waves: seq[Wave] # pending unresolved waves + bins: array[GFBins, float] + waves: seq[Wave] # pending unresolved waves # per-tick cache: store wave only once across multiple power-bin calls - cachedTick: int + cachedTick: int cachedWaveStored: bool + debugGraphics*: bool proc initGFGun*(): GFGun = result.cachedTick = -1 + result.debugGraphics = false # 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. let center = (GFBins - 1) div 2 # = 15 diff --git a/common_libs/guns/head_on.nim b/common_libs/guns/head_on.nim index c33be38..f7f06d0 100644 --- a/common_libs/guns/head_on.nim +++ b/common_libs/guns/head_on.nim @@ -4,7 +4,7 @@ import gun_harness/gun_interface type HeadOnGun* = object - discard + debugGraphics*: bool proc predict*(g: var HeadOnGun, state: WorldState, bulletSpeed: float): GunPrediction = GunPrediction(x: state.enemyX, y: state.enemyY) diff --git a/common_libs/guns/knn_gun.nim b/common_libs/guns/knn_gun.nim index 6ef2079..badfaff 100644 --- a/common_libs/guns/knn_gun.nim +++ b/common_libs/guns/knn_gun.nim @@ -23,23 +23,25 @@ type feat: array[7, float] KNNGun* = object - obs: seq[Obs] - obsHead: int # ring-buffer write index - waves: seq[KNNWave] + obs: seq[Obs] + obsHead: int # ring-buffer write index + waves: seq[KNNWave] # per-tick cache - cachedTick: int - cachedWaveStored: bool + cachedTick: int + cachedWaveStored: bool # rolling normalization ranges - featMin: array[7, float] - featMax: array[7, float] + featMin: array[7, float] + featMax: array[7, float] # state for feature extraction - lastSpeed: float - lastDirection: float # +1 or -1 + lastSpeed: float + lastDirection: float # +1 or -1 timeSinceDirChange: int + debugGraphics*: bool proc initKNNGun*(): KNNGun = - result.cachedTick = -1 + result.cachedTick = -1 result.lastDirection = 1.0 + result.debugGraphics = false for i in 0..6: result.featMin[i] = 1e18 result.featMax[i] = -1e18 diff --git a/common_libs/guns/linear.nim b/common_libs/guns/linear.nim index 17500eb..d8f2750 100644 --- a/common_libs/guns/linear.nim +++ b/common_libs/guns/linear.nim @@ -5,7 +5,7 @@ import std/math import gun_harness/gun_interface type LinearGun* = object - discard + debugGraphics*: bool proc predict*(g: var LinearGun, state: WorldState, bulletSpeed: float): GunPrediction = let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) diff --git a/common_libs/guns/pattern_matcher.nim b/common_libs/guns/pattern_matcher.nim index f3b12c6..10fde3c 100644 --- a/common_libs/guns/pattern_matcher.nim +++ b/common_libs/guns/pattern_matcher.nim @@ -16,18 +16,19 @@ type headingDelta: float ## heading change in radians this tick PatternMatcherGun* = object - buf: array[HistorySize, MoveTick] - head: int ## next write index (circular) - count: int ## filled entries (capped at HistorySize) - prevHeading: float - prevSpeed: float - prevTick: int - hasPrev: bool + buf: array[HistorySize, MoveTick] + head: int ## next write index (circular) + count: int ## filled entries (capped at HistorySize) + prevHeading: float + prevSpeed: float + prevTick: int + hasPrev: bool # per-tick cache — avoid re-searching for multiple power bins - cacheTick: int - cacheX: float - cacheY: float - cacheValid: bool + cacheTick: int + cacheX: float + cacheY: float + cacheValid: bool + debugGraphics*: bool # --- circular buffer helpers --- diff --git a/common_libs/guns/stop_shot.nim b/common_libs/guns/stop_shot.nim index b7ed236..b649123 100644 --- a/common_libs/guns/stop_shot.nim +++ b/common_libs/guns/stop_shot.nim @@ -11,15 +11,16 @@ const BrakeDecel = 2.0 ## same-direction stop const CoastDecel = 1.0 ## cross-zero coasting stop type StopShotGun* = object - prevSpeed: float - prevHeading: float - prevTick: int - frames: int - cachedTick: int - cachedPredX: float - cachedPredY: float + prevSpeed: float + prevHeading: float + prevTick: int + frames: int + cachedTick: int + cachedPredX: 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 = if bulletSpeed <= 0.0: diff --git a/common_libs/guns/tsetlin.nim b/common_libs/guns/tsetlin.nim index 34688d1..e9b52a4 100644 --- a/common_libs/guns/tsetlin.nim +++ b/common_libs/guns/tsetlin.nim @@ -200,17 +200,19 @@ type alive: bool TsetlinGun* = object - net: TmNet - frameBuffer: array[TM_WINDOW_SIZE, TmFrameEncoded] - bufferCount: int - traces: array[TM_TRACE_SLOTS, TmTrace] - traceHead: int - shotCount: int ## total onResult calls received + net: TmNet + frameBuffer: array[TM_WINDOW_SIZE, TmFrameEncoded] + bufferCount: int + traces: array[TM_TRACE_SLOTS, TmTrace] + traceHead: int + shotCount: int ## total onResult calls received + debugGraphics*: bool proc initTsetlinGun*(): TsetlinGun = # states init at 0 (boundary); one Type I step crosses into Include for s in result.net.states.mitems: s = 0'i16 randomize() + result.debugGraphics = false proc isWarmedUp*(g: TsetlinGun): bool {.inline.} = g.bufferCount >= TM_WINDOW_SIZE diff --git a/common_libs/guns/wall_bounce.nim b/common_libs/guns/wall_bounce.nim index 8f4955a..f2f71cc 100644 --- a/common_libs/guns/wall_bounce.nim +++ b/common_libs/guns/wall_bounce.nim @@ -6,9 +6,9 @@ import std/math import gun_harness/gun_interface 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, arenaW, arenaH: float): (float, float) = diff --git a/common_libs/movements/minimum_risk.nim b/common_libs/movements/minimum_risk.nim index c5764e0..426ab3d 100644 --- a/common_libs/movements/minimum_risk.nim +++ b/common_libs/movements/minimum_risk.nim @@ -33,9 +33,10 @@ type MinimumRiskModule* = object targetX, targetY: float ticksSinceCalc: int hasTarget: bool + debugGraphics*: bool proc initMinimumRisk*(): MinimumRiskModule = - MinimumRiskModule(hasTarget: false, ticksSinceCalc: RecalcInterval) + MinimumRiskModule(hasTarget: false, ticksSinceCalc: RecalcInterval, debugGraphics: false) proc wallRisk(p: Vec2, w, h: float): float {.inline.} = ## Linear penalty that ramps up inside WallMargin. diff --git a/common_libs/movements/oscillator.nim b/common_libs/movements/oscillator.nim index edef994..71d14e4 100644 --- a/common_libs/movements/oscillator.nim +++ b/common_libs/movements/oscillator.nim @@ -14,12 +14,13 @@ const WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires type OscillatorModule* = object - sign: float ## +1 or -1, forward/backward relative to perp heading - elapsed: int ## ticks since last reversal - wallLockout: int ## remaining ticks where wall-reversal is suppressed + sign: float ## +1 or -1, forward/backward relative to perp heading + elapsed: int ## ticks since last reversal + wallLockout: int ## remaining ticks where wall-reversal is suppressed + debugGraphics*: bool 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 = inc m.elapsed diff --git a/common_libs/movements/phantom_meteor.nim b/common_libs/movements/phantom_meteor.nim index 590e215..e53899b 100644 --- a/common_libs/movements/phantom_meteor.nim +++ b/common_libs/movements/phantom_meteor.nim @@ -146,16 +146,17 @@ proc computeForces(eng: GravityEngine, botPos: Vec2, enemies: openArray[Vec2], a hasClose = true total = total + normalize(botPos - ph.pos) * (KBullet * (1.0 - d/250.0)) - # Priority 2: wall escape (additive — blends with phantom forces) - let minW = min(min(botPos.x, arenaW-botPos.x), min(botPos.y, arenaH-botPos.y)) - if minW < VeryClose: - var ex = 0.0; var ey = 0.0 - if botPos.x < VeryClose: ex = 1.0 - if arenaW - botPos.x < VeryClose: ex = -1.0 - if botPos.y < VeryClose: ey = 1.0 - if arenaH - botPos.y < VeryClose: ey = -1.0 - let wm = sqrt(ex*ex + ey*ey) - if wm > 0.1: total = total + normalize(vec2(ex, ey)) * 500.0 + # Priority 2: wall escape — scales as inverse-square so it dominates bullets near walls + var ex = 0.0; var ey = 0.0 + let dLeft = botPos.x + let dRight = arenaW - botPos.x + let dBot = botPos.y + let dTop = arenaH - botPos.y + if dLeft < WallMinDist: ex += KWall / (dLeft * dLeft) + if dRight < WallMinDist: ex -= KWall / (dRight * dRight) + if dBot < WallMinDist: ey += KWall / (dBot * dBot) + if dTop < WallMinDist: ey -= KWall / (dTop * dTop) + total = total + vec2(ex, ey) # Priority 3: repulsion from ALL enemies; attraction only toward nearest. # ponytail: linear ramp per enemy, attraction only toward nearest to avoid being pulled into a crowd @@ -181,23 +182,17 @@ proc computeForces(eng: GravityEngine, botPos: Vec2, enemies: openArray[Vec2], a # Too far from nearest only → mild attraction total = total + normalize(ep - botPos) * (KEnemy * t) - # Priority 4: weak wall repulsion - let dL = max(botPos.x, WallMinDist) - let dR = max(arenaW - botPos.x, WallMinDist) - let dB = max(botPos.y, WallMinDist) - let dT = max(arenaH - botPos.y, WallMinDist) - total = total + vec2(KWall*0.5/(dL*dL) - KWall*0.5/(dR*dR), - KWall*0.5/(dB*dB) - KWall*0.5/(dT*dT)) total # ── MovementModule wrapper ──────────────────────────────────────────────────── type PhantomMeteorModule* = object - engine: GravityEngine - gunheat: GunheatTracker + engine: GravityEngine + gunheat: GunheatTracker + debugGraphics*: bool proc initPhantomMeteor*(): PhantomMeteorModule = - PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker()) + PhantomMeteorModule(engine: initEngine(), gunheat: initGunheatTracker(), debugGraphics: false) proc resetRound*(m: var PhantomMeteorModule) = ## Clear per-round transients (phantoms, waves, energy baseline), keep histogram. @@ -264,27 +259,43 @@ proc computeMove*(m: var PhantomMeteorModule, ws: WorldState): MoveCommand = if force.magnitude < 1e-9: return (speed: 0.0, turnRate: 0.0) - # Threat centroid (inverse-distance weighted) for perpendicular direction. - # With 1 enemy this equals the single enemy bearing — identical to prior behavior. - var cx = 0.0; var cy = 0.0; var wsum = 0.0 - for ep in enemyVecs: - let d = max(dist(ep, botPos), 1.0) - let w = 1.0 / d - cx += ep.x * w; cy += ep.y * w; wsum += w - let centroid = if wsum > 1e-9: vec2(cx/wsum, cy/wsum) else: enemyPos - let centroidBearingRad = arctan2(centroid.y - botPos.y, centroid.x - botPos.x) + # Corner detection: near 2 walls simultaneously → hard escape override. + # ponytail: WallMinDist threshold reused; increase if bot still traps at higher speeds. + let nearL = botPos.x < WallMinDist + let nearR = ws.arenaWidth - botPos.x < WallMinDist + let nearB = botPos.y < WallMinDist + let nearT = ws.arenaHeight - botPos.y < WallMinDist + let wallCount = (if nearL: 1 else: 0) + (if nearR: 1 else: 0) + + (if nearB: 1 else: 0) + (if nearT: 1 else: 0) + let cornered = wallCount >= 2 - # Two perpendicular directions to centroid bearing (±90°) - let perpCCW = vec2(-sin(centroidBearingRad), cos(centroidBearingRad)) # +90° - let perpCW = vec2( sin(centroidBearingRad), -cos(centroidBearingRad)) # -90° - - # Pick perpendicular direction that aligns with force vector - let fn = force.normalize let desiredDeg = - if fn.x * perpCCW.x + fn.y * perpCCW.y >= fn.x * perpCW.x + fn.y * perpCW.y: - radToDeg(arctan2(perpCCW.y, perpCCW.x)) + if cornered: + # Escape direction = inward wall normals averaged; ignore bullet/enemy pulls. + # Enemy perp is irrelevant here — any direction away from corner is correct. + var escX = 0.0; var escY = 0.0 + if nearL: escX += 1.0 + if nearR: escX -= 1.0 + if nearB: escY += 1.0 + if nearT: escY -= 1.0 + let escDir = normalize(vec2(escX, escY)) + radToDeg(arctan2(escDir.y, escDir.x)) else: - radToDeg(arctan2(perpCW.y, perpCW.x)) + # Normal open-field: snap to perp that best aligns with force vector. + var cx = 0.0; var cy = 0.0; var wsum = 0.0 + for ep in enemyVecs: + let d = max(dist(ep, botPos), 1.0) + let w = 1.0 / d + cx += ep.x * w; cy += ep.y * w; wsum += w + let centroid = if wsum > 1e-9: vec2(cx/wsum, cy/wsum) else: enemyPos + let centroidBearingRad = arctan2(centroid.y - botPos.y, centroid.x - botPos.x) + let perpCCW = vec2(-sin(centroidBearingRad), cos(centroidBearingRad)) + let perpCW = vec2( sin(centroidBearingRad), -cos(centroidBearingRad)) + let fn = force.normalize + if fn.x * perpCCW.x + fn.y * perpCCW.y >= fn.x * perpCW.x + fn.y * perpCW.y: + radToDeg(arctan2(perpCCW.y, perpCCW.x)) + else: + radToDeg(arctan2(perpCW.y, perpCW.x)) # Delta from current heading var delta = desiredDeg - ws.selfHeading diff --git a/common_libs/movements/rammer.nim b/common_libs/movements/rammer.nim index 8a88678..42099cb 100644 --- a/common_libs/movements/rammer.nim +++ b/common_libs/movements/rammer.nim @@ -8,9 +8,9 @@ import movement_harness/movement_interface const MaxSpeed* = 8.0 type RammerModule* = object - discard + debugGraphics*: bool -proc initRammer*(): RammerModule = RammerModule() +proc initRammer*(): RammerModule = RammerModule(debugGraphics: false) proc computeMove*(m: var RammerModule, ws: WorldState): MoveCommand = let dx = ws.enemyX - ws.selfX diff --git a/common_libs/movements/random_oscillator.nim b/common_libs/movements/random_oscillator.nim index e8be9fe..0bb2b7d 100644 --- a/common_libs/movements/random_oscillator.nim +++ b/common_libs/movements/random_oscillator.nim @@ -15,17 +15,18 @@ const WallLockout* = 20 ## ticks to suppress further wall-reversals after one fires type RandomOscillatorModule* = object - sign: float ## +1 or -1 - elapsed: int ## ticks since last reversal - nextPeriod: int ## random period chosen at last reversal - wallLockout: int ## remaining suppression ticks - rng: Rand + sign: float ## +1 or -1 + elapsed: int ## ticks since last reversal + nextPeriod: int ## random period chosen at last reversal + wallLockout: int ## remaining suppression ticks + rng: Rand + debugGraphics*: bool proc initRandomOscillator*(): RandomOscillatorModule = var rng = initRand(getTime().toUnix()) let period = rng.rand(MinPeriod..MaxPeriod) 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 = inc m.elapsed diff --git a/common_libs/movements/the_floor_is_lava.nim b/common_libs/movements/the_floor_is_lava.nim new file mode 100644 index 0000000..9a106f4 --- /dev/null +++ b/common_libs/movements/the_floor_is_lava.nim @@ -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..= 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.. 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.. 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..= 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.. maxLava: maxLava = v + + # Non-zero tiles: colored border + colored value text (yellow→orange→red) + setFont("Arial", 10.0) + for row in 0.. 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..= 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.. 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..= 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.. 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) diff --git a/common_libs/movements/wave_surfer.nim b/common_libs/movements/wave_surfer.nim index 92c9b9f..91ac790 100644 --- a/common_libs/movements/wave_surfer.nim +++ b/common_libs/movements/wave_surfer.nim @@ -23,13 +23,14 @@ type startDist: float64 WaveSurferModule* = object - bins: array[WS_BINS, float64] - waves: seq[WSWave] - prevEnergy: float64 - strafeDir: float64 ## +1.0 or -1.0 + bins: array[WS_BINS, float64] + waves: seq[WSWave] + prevEnergy: float64 + strafeDir: float64 ## +1.0 or -1.0 + debugGraphics*: bool 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..