fix(gun_harness): min 15 obs before competing, window 50→100, KNN min k=5

Gun selector now gates competition: guns with <15 total observations across
all power bins sit out until at least one gun qualifies. Falls back to ungated
selection if no gun reaches threshold, preventing cold-start stalls.

Sliding window increased from 50 to 100 ticks to reduce switching noise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 11:59:00 +02:00
parent 651ce80620
commit 5bbc8cbda7
+27 -8
View File
@@ -8,9 +8,10 @@ import gun_interface
const const
PowerBins* = [1.0, 1.5, 2.0, 3.0] ## 4 bins; ponytail: fixed array, add runtime config if needed 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 WindowSize* = 100 ## rolling window ticks for fitness
MaxBullets* = 2048 ## hard cap; ponytail: ring buffer, resize if more guns added MaxBullets* = 2048 ## hard cap; ponytail: ring buffer, resize if more guns added
MinHitRate* = 0.40 ## 40% threshold for acceptable power selection MinHitRate* = 0.40 ## 40% threshold for acceptable power selection
MinObsBeforeCompete* = 15 ## min observations before a gun×bin enters competition
type type
GunId* = int ## index into the guns seq GunId* = int ## index into the guns seq
@@ -124,12 +125,30 @@ proc bestPower*(t: VirtualTracker, gunId: GunId): (int, float) =
proc bestGun*(t: VirtualTracker): GunId = proc bestGun*(t: VirtualTracker): GunId =
## Pick gun with highest hit rate across all power bins. ## Pick gun with highest hit rate across all power bins.
## Guns with fewer than MinObsBeforeCompete observations across all bins are
## skipped unless every gun is below threshold (then fall back to best of all).
## ponytail: O(n*bins), fine for small gun counts ## ponytail: O(n*bins), fine for small gun counts
var bestRate = -1.0 proc bestAmong(t: VirtualTracker, requireMin: bool): GunId =
result = 0 var bestRate = -1.0
result = 0
for gunId in 0..<t.fitness.len:
var maxCount = 0
for binIdx in 0..<len(PowerBins):
maxCount = max(maxCount, t.fitness[gunId].bins[binIdx].count)
if requireMin and maxCount < MinObsBeforeCompete: continue
for binIdx in 0..<len(PowerBins):
let r = t.fitness[gunId].bins[binIdx].hitRate()
if r > bestRate:
bestRate = r
result = gunId
# Try gated selection first; fall back to ungated if nothing qualifies
var anyQualifies = false
for gunId in 0..<t.fitness.len: for gunId in 0..<t.fitness.len:
for binIdx in 0..<len(PowerBins): for binIdx in 0..<len(PowerBins):
let r = t.fitness[gunId].bins[binIdx].hitRate() if t.fitness[gunId].bins[binIdx].count >= MinObsBeforeCompete:
if r > bestRate: anyQualifies = true
bestRate = r break
result = gunId if anyQualifies: break
result = if anyQualifies: t.bestAmong(true) else: t.bestAmong(false)