fb36a0a685
The belief "BotDeathEvent never reaches ModularBot, so enemyTracker keeps dead
enemies alive forever" was written into a code comment and then believed twice.
It is FALSE. Measured in a 7-bot melee with a per-tick probe comparing
enemyTracker's alive count against the server's getEnemyCount():
metric 1.3.1 (20 rd) 0.35.5 (15 rd)
observed enemy deaths 83 68
...non-round-ending 83 (100%) 66 (97%)
ekBotDeath events DROPPED 0 0
max dispatch lag (turns behind) 1 1
phantom ticks 1 / 16,820 1 / 12,596
MAX CORPSE LIFETIME 0 ticks 0 ticks
victims still alive at round end 0 0
onBotDeath fires for every death, including non-round-ending ones. The
API-level event-drop mechanism IS real (test_event_drop_mechanism.nim proves
it: ekBotDeath is not in isCritical and MAX_EVENTS_AGE=2) - the bot simply
never falls far enough behind for it to trigger (max lag 1 turn).
Removed:
- reconcileWithServer + ReconcilePersistTicks/mismatchTicks/sawServerAlive
(uncommitted, and ON BY DEFAULT despite the premise being false). Its own
comment admitted a shorter window once KILLED A LIVE ENEMY ("it fired three
more times after the tracker marked it dead") - a latent mis-prune path
defending against a bug that does not exist.
- The radar's CorpseTicks=40 filter and the same-class age>60 filter in
recordRadarStats, both carrying the false comment. Removal changes no real
behaviour: buildState feeds the radar enemyTracker.allAlive(), so a dead
enemy never reaches computeScan.
Kept:
- The TR_TRACKER_PROBE instrument (default OFF), which produced the table above.
- test_event_drop_mechanism.nim - the drop mechanism is a genuine library
behaviour worth guarding.
- isAlive/aliveCount on the tracker.
Added: docs/tracker_death_events.md (the durable negative, so this is not
re-invented a third time) and test_enemy_tracker_death.nim (13 checks) in place
of the test for the deleted feature.
Guards: test_gun_harness 39/39, test_vbullet_metric 11, test_power_selection 3,
test_adaptive_radar 41/41, test_event_drop_mechanism 6, test_enemy_tracker_death
13, acceptance 12/12, ModularBot compiles.
96 lines
3.3 KiB
Nim
96 lines
3.3 KiB
Nim
## Unit tests for EnemyTracker's death invariant: once an enemy is marked dead
|
|
## it stays dead, and every alive view excludes it.
|
|
##
|
|
## This is the invariant that actually matters in production. The earlier
|
|
## premise — "BotDeathEvent never reaches the bot, so corpses persist" — was
|
|
## MEASURED false (0 dropped BotDeath events, 0 corpse ticks; see
|
|
## docs/tracker_death_events.md). The reconciliation feature that used to be
|
|
## tested here was retired with it. The event-drop MECHANISM is real and is
|
|
## covered separately by test_event_drop_mechanism.nim.
|
|
##
|
|
## Headless: no Java, no server, no battle.
|
|
## nim c -r common_libs/tests/test_enemy_tracker_death.nim
|
|
|
|
import std/[tables, algorithm]
|
|
import targeting/enemy_tracker
|
|
|
|
var failures = 0
|
|
proc check(name: string, ok: bool) =
|
|
if ok: echo "PASS: ", name
|
|
else: echo "FAIL: ", name; inc failures
|
|
|
|
proc see(et: var EnemyTracker, id, tick: int) =
|
|
et.update(id, float(id) * 100.0, 0.0, 0.0, 0.0, 100.0, tick)
|
|
|
|
proc testMarkDeadImmediate() =
|
|
var et: EnemyTracker
|
|
et.see(5, 10)
|
|
check "precondition: a scanned enemy is alive", et.isAlive(5)
|
|
et.markDead(5)
|
|
# Same tick: no intervening update, no reconciliation.
|
|
check "immediate: markDead flips the flag in the same tick", not et.isAlive(5)
|
|
check "immediate: aliveCount drops the dead enemy at once", et.aliveCount == 0
|
|
check "immediate: allAlive drops the dead enemy at once", et.allAlive.len == 0
|
|
|
|
proc testNoResurrection() =
|
|
var et: EnemyTracker
|
|
et.see(5, 10)
|
|
et.markDead(5)
|
|
# A later scan of the same id (queued/duplicate scan) must not revive it.
|
|
et.update(5, 1.0, 2.0, 3.0, 4.0, 100.0, 11)
|
|
check "no-resurrect: a later update does not revive a dead enemy",
|
|
not et.isAlive(5)
|
|
check "no-resurrect: the dead enemy is not counted alive", et.aliveCount == 0
|
|
check "no-resurrect: allAlive still excludes it", et.allAlive.len == 0
|
|
|
|
proc testAliveViewsExcludeDead() =
|
|
var et: EnemyTracker
|
|
et.see(1, 10)
|
|
et.see(2, 10)
|
|
et.see(3, 10)
|
|
et.markDead(2)
|
|
check "views: isAlive is false only for the dead id",
|
|
et.isAlive(1) and not et.isAlive(2) and et.isAlive(3)
|
|
check "views: aliveCount counts only the living", et.aliveCount == 2
|
|
var ids: seq[int]
|
|
for s in et.allAlive(): ids.add s.id
|
|
ids.sort()
|
|
check "views: allAlive returns exactly the living ids", ids == @[1, 3]
|
|
|
|
proc testMarkDeadUnknownIsNoop() =
|
|
var et: EnemyTracker
|
|
et.see(1, 10)
|
|
et.markDead(99)
|
|
check "noop: markDead on an unknown id creates nothing",
|
|
et.enemies.len == 1 and not et.isAlive(99) and et.aliveCount == 1
|
|
|
|
proc testDoubleMarkDead() =
|
|
var et: EnemyTracker
|
|
et.see(7, 10)
|
|
et.markDead(7)
|
|
et.markDead(7)
|
|
check "idempotent: marking the same enemy dead twice is harmless",
|
|
not et.isAlive(7) and et.aliveCount == 0
|
|
|
|
proc testResetRoundClears() =
|
|
var et: EnemyTracker
|
|
et.see(1, 10)
|
|
et.see(2, 10)
|
|
et.markDead(2)
|
|
et.resetRound()
|
|
check "resetRound: clears every tracked enemy (alive and dead)",
|
|
et.enemies.len == 0 and et.aliveCount == 0 and not et.isAlive(1) and
|
|
not et.isAlive(2)
|
|
|
|
testMarkDeadImmediate()
|
|
testNoResurrection()
|
|
testAliveViewsExcludeDead()
|
|
testMarkDeadUnknownIsNoop()
|
|
testDoubleMarkDead()
|
|
testResetRoundClears()
|
|
|
|
if failures > 0:
|
|
echo "\n", failures, " check(s) FAILED"
|
|
quit(1)
|
|
echo "\nAll enemy-tracker death checks passed."
|