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.
63 lines
1.7 KiB
Nim
63 lines
1.7 KiB
Nim
## Per-enemy state table. Keyed by bot ID.
|
|
## Tank Royale: 0° = East.
|
|
|
|
import std/[tables, math]
|
|
|
|
type
|
|
EnemyState* = object
|
|
id*: int
|
|
x*, y*: float
|
|
heading*: float ## degrees
|
|
speed*: float
|
|
energy*: float
|
|
lastSeenTick*: int
|
|
alive*: bool
|
|
|
|
EnemyTracker* = object
|
|
enemies*: Table[int, EnemyState]
|
|
|
|
proc update*(et: var EnemyTracker, botId: int, x, y, heading, speed, energy: float, tick: int) =
|
|
if botId in et.enemies and not et.enemies[botId].alive:
|
|
return # don't resurrect dead bots
|
|
et.enemies[botId] = EnemyState(id: botId, x: x, y: y, heading: heading,
|
|
speed: speed, energy: energy,
|
|
lastSeenTick: tick, alive: true)
|
|
|
|
proc markDead*(et: var EnemyTracker, botId: int) =
|
|
if botId in et.enemies:
|
|
et.enemies[botId].alive = false
|
|
|
|
proc isAlive*(et: EnemyTracker, botId: int): bool =
|
|
botId in et.enemies and et.enemies[botId].alive
|
|
|
|
proc aliveCount*(et: EnemyTracker): int =
|
|
for s in et.enemies.values:
|
|
if s.alive: inc result
|
|
|
|
proc resetRound*(et: var EnemyTracker) =
|
|
et.enemies.clear()
|
|
|
|
proc getEnemy*(et: EnemyTracker, botId: int): EnemyState =
|
|
et.enemies[botId]
|
|
|
|
proc allAlive*(et: EnemyTracker): seq[EnemyState] =
|
|
for s in et.enemies.values:
|
|
if s.alive: result.add s
|
|
|
|
proc closestTo*(et: EnemyTracker, x, y: float): EnemyState =
|
|
var bestDist = Inf
|
|
for s in et.enemies.values:
|
|
if not s.alive: continue
|
|
let d = hypot(s.x - x, s.y - y)
|
|
if d < bestDist:
|
|
bestDist = d
|
|
result = s
|
|
|
|
proc weakest*(et: EnemyTracker): EnemyState =
|
|
var bestEnergy = Inf
|
|
for s in et.enemies.values:
|
|
if not s.alive: continue
|
|
if s.energy < bestEnergy:
|
|
bestEnergy = s.energy
|
|
result = s
|