selector: arrival-accuracy tie-break measured NEGATIVE; randomness is load-bearing

Hypothesis under test (from the gun audit, which named the tie-band as "the
lever that matters most"): `bmPath` is deliberately generous (2.3-3.6x
`bmPoint`), so a gun can sit in the tied band on a ray that sweeps the target's
path while its bullets ARRIVE badly. So: keep the `path`-ranked band (path beat
point on real hit rate 7.43% vs 4.70%, z=5.56), but narrow the random draw
inside it using a parallel `point` (arrival-accuracy) window.

RESULT: NO EFFECT. Real DrussGT, ONE frozen binary (/tmp/ModularBot_tieband,
md5 2c0c56e6...), env knobs only, 7 runs x 7 rounds per arm, server-side events
sidecar, exact two-sided permutation test on per-run rates.

  arm                              runs  shots  real %  dmg/run   d      p
  tbbase (shipped)                    7   4128   7.17     175      --     --
  tbpt  path-rank + point-narrow      7   3938   7.08     165    +0.14  0.88
  tbpc  =commit control               7   3759   4.44      98    +2.74  0.0012
  tbpt25 point margin 0.25            7   3683   5.59     119    +1.65  0.20
  tbtie05 / tbtie40 (band width)      7   3937/3917  5.84/6.28  133/144  1.49/1.00  0.11/0.25
  tbwin50 (SelectorWindow=50)         7   3983   6.05     139    +1.20  0.11
  tbfloor10 (FloorPeakFrac=0.10)      7   3829   5.33     118    +2.12  0.11

tbpt vs base: fully overlapping ranges, p=0.88. This is a REAL null, not a dead
arm - the mechanism was live, and it visibly changed the selected-gun mix
(Pattern 24%->16%, Accel 6%->16%, Tsetlin ~0%->13%).

CONTROL VALIDATED, AND THIS IS THE THIRD TIME: removing the random draw inside
the band is SIGNIFICANTLY WORSE (4.44%, p=0.0012). Combined with the earlier
hysteresis A/B (7.02% -> 5.10% for commitment) and the light-hysteresis result,
the selector's per-tick randomness is now load-bearing on three independent
measurements. Narrowing the band on ANY second virtual statistic has not helped.

Every knob swept (band width, floor, window) is nominally worse than shipped at
n=7; that is "no credible win" rather than "proven harm" (sd ~1.8pp, ~1pp
resolution, underpowered).

Shipped default stays `GUN_SELECTOR_TIEBREAK=off`; the feature is opt-in, fully
guarded, and costs zero extra work on the default path (point windows are scored
only when the mode is on).

Guards: test_selector_tiebreak 19 (new, pure), test_gun_harness 39,
test_vbullet_metric 11, test_adaptive_radar 41, test_tfil_ring_weights 24,
test_power_policy 26, test_ram_decision 28, test_rack_membership 38,
acceptance_offline_vs_online 12/12 PASS (offline path calls neither
chooseFromFit nor the tie-break).

