Files
SirRoboGarage/common_libs/tests/test_strafe_fire_fix.nim
T

148 lines
6.8 KiB
Nim

## Unit checks for the j133/j134 fire-detection fix (`TR_STRAFE_FIRE_FIX`).
##
## NO battle, NO Java, NO server. Run with:
## nim c -r --nimcache:/tmp/nc_j133 --path:common_libs \
## common_libs/tests/test_strafe_fire_fix.nim
##
## The shipped detector classified the enemy energy delta `prev - cur` with the
## single window `0.09 <= drop <= 3.01`. Two SERVER facts break that window:
##
## 1. `BULLET_HIT_ENERGY_GAIN_FACTOR = 3` (server rules.kt): when an ENEMY
## bullet hits us, the SHOOTER's energy RISES by `3*power`. If it fired in
## the same tick the two cancel and the net delta reads as "no fire" ->
## the bullet gets NO heat.
## 2. our own bullet damaging the enemy in the same tick adds `damage` to the
## delta, so it can exceed 3.01 -> REJECTED, no heat for the enemy's shot.
##
## The fix undoes both from the events (`noteEnemyBulletHit` / `noteDamageDealt`)
## and splits any still-too-large delta instead of dropping it. These checks pin
## the four behaviours plus the OFF-switch parity with the shipped detector.
##
## ── The one-SCAN delay (j134, MEASURED LIVE) ────────────────────────────────
## The correction is NOT applied to the immediately-next reading. The server
## emits the hit event on turn N but applies the energy change to turn N+1's
## reading (live evidence: `EV hit getTurn=67` -> the `raw=-5.803` gain appears
## in the `getTurn=68` reading). Because `note*` runs during `go()`, AFTER that
## turn's `endScan`, the tracker keeps a one-slot double buffer so the
## correction lands on the reading that carries the change. A unit test must
## therefore feed: (1) a scan to rotate the event in, then (2) the reading whose
## raw shows the effect. `advance` below does exactly that.
import std/[math, os]
import gun_harness/gun_interface
# Private-field access: include (do NOT import) the mover, exactly as
# `measure_strafe_heat_display.nim` does for the display check.
include movements/strafe
var failures = 0
proc check(name: string, ok: bool) =
if ok: echo "PASS: ", name
else: echo "FAIL: ", name; inc failures
proc ws(ee: float): WorldState =
WorldState(
enemyX: 200.0, enemyY: 200.0, enemyEnergy: ee,
selfX: 400.0, selfY: 300.0, selfHeading: 0.0, selfSpeed: 6.0,
arenaWidth: 800.0, arenaHeight: 600.0, tick: 10,
enemies: @[EnemyInfo(id: 1, x: 200.0, y: 200.0, energy: ee)])
proc freshModule(): StrafeModule =
var m = initStrafe()
m.initGrid(800.0, 600.0)
m.resetRound()
m
# ── 1. plain fire (unchanged behaviour) ───────────────────────────────────────
block:
var mm = freshModule()
mm.detectFires(ws(100.0))
mm.detectFires(ws(97.95)) # 2.05 drop, in range -> one wave
check "plain fire creates exactly one wave", mm.bullets.len == 1
check "plain fire wave power == drop",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.05) < 1e-9
# ── 2. the +3*power bonus masks a fire (the owner's "bullet without heat") ────
block:
# Enemy at 100 fires 0.35 while an EARLIER 0.45 bullet of its own hits us
# (+3*0.45 = +1.35). Net energy = 100 - 0.35 + 1.35 = 101.0 -> raw drop = -1.0.
var mm = freshModule()
mm.detectFires(ws(100.0)) # establish prev = 100
mm.noteEnemyBulletHit(0.45) # event noted (staged)
mm.detectFires(ws(100.0)) # rotates the staged correction in
mm.detectFires(ws(101.0)) # the reading carrying the change
check "masked fire: WITH fix a wave is created", mm.bullets.len == 1
check "masked fire: WITH fix the power is recovered",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 0.35) < 1e-9
# ── 3. our own damage inflates the drop past the cap ─────────────────────────
block:
# Enemy at 50 fires 2.0 and our bullet hits it for 1.8 -> energy 46.2,
# raw drop 3.8 > 3.01 (shipped detector rejects it outright).
var mm = freshModule()
mm.prevEnergySet(1, 50.0)
mm.noteDamageDealt(1.8)
mm.detectFires(ws(50.0)) # rotates the staged correction in
mm.detectFires(ws(46.2)) # raw 3.8 - 1.8 = 2.0 -> wave
check "inflated drop: WITH fix a wave is created", mm.bullets.len == 1
check "inflated drop: WITH fix the power is recovered",
mm.bullets.len == 1 and abs(mm.bullets[0].power - 2.0) < 1e-9
# ── 4. never silently drop a too-large delta (unobserved contamination) ───────
block:
var mm = freshModule()
mm.detectFires(ws(100.0))
mm.detectFires(ws(93.0)) # 7.0 with no events -> split, not dropped
check "too-large drop is SPLIT, never dropped", mm.bullets.len == 3
check "split waves all within the power cap",
mm.bullets.len == 3 and mm.bullets[0].power <= 3.0 and mm.bullets[1].power <= 3.0
check "split waves sum back to the drop",
mm.bullets.len == 3 and abs(mm.bullets[0].power * 3.0 - 7.0) < 1e-9
# ── 5. OFF switch == shipped detector, byte for byte ─────────────────────────
block:
putEnv("TR_STRAFE_FIRE_FIX", "0")
loadStrafeEnv()
# (a) masked fire: shipped detector sees raw = -1.0 -> NO wave.
var a = freshModule()
a.detectFires(ws(100.0))
a.noteEnemyBulletHit(0.45) # must be a no-op when the fix is off
a.detectFires(ws(100.0))
a.detectFires(ws(101.0))
check "OFF: masked fire is NOT detected (shipped behaviour)", a.bullets.len == 0
# (b) inflated drop: shipped detector sees 3.8 -> rejected -> NO wave.
var b = freshModule()
b.prevEnergySet(1, 50.0)
b.noteDamageDealt(1.8)
b.detectFires(ws(50.0))
b.detectFires(ws(46.2))
check "OFF: inflated drop is rejected (shipped behaviour)", b.bullets.len == 0
# (c) in-range drop is still caught exactly once.
var c = freshModule()
c.detectFires(ws(100.0))
c.detectFires(ws(98.0))
check "OFF: plain fire still caught once",
c.bullets.len == 1 and abs(c.bullets[0].power - 2.0) < 1e-9
delEnv("TR_STRAFE_FIRE_FIX")
loadStrafeEnv()
# ── 6. default is ON ─────────────────────────────────────────────────────────
block:
delEnv("TR_STRAFE_FIRE_FIX")
delEnv("TR_FIRE_FIX")
loadStrafeEnv()
check "default TR_STRAFE_FIRE_FIX is ON", StrafeFireFix
# ── 7. the global TR_FIRE_FIX also gates STRAFE ──────────────────────────────
block:
putEnv("TR_FIRE_FIX", "0")
loadStrafeEnv()
check "TR_FIRE_FIX=0 disables STRAFE too", not StrafeFireFix
delEnv("TR_FIRE_FIX")
loadStrafeEnv()
if failures == 0:
echo "\nAll STRAFE fire-detection fix checks passed."
else:
echo "\n", failures, " STRAFE fire-detection fix check(s) FAILED."
quit(1)