## OFFLINE SWEEP: constant radial offset on the SHIPPED `Pattern` gun. ## ## Pattern's radial error was measured (measure_pattern_radial.nim) to be far ## smaller than the base linear forecast's: pooled median -3.2 px vs the base's ## -61 px, near/far 52.8%/45.0% vs 83.4%/14.4%. This sweep asks the empirical ## question anyway: does ANY constant radial offset on Pattern improve either ## virtual metric? ## ## `Pattern` is deterministic and history-dependent, so each fixture is replayed ## ONCE with a fresh gun per arm (one batched pass), exactly the methodology of ## sweep_radial_offset.nim / sweep_tm_pattern.nim. ## ## Arms: Linear (reference), Pattern at (1.0, 0.0) plus a grid of scale and ## fixed-px offsets, and a +30 px opposite-direction control. ## ## Usage: ## nim c -r -d:release --path:common_libs \ ## common_libs/tests/sweep_pattern_radial.nim --metric=point ## ... --metric=path ## Flags: --metric=point|path, --set=real|synthetic. import std/[os, strformat, strutils, json, tables, math, algorithm] import gun_harness/offline_range import gun_harness/gun_interface import gun_harness/virtual_bullets as vb import guns/linear import guns/pattern_matcher const repoRoot = currentSourcePath().parentDir.parentDir.parentDir fixturesDir = repoRoot / "tools" / "fixtures" type Adapt = object h100, n100, h300, n300, hall, nall, f100, m100: int rounds: int Arm = object name: string scale, offsetPx: float RoundSpan = tuple[start, count: int] var gDropped = 0 proc addAdapt(dst: var Adapt, src: Adapt) = inc dst.rounds, src.rounds dst.h100 += src.h100; dst.n100 += src.n100 dst.h300 += src.h300; dst.n300 += src.n300 dst.hall += src.hall; dst.nall += src.nall dst.f100 += src.f100; dst.m100 += src.m100 proc rateStr(h, n: int): string = if n == 0: " n/a " else: &"{h.float / n.float * 100.0:5.1f}%" proc binomPmf(k, n: int): float = if k < 0 or k > n: return 0.0 var lg = 0.0 for i in 1..k: lg += ln(float(n - k + i)) - ln(float(i)) exp(lg - float(n) * ln(2.0)) proc signTestP(wins, n: int): float = if n == 0: return 1.0 let lo = min(wins, n - wins) var s = 0.0 for k in 0..lo: s += binomPmf(k, n) min(1.0, 2.0 * s) proc loadRounds(path: string): seq[RoundSpan] = let dir = path.parentDir let base = path.extractFilename var side = dir / "drussgt_meta" / (base & ".rounds.json") if not fileExists(side): side = dir / (base & ".rounds.json") if not fileExists(side): return @[] let node = parseJson(readFile(side)) if not node.hasKey("rounds"): return @[] for r in node["rounds"]: result.add (r["startTick"].getInt(), r["count"].getInt()) proc resolve(name: string): tuple[fx: Fixture, path: string] = let p = if fileExists(name): name else: fixturesDir / (name & ".jsonl") (loadFixture(p), p) proc fixtureSet(name: string): seq[string] = if name == "synthetic": for n in SyntheticFixtureNames: result.add n else: for n in ["drussgt_vs_crazy", "drussgt_vs_spinbot", "drussgt_vs_drussgt", "tr_drussgt_vs_crazy", "tr_drussgt_vs_spinbot", "tr_drussgt_vs_modularbot"]: result.add(fixturesDir / (n & ".jsonl")) proc runRound(states: seq[WorldState], lastSeen: seq[int], enemyId, baseTick: int, drivers: seq[GunDriver], metric: BulletMetric): seq[Adapt] = var tracker = initTracker(drivers.len, metric) var accum = newSeq[Adapt](drivers.len) for ad in accum.mitems: inc ad.rounds for si in 0..= 0: lst = lastSeen[si] if state.enemies.len > 0: for e in state.enemies: enemyPositions[e.id] = (x: e.x, y: e.y, lastSeenTick: lst, alive: true) else: enemyPositions[enemyId] = (x: state.enemyX, y: state.enemyY, lastSeenTick: lst, alive: true) let localTick = state.tick - baseTick let dref = drivers tracker.tickBullets(state, enemyPositions, proc(gunId: GunId, binIdx: int, e: FeedbackEvent) = inc accum[gunId].nall if e.hit: inc accum[gunId].hall if localTick < 100: inc accum[gunId].n100 if e.hit: inc accum[gunId].h100 if localTick < 300: inc accum[gunId].n300 if e.hit: inc accum[gunId].h300 let fireTick = e.fireTick - baseTick if fireTick < 100: inc accum[gunId].m100 if e.hit: inc accum[gunId].f100 dref[gunId].resultCb(e)) gDropped += tracker.droppedBullets result = accum proc runFixtureClean(fx: Fixture, path: string, drivers: seq[GunDriver], metric: BulletMetric): seq[Adapt] = var spans = if fx.meta.source == "synthetic": @[(start: 0, count: fx.states.len)] else: loadRounds(path) result = newSeq[Adapt](drivers.len) if spans.len == 0: result = runRound(fx.states, fx.lastSeen, fx.enemyId, 0, drivers, metric) return for sp in spans: var st: seq[WorldState] var ls: seq[int] for i in 0..= sp.start and t < sp.start + sp.count: st.add fx.states[i] ls.add(if i < fx.lastSeen.len: fx.lastSeen[i] else: -1) if st.len == 0: continue let rr = runRound(st, ls, fx.enemyId, sp.start, drivers, metric) for gi in 0..B,nB>A,ties,p" for a in arms: if a.name in ["Linear", "Pattern"]: continue var winsA, winsB, ties, n = 0 for name in names: let (_, path) = resolve(name) let fxName = path.extractFilename.replace(".jsonl", "") var ha, na, hb, nb: int for row in rows: if row.fixture != fxName: continue if row.arm == a.name: ha += row.r.hall; na += row.r.nall elif row.arm == "Pattern": hb += row.r.hall; nb += row.r.nall if na == 0 or nb == 0: continue let ra = ha.float / na.float let rb = hb.float / nb.float inc n if ra > rb: inc winsA elif rb > ra: inc winsB else: inc ties echo &"{a.name},overall,{n},{winsA},{winsB},{ties},{signTestP(winsA, n - ties):.4f}" if gDropped > 0: echo &"\n# WARNING: droppedBullets={gDropped}" when isMainModule: main()