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:
2026-09-20 00:37:10 +02:00
parent c9191b7afb
commit 254c7dc997
39 changed files with 2630 additions and 30 deletions
+133
View File
@@ -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