## Focused guard for the virtual-bullet METRIC switch ## (`GUN_VBULLET_METRIC`, see common_libs/gun_harness/virtual_bullets.nim). ## ## The switch selects how a virtual bullet is scored: ## point (default) — single point at the fire-time aim distance; ## path — swept-segment collision along the whole flight. ## ## This test pins the geometry that actually distinguishes the two models, and ## proves the switch is a real runtime override (not a compile-time constant) by ## driving BOTH models from one process via the explicit `initTracker` / ## `replayFixture` metric parameter. ## ## Run: nim c -r common_libs/tests/test_vbullet_metric.nim import std/[math, tables, strformat] import gun_harness/gun_interface import gun_harness/virtual_bullets import gun_harness/offline_range import guns/head_on import guns/linear var failures = 0 proc check(name: string, ok: bool) = if ok: echo "PASS: ", name else: echo "FAIL: ", name; inc failures # ── parsing / default ───────────────────────────────────────────────────────── proc testParsing() = check "metric parse: empty -> point (default)", parseMetric("") == bmPoint check "metric parse: 'point' -> bmPoint", parseMetric("point") == bmPoint check "metric parse: 'path' -> bmPath", parseMetric("path") == bmPath check "metric parse: case/space insensitive", parseMetric(" PaTh ") == bmPath check "metric parse: unknown -> point (safe fallback, warns)", parseMetric("definitely-not-a-metric") == bmPoint # ── geometry that distinguishes the models ──────────────────────────────────── 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(metric: BulletMetric, states: seq[WorldState], targetId = 7): tuple[hits, shots: int] = ## Spawn one bullet per power bin at tick 0, aimed at the enemy's tick-0 ## position, then tick the tracker over the rest of the stream. var t = initTracker(1, metric) let s0 = states[0] let preds = [GunPrediction(x: s0.enemyX, y: s0.enemyY), GunPrediction(x: s0.enemyX, y: s0.enemyY), GunPrediction(x: s0.enemyX, y: s0.enemyY), GunPrediction(x: s0.enemyX, y: s0.enemyY)] t.spawnBullets(0, preds, s0, targetId) for i in 1.. miss. The path model keeps the bullet ## flying until the wall and it physically catches up -> hit. var states: seq[WorldState] for t in 0..<140: states.add mkState(t, 300.0 + 4.0*t.float, 100.0) let p = runOneBullet(bmPoint, states) let q = runOneBullet(bmPath, states) check "receding target: point model scores 0 hits (enemy left the aim point)", p.hits == 0 check "receding target: path model catches the receding enemy", q.hits > 0 check "receding target: both models emit the same number of samples", p.shots == q.shots and q.shots == len(PowerBins) echo fmt" receding: point {p.hits}/{p.shots}, path {q.hits}/{q.shots}" proc testPerpendicularTarget() = ## Enemy crosses the ray perpendicularly. The ray and the enemy's trajectory ## intersect at the enemy's tick-0 position, but the bullet is 200 px away at ## that instant. A physical swept-collision model must MISS; a naive ## path-intersection model would wrongly hit. var states: seq[WorldState] for t in 0..<140: states.add mkState(t, 300.0, 100.0 + 8.0*t.float) let q = runOneBullet(bmPath, states) check "perpendicular target: path model misses (timing matters, not just path)", q.hits == 0 echo fmt" perpendicular: path {q.hits}/{q.shots}" # ── switch drives the offline replay ───────────────────────────────────────── proc testReplayMetricOverride() = let fx = synthesizeConstantVelocity(ticks = 140) let p = replayFixture(fx, @[makeDriver("Linear", LinearGun())], metric = bmPoint) let q = replayFixture(fx, @[makeDriver("Linear", LinearGun())], metric = bmPath) check "replay: both metrics record samples on a fixture", p[0].shots > 0 and q[0].shots > 0 check "replay: path model is deterministic run-to-run", block: let q2 = replayFixture(fx, @[makeDriver("Linear", LinearGun())], metric = bmPath) q2[0].hits == q[0].hits and q2[0].shots == q[0].shots echo fmt" constant-velocity Linear: point {p[0].hits}/{p[0].shots}, path {q[0].hits}/{q[0].shots}" # ── driver ──────────────────────────────────────────────────────────────────── testParsing() testRecedingTarget() testPerpendicularTarget() testReplayMetricOverride() if failures > 0: echo "\n", failures, " check(s) FAILED" quit(1) echo "\nAll virtual-bullet metric checks passed."