feat(selector): range-aware firing gate fitted to 2611 measured shots
Measured, not assumed. With the gate temporarily opened to 20 deg, every real shot was logged (tick, angle error at fire time, distance, power, hit) across 3 gauntlets: 2611 shots, 57.3% aggregate. Findings: - The geometric cone atan(BotRadius/d) is directionally confirmed but a WEAK lever: even at 0.0-0.1 deg error the hit rate at 400-600px is only ~53-57%, because PREDICTION error dominates alignment error. - Real effect of tightening the gate: 57.9% -> 68.0% aggregate hit rate (fixed 0.1 deg), not the 76.9% previously reported -- that was a high-variance draw (per-rep 62.8/66.4/77.2%). - The shipped range-aware gate (SafetyFactor 0.6) does NOT beat the fixed 2.0 deg gate on hit rate (55.8% vs 57.9%, ~1.5 sigma, inside noise). It fires 22-28% more shots and therefore lands more total hits (~509 vs ~434 per rep). No per-adversary score delta exceeded the 300-point run-to-run noise band, so no config is demonstrably better on score. Shipped anyway because it is strictly more expressive (a fixed threshold is the special case), tunable from one const, and physically motivated, but the honest verdict is recorded in-code: the gate is not the bottleneck. AimThresholdDeg is removed; shouldFire now takes distPx. Degenerate or NaN distance falls back to the ceiling rather than dividing by zero. Also adds a per-shot logger to ModularBot behind 'const ShotLog' so the measurement above is reproducible, and 10 new guard checks (24 total, all passing) covering monotonicity, clamping, formula, perfect alignment, gross misalignment and degenerate distance. Cross-checked against the server source: the gun fires BEFORE the turn is applied, so the logged angle error is the true departure error, and fireAssist auto-aim is off (unset by the Nim API and forced false by setAdjustRadarForGunTurn).
This commit is contained in:
@@ -214,6 +214,57 @@ proc testPatternMatcherSpeedSensitivity() =
|
||||
check "pattern_matcher: same tick, different bulletSpeed -> different point",
|
||||
pointsDiffer(m0, m3)
|
||||
|
||||
# ── range-aware firing gate (Task B) ────────────────────────────────────────
|
||||
|
||||
proc testToleranceStrictlyDecreases() =
|
||||
# In the unclamped band the tolerance must fall monotonically with distance.
|
||||
# 100..10000 px sits strictly inside [floor, ceiling] for the shipped
|
||||
# SafetyFactor, so clamping cannot mask a flat or rising curve.
|
||||
let dists = [100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 10000.0]
|
||||
var ok = true
|
||||
for i in 0..<dists.high:
|
||||
if aimToleranceDeg(dists[i]) <= aimToleranceDeg(dists[i+1]): ok = false
|
||||
check "range gate: tolerance strictly decreases as distance grows (unclamped)",
|
||||
ok
|
||||
|
||||
proc testToleranceClamps() =
|
||||
let near = aimToleranceDeg(1.0) # point blank -> ceiling
|
||||
let far = aimToleranceDeg(1.0e9) # effectively infinite -> floor
|
||||
let mid = aimToleranceDeg(500.0)
|
||||
check "range gate: point-blank clamps to MaxAimThresholdDeg",
|
||||
near == MaxAimThresholdDeg
|
||||
check "range gate: extreme range clamps to MinAimThresholdDeg",
|
||||
far == MinAimThresholdDeg
|
||||
check "range gate: mid-range tolerance is strictly inside the clamps",
|
||||
mid > MinAimThresholdDeg and mid < MaxAimThresholdDeg
|
||||
|
||||
proc testToleranceFormula() =
|
||||
let d = 500.0
|
||||
let expected = radToDeg(arctan(BotRadius * SafetyFactor / d))
|
||||
check "range gate: tolerance matches radToDeg(arctan(BotRadius*SF/dist))",
|
||||
abs(aimToleranceDeg(d) - expected) < 1e-9
|
||||
|
||||
proc testPerfectAlignmentAlwaysFires() =
|
||||
var allPass = true
|
||||
for d in [0.0, 1.0, 100.0, 1000.0, 1.0e9]:
|
||||
if not shouldFire(100.0, 100.0, 0.0, d): allPass = false
|
||||
check "range gate: a perfectly aligned cool gun passes at every distance",
|
||||
allPass
|
||||
|
||||
proc testGrossMisalignmentFailsLongRange() =
|
||||
# 5 deg is far outside the ~0.8 deg cone at 800 px.
|
||||
check "range gate: gross misalignment fails at long range",
|
||||
not shouldFire(100.0, 105.0, 0.0, 800.0)
|
||||
|
||||
proc testDegenerateDistance() =
|
||||
let t = aimToleranceDeg(0.0)
|
||||
check "range gate: distPx = 0 falls back to the ceiling",
|
||||
t == MaxAimThresholdDeg
|
||||
check "range gate: distPx = 0 yields a finite, non-NaN tolerance",
|
||||
t == t and t < Inf and t > -Inf
|
||||
check "range gate: NaN distance also falls back to the ceiling",
|
||||
aimToleranceDeg(NaN) == MaxAimThresholdDeg
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
randomize()
|
||||
@@ -229,6 +280,12 @@ testStopShotSpeedSensitivity()
|
||||
testDisplacementSpeedSensitivity()
|
||||
testAveragedLeadSpeedSensitivity()
|
||||
testPatternMatcherSpeedSensitivity()
|
||||
testToleranceStrictlyDecreases()
|
||||
testToleranceClamps()
|
||||
testToleranceFormula()
|
||||
testPerfectAlignmentAlwaysFires()
|
||||
testGrossMisalignmentFailsLongRange()
|
||||
testDegenerateDistance()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
|
||||
Reference in New Issue
Block a user