## 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..= 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.. 0: echo "\n", failures, " check(s) FAILED" quit(1) echo "\nAll selector tie-break checks passed."