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:
@@ -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)
|
||||
Reference in New Issue
Block a user