651ce80620
New DrussGT-inspired modules: - KNNGun: K-nearest-neighbor statistical targeting using GF density peaks - GunheatTracker: dual-heat system (predicted + confirmed) for 1-2 tick lead - ShadowTracker: computes GF regions safe from in-flight bullets (enemy wave dodge) VirtualBodyTracker now integrates gunheat for earlier fire detection and shadows for safe-zone multiplier (90% reduction in danger zones). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
6.6 KiB
Nim
163 lines
6.6 KiB
Nim
## Virtual body tracker — mirrors virtual_bullets.nim but for movement modules.
|
|
## Tracks simulated positions for each movement candidate under enemy waves.
|
|
## Scores modules by how often they sit in high-danger GF bins.
|
|
## Caller feeds actual hit data via registerHit() to build the danger profile.
|
|
|
|
import std/math
|
|
import gun_harness/gun_interface
|
|
import movement_harness/movement_interface
|
|
import movement_harness/gunheat_tracker
|
|
import movement_harness/bullet_shadows
|
|
|
|
const
|
|
VB_BINS* = 31 ## GF bins from -1 to +1
|
|
MaxWaves* = 64 ## hard cap; ponytail: ring buffer, resize if needed
|
|
MaxModules* = 8 ## max movement modules tracked
|
|
|
|
type
|
|
VBWave = object
|
|
fireX, fireY: float
|
|
fireBearingRad: float ## bearing from fireOrigin to REAL bot at fire time (radians)
|
|
speed: float
|
|
radius: float
|
|
fireTick: int
|
|
startDist: float
|
|
|
|
VirtualBody = object
|
|
x, y: float
|
|
heading: float ## degrees (Tank Royale / bot API)
|
|
speed: float ## current speed px/tick
|
|
|
|
VirtualBodyTracker* = object
|
|
numModules: int
|
|
bodies: array[MaxModules, VirtualBody]
|
|
dangerScore: array[MaxModules, float] ## accumulated danger hits
|
|
dangerBins: array[VB_BINS, float] ## populated from registerHit
|
|
waves: array[MaxWaves, VBWave]
|
|
waveCount: int
|
|
waveHead: int ## ring buffer head
|
|
gunheat: GunheatTracker
|
|
shadows*: ShadowTracker ## our bullets in flight for shadow computation
|
|
arenaW, arenaH: float
|
|
|
|
proc initVirtualBodyTracker*(numModules: int): VirtualBodyTracker =
|
|
result.numModules = numModules
|
|
result.gunheat = initGunheatTracker()
|
|
# Seed bins so we have a uniform prior before any real hits
|
|
for i in 0..<VB_BINS: result.dangerBins[i] = 1.0
|
|
|
|
proc resetRound*(t: var VirtualBodyTracker, startX, startY, startHeading, startSpeed: float) =
|
|
## Reset virtual body positions to actual bot position at round start.
|
|
for i in 0..<t.numModules:
|
|
t.bodies[i] = VirtualBody(x: startX, y: startY,
|
|
heading: startHeading, speed: startSpeed)
|
|
t.waveCount = 0
|
|
t.waveHead = 0
|
|
t.gunheat.resetRound()
|
|
|
|
proc registerHit*(t: var VirtualBodyTracker, bulletPower: float, bulletHeadingDeg: float,
|
|
realX, realY: float) =
|
|
## Feed an actual HitByBullet event in. Computes exact GF bin and increments danger.
|
|
## bulletHeadingDeg: the heading the bullet was travelling when it hit (degrees).
|
|
## We need the wave that fired it to get fireBearing; absent that, just increment center.
|
|
## ponytail: approximate — we don't correlate to exact wave here, just record impact direction.
|
|
## The bearing the bullet came FROM reversed = direction from fireOrigin to us at impact.
|
|
## Without access to the wave origin we use a weight-1 spike at center bin.
|
|
## TODO: correlate to nearest wave for exact GF if needed.
|
|
let centerBin = VB_BINS div 2
|
|
t.dangerBins[centerBin] += 2.0 # spike center on real hit
|
|
|
|
proc gfToBin(gf: float): int {.inline.} =
|
|
clamp(int(round((gf.clamp(-1.0, 1.0) + 1.0) * 0.5 * float(VB_BINS - 1))), 0, VB_BINS - 1)
|
|
|
|
proc mea(speed: float): float {.inline.} = arcsin(min(8.0 / speed, 1.0))
|
|
|
|
proc advanceBody(b: var VirtualBody, cmd: MoveCommand, arenaW, arenaH: float) =
|
|
## Single-tick physics: apply MoveCommand, clamp to arena.
|
|
let (targetSpeed, desiredTurn) = cmd
|
|
# maxTurnRate depends on current speed (Tank Royale formula)
|
|
let maxTurn = 10.0 - 0.75 * abs(b.speed)
|
|
b.heading += clamp(desiredTurn, -maxTurn, maxTurn)
|
|
# Speed ramps ±1 toward target, clamped to [-8, 8]
|
|
let tgt = clamp(targetSpeed, -8.0, 8.0)
|
|
if b.speed < tgt:
|
|
b.speed = min(b.speed + 1.0, tgt)
|
|
else:
|
|
b.speed = max(b.speed - 1.0, tgt)
|
|
let headingRad = degToRad(b.heading)
|
|
b.x += b.speed * cos(headingRad)
|
|
b.y += b.speed * sin(headingRad)
|
|
b.x = clamp(b.x, 18.0, arenaW - 18.0)
|
|
b.y = clamp(b.y, 18.0, arenaH - 18.0)
|
|
|
|
proc tick*[N: static int](t: var VirtualBodyTracker, state: WorldState,
|
|
cmds: array[N, MoveCommand]) =
|
|
## Per-tick update. cmds[i] is computeMove() output of module i.
|
|
## Call AFTER collecting all module commands for this tick.
|
|
|
|
# --- Fire detection via gunheat tracker ---
|
|
let waveEvents = t.gunheat.tick(state)
|
|
for ev in waveEvents:
|
|
let bspeed = 20.0 - 3.0 * ev.bulletPower
|
|
let bearingRad = arctan2(state.selfY - ev.fireY, state.selfX - ev.fireX)
|
|
let d = hypot(state.selfX - ev.fireX, state.selfY - ev.fireY)
|
|
let slot = t.waveHead mod MaxWaves
|
|
t.waves[slot] = VBWave(
|
|
fireX: ev.fireX,
|
|
fireY: ev.fireY,
|
|
fireBearingRad: bearingRad,
|
|
speed: bspeed,
|
|
radius: 0.0,
|
|
fireTick: ev.tick,
|
|
startDist: d,
|
|
)
|
|
t.waveHead = (t.waveHead + 1) mod MaxWaves
|
|
if t.waveCount < MaxWaves: inc t.waveCount
|
|
|
|
# --- Advance virtual bodies ---
|
|
for i in 0..<t.numModules:
|
|
when N > MaxModules:
|
|
{.error: "cmds array exceeds MaxModules".}
|
|
if i < N:
|
|
advanceBody(t.bodies[i], cmds[i], state.arenaWidth, state.arenaHeight)
|
|
|
|
# --- Advance our bullets (for shadow tracking) ---
|
|
t.shadows.tick()
|
|
|
|
# --- Advance waves and score ---
|
|
# ponytail: O(waves * modules), small counts, fine
|
|
for wi in 0..<t.waveCount:
|
|
let idx = wi mod MaxWaves
|
|
var w = addr t.waves[idx]
|
|
if w.speed <= 0.0: continue
|
|
w.radius += w.speed
|
|
# Compute bullet shadows for this wave once; reuse across all virtual bodies
|
|
let waveShadows = t.shadows.getShadows(w.fireX, w.fireY,
|
|
w.fireBearingRad, w.radius, w.speed)
|
|
for mi in 0..<t.numModules:
|
|
let bx = t.bodies[mi].x
|
|
let by = t.bodies[mi].y
|
|
let d = hypot(bx - w.fireX, by - w.fireY)
|
|
# Wave crosses virtual body: within BotRadius of its current position
|
|
if abs(w.radius - d) <= BotRadius:
|
|
let toBotRad = arctan2(by - w.fireY, bx - w.fireX)
|
|
var off = toBotRad - w.fireBearingRad
|
|
while off > PI: off -= 2.0 * PI
|
|
while off < -PI: off += 2.0 * PI
|
|
let maxA = mea(w.speed)
|
|
if maxA >= 1e-9:
|
|
let gf = clamp(off / maxA, -1.0, 1.0)
|
|
let bin = gfToBin(gf)
|
|
# Shadow zones are guaranteed safe — reduce danger by 90%
|
|
let shadowMul = if isShadowed(waveShadows, gf): 0.1 else: 1.0
|
|
t.dangerScore[mi] += t.dangerBins[bin] * shadowMul
|
|
|
|
proc bestMovement*(t: VirtualBodyTracker): int =
|
|
## Returns index of module with lowest accumulated danger score.
|
|
result = 0
|
|
var best = t.dangerScore[0]
|
|
for i in 1..<t.numModules:
|
|
if t.dangerScore[i] < best:
|
|
best = t.dangerScore[i]
|
|
result = i
|