148 lines
6.8 KiB
Nim
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)
|