feat(ModularBot): pluggable bot with 4 guns, phantom meteor movement, radar harness
- Gun harness: virtual bullet tracker, rolling fitness, auto-selector - Guns: head-on, linear (extrapolation), circular (integrated formula), tsetlin machine (learning) - Movement: phantom meteor gravity engine (danger histograms, phantom bullets, fire detection) - Radar: harness + radar_lock adapter - Color-coded modules: turret/bullet color per gun, body per movement, scan per radar - Beats Target, SpinBot, Crazy, TrackFire in 10-round battles
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
## Virtual bullet tracker.
|
||||
## Spawns virtual bullets per gun×power bin every tick (no real firing).
|
||||
## Resolves by travel distance. Rolling window fitness per gun×power.
|
||||
## Calls onResult() on the owning gun when a bullet resolves.
|
||||
|
||||
import std/math
|
||||
import gun_interface
|
||||
|
||||
const
|
||||
PowerBins* = [1.0, 1.5, 2.0, 3.0] ## 4 bins; ponytail: fixed array, add runtime config if needed
|
||||
WindowSize* = 50 ## rolling window ticks for fitness
|
||||
MaxBullets* = 512 ## hard cap; ponytail: ring buffer, resize if more guns added
|
||||
MinHitRate* = 0.40 ## 40% threshold for acceptable power selection
|
||||
|
||||
type
|
||||
GunId* = int ## index into the guns seq
|
||||
|
||||
VirtualBullet* = object
|
||||
gunId*: GunId
|
||||
powerBin*: int ## index into PowerBins
|
||||
fireX*, fireY*: float
|
||||
aimX*, aimY*: float ## predicted target (absolute)
|
||||
bulletSpeed*: float
|
||||
travelDist*: float ## accumulated px so far
|
||||
fireDist*: float ## distance to target at fire time
|
||||
active*: bool
|
||||
|
||||
FitnessWindow* = object
|
||||
## Ring buffer of hit booleans.
|
||||
hits*: array[WindowSize, bool]
|
||||
count*: int ## total samples so far (capped at WindowSize for rate)
|
||||
head*: int
|
||||
|
||||
GunFitness* = object
|
||||
bins*: array[len(PowerBins), FitnessWindow]
|
||||
|
||||
VirtualTracker* = object
|
||||
bullets*: array[MaxBullets, VirtualBullet]
|
||||
head*: int ## ring buffer head
|
||||
fitness*: seq[GunFitness] ## indexed by GunId
|
||||
|
||||
proc initTracker*(numGuns: int): VirtualTracker =
|
||||
result.fitness = newSeq[GunFitness](numGuns)
|
||||
|
||||
proc hitRate*(fw: FitnessWindow): float =
|
||||
## Returns fraction of hits in the rolling window. 0.0 when no data.
|
||||
if fw.count == 0: return 0.0
|
||||
let n = min(fw.count, WindowSize)
|
||||
var h = 0
|
||||
for i in 0..<n: h += (if fw.hits[i]: 1 else: 0)
|
||||
result = h.float / n.float
|
||||
|
||||
proc record(fw: var FitnessWindow, hit: bool) =
|
||||
fw.hits[fw.head] = hit
|
||||
fw.head = (fw.head + 1) mod WindowSize
|
||||
inc fw.count
|
||||
|
||||
proc spawnBullets*(t: var VirtualTracker, gunId: GunId,
|
||||
predictions: array[len(PowerBins), GunPrediction],
|
||||
state: WorldState) =
|
||||
## Call once per gun per tick with predictions for all power bins.
|
||||
for binIdx in 0..<len(PowerBins):
|
||||
let power = PowerBins[binIdx]
|
||||
let speed = bulletSpeed(power)
|
||||
let pred = predictions[binIdx]
|
||||
let fireDist = hypot(pred.x - state.selfX, pred.y - state.selfY)
|
||||
let slot = t.head mod MaxBullets
|
||||
t.bullets[slot] = VirtualBullet(
|
||||
gunId: gunId,
|
||||
powerBin: binIdx,
|
||||
fireX: state.selfX,
|
||||
fireY: state.selfY,
|
||||
aimX: pred.x,
|
||||
aimY: pred.y,
|
||||
bulletSpeed: speed,
|
||||
travelDist: 0.0,
|
||||
fireDist: fireDist,
|
||||
active: true,
|
||||
)
|
||||
t.head = (t.head + 1) mod MaxBullets
|
||||
|
||||
proc tickBullets*(t: var VirtualTracker, state: WorldState,
|
||||
onResolved: proc(gunId: GunId, binIdx: int, e: FeedbackEvent)) =
|
||||
## Advance all active bullets one tick. Resolve when bullet reaches target distance.
|
||||
for i in 0..<MaxBullets:
|
||||
var b = addr t.bullets[i]
|
||||
if not b.active: continue
|
||||
b.travelDist += b.bulletSpeed
|
||||
if b.travelDist < b.fireDist: continue
|
||||
|
||||
# Resolved: compute miss distance against current enemy position
|
||||
# Direction from fire point to aim point
|
||||
let dx = b.aimX - b.fireX
|
||||
let dy = b.aimY - b.fireY
|
||||
let dist = hypot(dx, dy)
|
||||
let (bx, by) =
|
||||
if dist < 1e-6: (b.aimX, b.aimY)
|
||||
else: (b.fireX + dx / dist * b.travelDist,
|
||||
b.fireY + dy / dist * b.travelDist)
|
||||
let missDist = hypot(bx - state.enemyX, by - state.enemyY)
|
||||
let hit = missDist < BotRadius
|
||||
|
||||
t.fitness[b.gunId].bins[b.powerBin].record(hit)
|
||||
|
||||
let fe = FeedbackEvent(
|
||||
prediction: GunPrediction(x: b.aimX, y: b.aimY),
|
||||
bulletPower: PowerBins[b.powerBin],
|
||||
missDistance: missDist,
|
||||
hit: hit,
|
||||
)
|
||||
onResolved(b.gunId, b.powerBin, fe)
|
||||
b.active = false
|
||||
|
||||
proc bestPower*(t: VirtualTracker, gunId: GunId): (int, float) =
|
||||
## Returns (binIdx, power) with highest power that has >= MinHitRate.
|
||||
## Falls back to lowest power bin if nothing qualifies yet.
|
||||
result = (0, PowerBins[0])
|
||||
for binIdx in countdown(len(PowerBins) - 1, 0):
|
||||
let rate = t.fitness[gunId].bins[binIdx].hitRate()
|
||||
if rate >= MinHitRate or t.fitness[gunId].bins[binIdx].count == 0:
|
||||
return (binIdx, PowerBins[binIdx])
|
||||
|
||||
proc bestGun*(t: VirtualTracker): GunId =
|
||||
## Pick gun with highest hit rate across all power bins.
|
||||
## ponytail: O(n*bins), fine for small gun counts
|
||||
var bestRate = -1.0
|
||||
result = 0
|
||||
for gunId in 0..<t.fitness.len:
|
||||
for binIdx in 0..<len(PowerBins):
|
||||
let r = t.fitness[gunId].bins[binIdx].hitRate()
|
||||
if r > bestRate:
|
||||
bestRate = r
|
||||
result = gunId
|
||||
Reference in New Issue
Block a user