STRATEGIC CONCLUSION: three selection-side attempts have now failed (hysteresis,
commitment, point tie-break). The selector is at a local optimum and the
remaining lever is the QUALITY OF THE GUNS, not the selection among them.
This commit is contained in:
2026-09-22 01:07:12 +02:00
parent ca82053a11
commit 0ede6d12ec
4 changed files with 422 additions and 2 deletions
+178 -2
View File
@@ -128,6 +128,50 @@ proc parseSelectorMode*(value: string): SelectorMode =
let ActiveSelectorMode* = parseSelectorMode(getEnv(SelectorModeEnvVar, "relative"))
# ── arrival-accuracy tie-break (GUN_SELECTOR_TIEBREAK) ───────────────────────
#
# The shipped selector ranks guns by the `path` metric (the swept-ray score,
# measurably the better coarse signal: 7.43% vs 4.70% real hit rate) and then
# draws the shot UNIFORMLY at random inside a tie band built on that rate. The
# randomness is load-bearing (replacing it with commitment to the virtual best
# cost real hit rate, 7.02% -> 5.10%), but `path` is deliberately generous: it
# asks "does the ray ever sweep the target's path", which a gun can satisfy
# while its bullets ARRIVE poorly. Such a gun then sits inside the band and
# gets picked.
#
# The tie-break below keeps the band construction on `path` (and therefore the
# band's measured advantage) and makes the DRAW narrower by arrival accuracy:
# each virtual bullet is additionally scored with the `point` model (the
# prediction-accuracy score at the exact tick the bullet reaches its aim
# distance) into a parallel `pointBins` window, and the tied set is restricted
# to the guns whose point rate is within `GUN_SELECTOR_POINT_TIE` of the best
# point rate in the band. It is still a random draw inside a band — just a
# better-informed band — so the load-bearing randomness is preserved.
#
# `off` is the SHIPPED default, so an unset environment behaves exactly as
# before, and the parallel point scoring is not even performed.
const TieBreakEnvVar* = "GUN_SELECTOR_TIEBREAK"
type
TieBreakMode* = enum
tbOff ## DEFAULT: pure `path` band, uniform random draw.
tbPoint ## narrow the `path` tie band to the point-accurate guns.
tbPointCommit ## CONTROL (removes the randomness): take the best point rate
## inside the band, deterministically. Measured to be worse
## when done on the path rate; included so the point variant
## can be compared against its own commitment control.
proc parseTieBreak*(value: string): TieBreakMode =
## Empty / unknown values fall back to the shipped `off` and warn.
case value.strip().toLowerAscii()
of "", "off", "none", "0", "false": tbOff
of "point", "arrival", "p": tbPoint
of "commit", "pointcommit", "best": tbPointCommit
else:
stderr.writeLine("[gun_harness] unknown " & TieBreakEnvVar & "='" & value &
"'; falling back to 'off' (valid: off|point|commit)")
tbOff
# ── rack membership (melee vs 1v1) ────────────────────────────────────────────
#
# The selector can run two racks and switch between them on SERVER truth —
@@ -246,6 +290,25 @@ let ActiveShrink* = max(0.0, envFloat("GUN_SELECTOR_SHRINK", 20.0))
let ActiveDwellTicks* = max(0, envInt("GUN_SELECTOR_DWELL", GunDwellTicks))
let ActiveSwitchMargin* = max(0.0, envFloat("GUN_SELECTOR_MARGIN", GunSwitchMargin))
# Arrival-accuracy tie-break: mode plus the RELATIVE width of the point band
# (fraction of the best in-band point rate), matching the `RelTieMargin` style.
let ActiveTieBreak* = parseTieBreak(getEnv(TieBreakEnvVar, ""))
let ActivePointTie* = clamp(envFloat("GUN_SELECTOR_POINT_TIE", 0.5), 0.0, 1.0)
# A `point` tie-break needs the parallel point windows, which only the `path`
# metric records (under `point` the primary ranking already IS arrival
# accuracy, so the tie-break would be a no-op). Warn loudly rather than
# silently measuring nothing.
block:
if ActiveTieBreak != tbOff and ActiveMetric == bmPoint:
stderr.writeLine("[gun_harness] " & TieBreakEnvVar & "=" & $ActiveTieBreak &
" has no effect under " & MetricEnvVar & "=point (the primary " &
"ranking already uses arrival accuracy)")
elif ActiveTieBreak != tbOff:
# One audit line per process so a live run's log records which arm it ran.
stderr.writeLine("[gun_harness] " & TieBreakEnvVar & "=" & $ActiveTieBreak &
" pointTie=" & $ActivePointTie)
# Optional per-process seed so independent A/B runs use independent tie-breaks
# (Nim's default rand() stream is identical in every process, which would make
# "random" tie-breaks repeat across runs). Unset => leave the RNG untouched.
@@ -268,6 +331,8 @@ type
travelDist*: float ## accumulated px so far
fireDist*: float ## distance to target at fire time
active*: bool
pointScored*: bool ## the parallel point-model score for this flight has
## been recorded (tie-break mode only)
# --- path-metric bookkeeping (unused by the point metric) ---
hitSeen*: bool ## a swept segment already touched the target
bestMissDist*: float ## closest segment->target distance seen so far
@@ -282,6 +347,11 @@ type
GunFitness* = object
bins*: array[len(PowerBins), FitnessWindow]
pointBins*: array[len(PowerBins), FitnessWindow]
## Parallel `point`-model window, populated ONLY while the arrival-accuracy
## tie-break is active. The `path` ranking continues to read `bins`; the
## tie-break reads `pointBins` for the SAME bullets, so both scores come
## from one flight with no extra virtual bullets.
SelectorDiag* = object
## Optional observability for `chooseFromFit`/`bestGun`. Never needed by the
@@ -292,6 +362,9 @@ type
anyQualifies*: bool ## at least one gun reached MinObsBeforeCompete
floorRate*: float ## the floor actually applied this tick
incumbentKept*: bool ## hysteresis retained the incumbent this tick
pointTiedCount*: int ## guns kept after the arrival-accuracy tie-break
bestPointRate*: float ## best in-band point rate (the tie-break reference)
pointTieFired*: bool ## the tie-break actually narrowed the band
VirtualTracker* = object
bullets*: array[MaxBullets, VirtualBullet]
@@ -371,6 +444,7 @@ proc spawnBullets*(t: var VirtualTracker, gunId: GunId,
travelDist: 0.0,
fireDist: fireDist,
active: true,
pointScored: false,
hitSeen: false,
bestMissDist: Inf,
bestMissX: 0.0,
@@ -426,6 +500,31 @@ proc gunRate*(fit: GunFitness, pooled: bool): float =
let (h, n) = gunCounts(fit, pooled)
result = if n > 0: h.float / n.float else: 0.0
proc pointCounts*(fit: GunFitness, pooled: bool): tuple[hits, n: int] =
## Sample counts behind `pointRate`: the PARALLEL point-model window. All zero
## unless the arrival-accuracy tie-break is active, which is what makes the
## tie-break a graceful no-op on every existing caller.
if pooled:
for binIdx in 0..<len(PowerBins):
let m = min(fit.pointBins[binIdx].count, min(ActiveWindow, WindowSize))
result.n += m
result.hits += windowHits(fit.pointBins[binIdx], m)
else:
var best = -1.0
for binIdx in 0..<len(PowerBins):
let m = min(fit.pointBins[binIdx].count, min(ActiveWindow, WindowSize))
if m == 0: continue
let h = windowHits(fit.pointBins[binIdx], m)
let r = h.float / m.float
if r > best:
best = r
result = (h, m)
proc pointRate*(fit: GunFitness, pooled: bool): float =
## Arrival-accuracy rate over the parallel point window (0.0 when no data).
let (h, n) = pointCounts(fit, pooled)
result = if n > 0: h.float / n.float else: 0.0
proc rankScore(fit: GunFitness, pooled: bool, stat: RankStat,
fieldRate, shrinkK: float): float =
## Ranking statistic over the gun's window. All are monotone-ish in the mean,
@@ -478,7 +577,8 @@ proc noteBestRate*(t: var VirtualTracker) =
proc tickBullets*(t: var VirtualTracker, state: WorldState,
enemies: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]],
onResolved: proc(gunId: GunId, binIdx: int, e: FeedbackEvent)) =
onResolved: proc(gunId: GunId, binIdx: int, e: FeedbackEvent),
tieBreak: TieBreakMode = ActiveTieBreak) =
## Advance all active bullets one tick.
##
## The `bmPoint` branch (default) is unchanged: resolve when the bullet
@@ -563,6 +663,20 @@ proc tickBullets*(t: var VirtualTracker, state: WorldState,
let by = b.fireY + uy * b.travelDist
let segMiss = distPointToSegment(ex, ey, ax, ay, bx, by)
# Arrival-accuracy probe (tie-break only; skipped entirely when off). The
# tick the bullet first reaches its fire-time aim distance is exactly the
# tick `bmPoint` would resolve on, so this records the SAME outcome the
# point model would have — just into a parallel window, leaving the path
# score (and therefore the shipped ranking) untouched.
if tieBreak != tbOff and not b.pointScored and b.travelDist >= b.fireDist:
b.pointScored = true
let (px, py) =
if dist < 1e-6: (b.aimX, b.aimY)
else: (b.fireX + ux * b.fireDist, b.fireY + uy * b.fireDist)
let pMiss = hypot(px - ex, py - ey)
if b.targetId in t.fitness:
t.fitness[b.targetId][b.gunId].pointBins[b.powerBin].record(pMiss < BotRadius)
if not b.hitSeen:
if segMiss < BotRadius:
# First physical contact — freeze it so a later closer approach
@@ -627,6 +741,11 @@ proc fitnessFor*(t: VirtualTracker, targetId: int): seq[GunFitness] =
let src = perEnemy[gunId].bins[binIdx]
for k in 0..<min(src.count, WindowSize):
result[gunId].bins[binIdx].record(src.hits[k])
# The parallel point window (tie-break) must be merged by the same rule,
# or the aggregate fallback would silently lose the arrival score.
let psrc = perEnemy[gunId].pointBins[binIdx]
for k in 0..<min(psrc.count, WindowSize):
result[gunId].pointBins[binIdx].record(psrc.hits[k])
proc bestPower*(t: VirtualTracker, gunId: GunId, targetId: int = -1): (int, float) =
## Returns (binIdx, power). Prefers the HIGHEST power bin whose virtual hit
@@ -760,7 +879,9 @@ proc chooseFromFit*(fit: seq[GunFitness], diag: ptr SelectorDiag = nil,
incumbent: GunId = -1,
switchMargin = 0.0,
rackMode: RackMode = rm1v1,
membership: openArray[RackMembership] = []): GunId =
membership: openArray[RackMembership] = [],
tieBreak: TieBreakMode = ActiveTieBreak,
pointTieMargin: float = ActivePointTie): GunId =
## Core gun ranking over an already-resolved fitness seq. Split out from
## `bestGun` so the offline range can rank without copying a VirtualTracker,
## and so callers can request `diag` for the selection internals.
@@ -789,6 +910,12 @@ proc chooseFromFit*(fit: seq[GunFitness], diag: ptr SelectorDiag = nil,
## runs ONLY when a switch is actually permitted — a tick that retains the
## incumbent returns before `rand`, so the tie-break no longer re-decides
## every tick.
##
## `tieBreak` (see `TieBreakMode`) optionally narrows the tied set by the
## PARALLEL arrival-accuracy window (`GunFitness.pointBins`, filled by
## `tickBullets` only while the tie-break is active) before that random draw.
## The default is the shipped `off`, and a fitness seq with no point data
## leaves the band untouched, so every existing caller is byte-identical.
let pooled = if mode == smRelative: ActivePooled else: false
let admitted = admittedGuns(fit.len, rackMode, membership)
@@ -877,6 +1004,55 @@ proc chooseFromFit*(fit: seq[GunFitness], diag: ptr SelectorDiag = nil,
return incumbent
if tied.len == 0: return 0
# ── arrival-accuracy tie-break (GUN_SELECTOR_TIEBREAK) ──────────────────────
# `tied` is the `path` band (optionally already narrowed to the challengers
# that cleared the switch margin). Re-order it by the parallel point window so
# the uniform draw below lands on guns whose bullets actually ARRIVE, not just
# on guns whose ray sweeps the target generously.
#
# tbPoint — narrow the band to guns within `pointTieMargin` of the best
# in-band point rate, then keep the uniform random draw.
# tbPointCommit — CONTROL: deterministically take the best point rate,
# removing the random draw. Included so the narrowed band can
# be compared against its own commitment control.
#
# A gun with NO point samples is KEPT: it cannot be judged, and dropping it
# would turn "cold" into "bad". If no tied gun has any point data the band is
# left untouched, so the tie-break is a strict no-op until the point window
# warms up.
if tieBreak != tbOff and tied.len > 1:
var bestPoint = 0.0
var anyPoint = false
for g in tied:
let (h, n) = pointCounts(fit[g], pooled = true)
if n == 0: continue
anyPoint = true
bestPoint = max(bestPoint, h.float / n.float)
if diag != nil: diag[].bestPointRate = bestPoint
if anyPoint and bestPoint > 0.0:
if tieBreak == tbPointCommit:
var bestGun = tied[0]
var bestP = -1.0
for g in tied:
let p = pointRate(fit[g], pooled = true)
if p > bestP:
bestP = p
bestGun = g
if diag != nil:
diag[].pointTieFired = true
diag[].pointTiedCount = 1
return bestGun
var kept: seq[GunId]
for g in tied:
let (h, n) = pointCounts(fit[g], pooled = true)
if n == 0 or h.float / n.float >= bestPoint * (1.0 - pointTieMargin):
kept.add g
if kept.len > 0 and kept.len < tied.len:
if diag != nil: diag[].pointTieFired = true
tied = kept
if diag != nil: diag[].pointTiedCount = kept.len
result = tied[rand(tied.len - 1)]
proc bestGun*(t: VirtualTracker, targetId: int = -1,
@@ -0,0 +1,185 @@
## Pure guard for the selector's ARRIVAL-ACCURACY TIE-BREAK
## (`GUN_SELECTOR_TIEBREAK`, see common_libs/gun_harness/virtual_bullets.nim).
##
## No Java, no server, no battle:
## nim c -r common_libs/tests/test_selector_tiebreak.nim
##
## Two things are pinned here:
## 1. the RANKING rule — `chooseFromFit` narrows a `path` tie band by the
## parallel `point` (arrival-accuracy) window while still drawing the shot
## at random inside the narrowed band;
## 2. the RECORDING rule — `tickBullets` fills that parallel window from the
## exact tick the bullet reaches its aim distance, leaving the `path` score
## untouched.
import std/[math, random, tables, strformat]
import gun_harness/gun_interface
import gun_harness/virtual_bullets
var failures = 0
proc check(name: string, ok: bool) =
if ok: echo "PASS: ", name
else: echo "FAIL: ", name; inc failures
proc recordHit(fw: var FitnessWindow, hit: bool) =
fw.hits[fw.head] = hit
fw.head = (fw.head + 1) mod WindowSize
inc fw.count
proc seed(fit: var GunFitness, binIdx, hits, misses: int, point: bool) =
## Record `hits`/`misses` into the gun's path window (or the parallel point
## window when `point`).
var fw = if point: addr fit.pointBins[binIdx] else: addr fit.bins[binIdx]
for _ in 0..<hits: recordHit(fw[], true)
for _ in 0..<misses: recordHit(fw[], false)
proc mkFit(rates: openArray[tuple[pHits, pMiss, aHits, aMiss: int]]): seq[GunFitness] =
## One gun per entry: (path hits, path misses, point hits, point misses) in
## power bin 0. Both windows get the same >= MinObsBeforeCompete sample count.
result = newSeq[GunFitness](rates.len)
for i, r in rates:
result[i].seed(0, r.pHits, r.pMiss, point = false)
result[i].seed(0, r.aHits, r.aMiss, point = true)
# ── parsing / default ─────────────────────────────────────────────────────────
proc testParsing() =
check "tiebreak parse: empty -> shipped default (off)", parseTieBreak("") == tbOff
check "tiebreak parse: 'off' -> tbOff", parseTieBreak("off") == tbOff
check "tiebreak parse: 'point' -> tbPoint", parseTieBreak("point") == tbPoint
check "tiebreak parse: 'commit' -> tbPointCommit", parseTieBreak("commit") == tbPointCommit
check "tiebreak parse: case/space insensitive", parseTieBreak(" PoInT ") == tbPoint
check "tiebreak parse: unknown -> off (safe fallback, warns)",
parseTieBreak("definitely-not-a-mode") == tbOff
# ── ranking rule ──────────────────────────────────────────────────────────────
proc testNarrowingKeepsRandomness() =
## Three path-tied guns (10.0 / 9.5 / 9.0 %), all inside the 20% relative
## band. Arrival accuracy is sharply different: 1 / 4 / 3 %. With a 0.5
## relative point margin the band must lose gun 0 (0.01 < 0.5 * 0.04) but keep
## the uniform random draw over guns 1 and 2.
randomize(20240921)
let fit = mkFit([(100, 0, 1, 99), (95, 5, 40, 60), (90, 10, 30, 70)])
var seen: array[3, int]
var narrowed = 0
for _ in 0..<400:
var d: SelectorDiag
let g = chooseFromFit(fit, addr d, mode = smRelative,
tieBreak = tbPoint, pointTieMargin = 0.5)
if g >= 0 and g < 3: inc seen[g]
if d.pointTieFired and d.pointTiedCount == 2: inc narrowed
check "tiebreak(point): the bad-arrival gun is never drawn", seen[0] == 0
let bothDrawn = seen[1] > 0 and seen[2] > 0
check "tiebreak(point): both point-accurate tied guns are still drawn (randomness kept)", bothDrawn
check "tiebreak(point): the band really was narrowed (diag)", narrowed == 400
echo fmt" point draws: gun0={seen[0]} gun1={seen[1]} gun2={seen[2]} (n=400)"
proc testOffIsBaseline() =
## With the tie-break OFF the identical fitness set must still expose the
## bad-arrival gun — i.e. the shipped behaviour is unchanged.
randomize(20240921)
let fit = mkFit([(100, 0, 1, 99), (95, 5, 40, 60), (90, 10, 30, 70)])
var seen: array[3, int]
for _ in 0..<400:
let g = chooseFromFit(fit, nil, mode = smRelative, tieBreak = tbOff)
if g >= 0 and g < 3: inc seen[g]
check "tiebreak(off): the bad-arrival gun is still drawn (no behaviour change)",
seen[0] > 0 and seen[1] > 0 and seen[2] > 0
echo fmt" off draws: gun0={seen[0]} gun1={seen[1]} gun2={seen[2]} (n=400)"
proc testCommitIsDeterministic() =
## Control arm: `tbPointCommit` removes the random draw and takes the best
## in-band arrival rate.
randomize(1)
let fit = mkFit([(100, 0, 1, 99), (95, 5, 40, 60), (90, 10, 30, 70)])
var allOne = true
for _ in 0..<200:
if chooseFromFit(fit, nil, mode = smRelative,
tieBreak = tbPointCommit) != 1: allOne = false
check "tiebreak(commit): deterministically returns the best arrival gun", allOne
proc testColdPointIsNoOp() =
## With no point samples at all the tie-break must leave the band untouched.
randomize(7)
let fit = mkFit([(100, 0, 0, 0), (95, 5, 0, 0), (90, 10, 0, 0)])
var seen: array[3, int]
var fired = 0
for _ in 0..<400:
var d: SelectorDiag
let g = chooseFromFit(fit, addr d, mode = smRelative, tieBreak = tbPoint)
if g >= 0 and g < 3: inc seen[g]
if d.pointTieFired: inc fired
check "tiebreak(cold point data): band untouched, all tied guns drawn",
seen[0] > 0 and seen[1] > 0 and seen[2] > 0
check "tiebreak(cold point data): never reports a narrowing", fired == 0
proc testColdGunIsKept() =
## A gun with ZERO point samples cannot be judged and must be KEPT, while a
## gun with a measured bad arrival rate is dropped.
randomize(11)
let fit = mkFit([(100, 0, 0, 0), (95, 5, 40, 60), (90, 10, 1, 99)])
var seen: array[3, int]
for _ in 0..<400:
let g = chooseFromFit(fit, nil, mode = smRelative,
tieBreak = tbPoint, pointTieMargin = 0.5)
if g >= 0 and g < 3: inc seen[g]
check "tiebreak: a cold-on-point gun is kept (not judged bad)", seen[0] > 0
check "tiebreak: a measured bad-arrival gun is dropped", seen[2] == 0
check "tiebreak: the point-accurate gun is still drawn", seen[1] > 0
echo fmt" cold/mixed draws: gun0={seen[0]} gun1={seen[1]} gun2={seen[2]}"
# ── recording rule (tickBullets) ──────────────────────────────────────────────
proc mkState(tick: int, ex, ey: float): WorldState =
WorldState(selfX: 100.0, selfY: 100.0,
enemyX: ex, enemyY: ey, enemySpeed: 0.0, enemyHeading: 0.0,
arenaWidth: 1000.0, arenaHeight: 1000.0, tick: tick)
proc runOneBullet(tieBreak: TieBreakMode): tuple[samples, hits: int] =
## Spawn one bullet per power bin aimed at a stationary enemy 200 px away, then
## tick until it passes its aim distance. The parallel window must fill only
## when the tie-break is active.
let targetId = 7
var t = initTracker(1, bmPath)
let s0 = mkState(0, 300.0, 100.0)
let preds = [GunPrediction(x: 300.0, y: 100.0), GunPrediction(x: 300.0, y: 100.0),
GunPrediction(x: 300.0, y: 100.0), GunPrediction(x: 300.0, y: 100.0)]
t.spawnBullets(0, preds, s0, targetId)
for i in 1..<80:
let st = mkState(i, 300.0, 100.0)
var enemies: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
enemies[targetId] = (x: st.enemyX, y: st.enemyY, lastSeenTick: st.tick, alive: true)
t.tickBullets(st, enemies,
proc(gid: GunId, bin: int, e: FeedbackEvent) = discard, tieBreak = tieBreak)
let fit = t.fitnessFor(targetId)
for bin in 0..<len(PowerBins):
let n = min(fit[0].pointBins[bin].count, WindowSize)
result.samples += n
for k in 0..<n:
if fit[0].pointBins[bin].hits[k]: inc result.hits
proc testRecording() =
let on = runOneBullet(tbPoint)
let off = runOneBullet(tbOff)
check "recording: tie-break OFF records no parallel samples (zero overhead)",
off.samples == 0
check "recording: tie-break ON records one parallel sample per bullet",
on.samples == len(PowerBins)
check "recording: an aimed stationary target is a point hit",
on.hits == on.samples
# ── driver ────────────────────────────────────────────────────────────────────
testParsing()
testNarrowingKeepsRandomness()
testOffIsBaseline()
testCommitIsDeterministic()
testColdPointIsNoOp()
testColdGunIsKept()
testRecording()
if failures > 0:
echo "\n", failures, " check(s) FAILED"
quit(1)
echo "\nAll selector tie-break checks passed."
+52
View File
@@ -715,6 +715,58 @@ constructor, so ties resolved **identically across process restarts** — the
startup seed plus a `GUN_SELECTOR_SEED` override. Evidence: unseeded runs vary
across processes, seeded runs are identical. **[MEASURED]**.
### 6.8 The arrival-accuracy tie-band does not beat the shipped band
**Hypothesis.** Rank by `path` (robust, keeps its measured advantage) but narrow
the tied random draw by **arrival accuracy** (`point`): a gun whose ray sweeps
the target generously can sit in the band while its bullets arrive badly, so
making the band informative should improve the real hit rate without removing
the load-bearing randomness.
**Implementation** (`GUN_SELECTOR_TIEBREAK`, `common_libs/gun_harness/`,
default `off`): each virtual bullet is additionally scored with the `point`
model at the exact tick it reaches its aim distance into a parallel
`GunFitness.pointBins` window, and the `path` tie band is narrowed to the guns
within `GUN_SELECTOR_POINT_TIE` (default 0.5) of the best in-band point rate. The
uniform random draw over the narrowed band is kept. `GUN_SELECTOR_TIEBREAK=point`
selects it; `=commit` is the no-randomness control. `off` performs no parallel
scoring at all, so the shipped path is byte-identical. The recording rule and
the ranking rule are pinned by `common_libs/tests/test_selector_tiebreak.nim`
(19 checks, no battle).
**Live A/B** vs the real DrussGT, one frozen binary, 7 runs x 7 rounds per arm,
server-side events sidecar (~200-260 shots/run). `d = base - arm` (positive =
arm worse); p is the exact two-sided permutation test on per-run rates.
**[MEASURED]** (`/tmp/battle_tb*_r*.log`, `/tmp/events_tb*_r*.json`):
| Arm | Runs | Shots | Real % | dmg/run | d | p |
|---|---:|---:|---:|---:|---:|---:|
| `tbbase` (shipped) | 7 | 4,128 | **7.17** | **175** | — | — |
| `tbpt` (path + point narrow) | 7 | 3,938 | 7.08 | 165 | +0.14 | 0.88 |
| `tbpc` (`=commit` control) | 7 | 3,759 | 4.44 | 98 | +2.74 | **0.0012** |
| `tbpt25` (point margin 0.25) | 7 | 3,683 | 5.59 | 119 | +1.65 | 0.20 |
| `tbtie05` (`TIE=0.05`) | 7 | 3,937 | 5.84 | 133 | +1.49 | 0.11 |
| `tbtie40` (`TIE=0.40`) | 7 | 3,917 | 6.28 | 144 | +1.00 | 0.25 |
| `tbwin50` (`WINDOW=50`) | 7 | 3,983 | 6.05 | 139 | +1.20 | 0.11 |
| `tbfloor10` (`FLOOR=0.10`) | 7 | 3,829 | 5.33 | 118 | +2.12 | 0.11 |
The mechanism DID fire — the tie-break re-shaped the selection mix (over all 7
runs: Pattern 24%→16%, Accel 6%→16%, Tsetlin ~0%→13% under `tbpt`; the
no-randomness control `tbpc` collapses to HeadOn 43% vs 27%) — but it **did not
improve the real hit rate**: 7.08% vs 7.17%, fully
overlapping per-run ranges (base 5.29-8.73, arm 3.71-10.39), p = 0.88. The
no-randomness control `tbpc` is **significantly worse** (4.44%, p = 0.0012),
which independently replicates the earlier "commitment to the virtual best
costs real hit rate" result and validates that the arm was live. Every knob
variant (`TIE`, `FLOOR`, `WINDOW`) is also nominally *worse* than the shipped
values, none credibly better. **Verdict: clean negative — the shipped selector
is unchanged** (`GUN_SELECTOR_TIEBREAK` defaults to `off`).
This is consistent with §2: the virtual rate is a poor ranker, and the
selector's value is its **floor/tie hedging**, not the ordering it computes.
Narrowing the band with a second virtual statistic changes *which* guns are
drawn without making that draw any better.
---
## 7. Known caveats and open problems
+7
View File
@@ -78,3 +78,10 @@ distributions and paired permutation p = 0.57 / 0.21. Being below overall does
(`randomize()` reached only incidentally via the Tsetlin constructor); now an
explicit startup seed plus a `GUN_SELECTOR_SEED` override makes seeded runs
reproducible and unseeded runs vary.
8. **The arrival-accuracy tie-band does not beat the shipped band** (7 runs x 7
rounds/arm vs DrussGT, one frozen binary): path-ranking + point-narrowing
7.08% vs shipped 7.17% (overlapping, p = 0.88), while the no-randomness
control (`GUN_SELECTOR_TIEBREAK=commit`) is significantly worse at 4.44%
(p = 0.0012). The `TIE`/`FLOOR`/`WINDOW` sweep is also nominally worse at
every setting. **Shipped selector unchanged**; `GUN_SELECTOR_TIEBREAK`
defaults to `off`. Detail: [`gun_rack_analysis.md`](gun_rack_analysis.md) §6.8.