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.
3.6 KiB
Enemy-tracker deaths: there are no corpses
Question. Two independent agents believed "BotDeathEvent never reaches
ModularBot, so dead enemies stay alive in enemyTracker forever." If true,
every consumer of the tracker (targeting, the adaptive melee radar, the
virtual-bullet enemy table) would silently mis-prune. This note records the
direct measurement that settled it and the verdict, so the premise is not
re-invented a third time.
Why the premise sounds right (and is still real)
In robocode_tankroyale_botapi 1.0.7 the event queue's isCritical set is
only ekDeath, ekWonRound, ekSkippedTurn — not ekBotDeath.
removeOldEvents deletes any non-critical event with
turnNumber < currentTurn - MAX_EVENTS_AGE (2). So a bot that falls more than
two turns behind does drop BotDeathEvent. That mechanism is genuine and is
proven at the API level by common_libs/tests/test_event_drop_mechanism.nim.
The mistake was assuming the mechanism triggers in a real ModularBot match.
Measurement
A 7-bot melee (ModularBot + DrussGT + 5 legacy sample bots). A per-tick probe
compared enemyTracker alive-count against the server's getEnemyCount(), and
a queue probe inspected the event queue before removeOldEvents ran.
The probe ran with TR_TRACKER_RECONCILE=0 — i.e. raw behaviour, no
reconciliation masking anything.
| metric | server 1.3.1 (20 rounds) | server 0.35.5 (15 rounds) |
|---|---|---|
| observed enemy deaths | 83 | 68 |
| non-round-ending deaths | 83 (100%) | 66 (97%) |
ekBotDeath events DROPPED |
0 | 0 |
| max dispatch lag (turns behind) | 1 | 1 |
phantom ticks (tracker_alive > server_ec) |
1 / 16,820 | 1 / 12,596 |
| max corpse lifetime | 0 ticks | 0 ticks |
| victims still alive at round end | 0 | 0 |
The maximum dispatch lag of 1 turn is below MAX_EVENTS_AGE (2), so the drop
path is never reached. onBotDeath fires for every death, including
non-round-ending ones, and markDead takes effect on the same tick.
Verdict
There are no corpses. onBotDeath + EnemyTracker.markDead is a complete,
prompt death signal at this workload. The API-level drop mechanism is real but
never triggered. The two rare phantom ticks resolve within one tick and are not
persistent corpses.
Do not re-add a corpse workaround
- Do not reintroduce tracker reconciliation against
getEnemyCount()(the removedreconcileWithServer). It was a latent mis-prune path: its own doc comment recorded that a short persistence window marked a live enemy dead (it then "fired three more times"). - Do not add an "unseen for N ticks => treat as dead" filter to
adaptive_melee_radaror any other consumer. The tracker'saliveflag (andallAlive()/aliveCount()) already excludes dead enemies. Treating a long-unseen entry as dead is actively wrong: it is a stale live enemy that the freshness fallback must re-acquire. - The
CorpseTicks = 40radar filter and the>60-tick coverage filter that existed only because of this premise have been removed.
If a future workload ever does show corpses (e.g. a much larger melee, a higher-latency link), reproduce the measurement above first — do not patch from the premise.
Artifacts
- Instrument:
TR_TRACKER_PROBE=1inModularBot(default off; writes JSONL toTR_TRACKER_PROBE_PATH). Kept because it produced the table above. - Measurement harness:
common_libs/tests/measure_corpse_melee.nim. - Kept API-level proof:
common_libs/tests/test_event_drop_mechanism.nim. - Death invariant:
common_libs/tests/test_enemy_tracker_death.nim.