## 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* = 2048 ## 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..= 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.. bestRate: bestRate = r result = gunId