|
| 1 | +--- |
| 2 | +id: AN-009 |
| 3 | +type: analysis |
| 4 | +status: active |
| 5 | +links: [CAP-001, AN-006, AN-008, PDR-001, C-006] |
| 6 | +title: The Tank Royale server never sends a death event to any bot, which is why onDeath never fires |
| 7 | +provenance: inferred |
| 8 | +reversal-cost: low |
| 9 | +--- |
| 10 | + |
| 11 | +# AN-009 — The Tank Royale server never sends a death event to any bot |
| 12 | + |
| 13 | +## What was investigated |
| 14 | + |
| 15 | +The cause of [`AN-006`](AN-006-own-death-still-does-not-reach-on-death.md), which measured that `onDeath` is never called under the bridge and named a suspect it could not confirm: the Bot API event queue's age and criticality filter. This spike set out to establish which of three things is true — the death event never reaches the queue, it reaches the queue and is discarded, or it is dispatched onto a thread that has already stopped. |
| 16 | + |
| 17 | +None of the three. The question was wrong, because it assumed a death event arrives at the bot at all. |
| 18 | + |
| 19 | +## What was found |
| 20 | + |
| 21 | +**No death message of any kind reaches any bot.** Not the dying bot, not the survivors. |
| 22 | + |
| 23 | +A probe in the Bot API's `WebSocketHandler.onText`, ahead of any parsing, printed the raw payload of every message whose text contained `death` in any casing. Across a two-participant battle and a four-participant battle, five rounds each, it fired zero times in every bot's log. The same logs show scan, wall-hit, bullet-hit, bullet-hit-bot and won-round events arriving and dispatching normally. |
| 24 | + |
| 25 | +The four-participant run is the one that settles it. Three bots die there while the round continues, so the survivors should be told about each death. They were not. |
| 26 | + |
| 27 | +The cause is in the Tank Royale server, in `TurnProcessor.processTurn`. A death is emitted with `addPublicBotEvent`, which fans out over the turn's own bots. Every turn is constructed empty and only filled from the bots map at the end of that pipeline, and the emission ran before that snapshot, so the event was delivered to nobody. The server's own turn-pipeline unit test reproduces it with no engine, no network, and no bot processes: a defeated bot yields an empty event map. |
| 28 | + |
| 29 | +## What was ruled out, and how |
| 30 | + |
| 31 | +**The Bot API's event queue** — the suspect `AN-006` named. Refuted twice. By reading: `DeathEvent.isCritical()` returns true, and the queue exempts critical events from its age filter. By measurement: a probe printed the queue's full contents at every dispatch and no death event was ever in it. Nothing was dropped, because nothing arrived. |
| 32 | + |
| 33 | +**A negative priority.** Ruled out by reading rather than by probe: the bridge registers `DeathEvent` at priority -1, matching classic, and the queue does not filter on priority sign. |
| 34 | + |
| 35 | +**The Bot API's instant death handler.** `BotInternals` subscribes a handler to `DeathEvent` whose body stops the bot thread, and the bridge's bot inherits it, so it could in principle stop the thread before the queued dispatch ran. This was the hypothesis the change was opened on. A probe on that handler never fired. |
| 36 | + |
| 37 | +**The bridge.** It overrides the Bot API's `onDeath`, maps it, and calls the robot's handler. A probe inside that override never fires, because it is never called. The bridge needs no change. |
| 38 | + |
| 39 | +## The repair |
| 40 | + |
| 41 | +Emitting after the turn's snapshot, which still holds the dead bots, so a bot also receives its own death. Committed in the Tank Royale repository (`https://github.com/robocode-dev/tank-royale`) on the local branch `fix-death-events-never-reach-bots`, with a positive and a negative test in the server's own turn-pipeline suite; the positive test fails without the change and the server suite passes with it. At the time of writing that branch is not pushed and has no pull request, so nothing downstream can consume it yet. |
| 42 | + |
| 43 | +`C-006` does not apply. The obligation to land a change in all four language implementations is about the Bot API; this is server code, and the server exists once. |
| 44 | + |
| 45 | +## The controlled comparison |
| 46 | + |
| 47 | +Every Tank Royale build was rebuilt from clean and identified by the runner jar's checksum. The first attempt at this comparison was worthless: Gradle's up-to-date check reused a stale shrunk server jar and produced byte-identical runner jars for two different source trees. Six five-round battles per build, against nine classic battles, on 2026-08-30: |
| 48 | + |
| 49 | +| Build | wins per battle | deaths per battle | |
| 50 | +|---|---|---| |
| 51 | +| The runner jar the bridge builds against | 4, 5, 5 | 0 in every battle | |
| 52 | +| Current Tank Royale `main` | 5, 5, 5, 6, 7, 7 | 0 in every battle | |
| 53 | +| Current `main` with the repair | 4, 4, 5, 5, 6, 7 | equal to the wins, in every battle | |
| 54 | +| Classic Robocode | 5, 5, 5, 5, 5, 4, 5, 5, 4 | 5, 7, 6, 5, 5, 6, 6, 6, 6 | |
| 55 | + |
| 56 | +Deaths appear only with the repair. Wins are untouched by it — totals above five occur on the unrepaired build too, which is [`AN-008`](AN-008-tank-royale-declares-more-round-winners-than-rounds.md) and not this. |
| 57 | + |
| 58 | +## What supersedes AN-006 |
| 59 | + |
| 60 | +`AN-006`'s conclusions, not its measurement. Its marker table stands as what was observed then; its suspect is refuted above. |
| 61 | + |
| 62 | +Its recorded second divergence — classic splitting the rounds 3–2 between the instances while the bridge gave one instance all five — did not reproduce. Wins split across the instances in every battle measured here. It was recorded before the event-dispatch redesign and the Bot API upgrade, and nothing here re-establishes it. |
| 63 | + |
| 64 | +## What this means for the criteria |
| 65 | + |
| 66 | +`EVT-004` stays `@draft`. The behaviour is missing, the cause is established, and the repair is unreleased; the bridge builds against released Bot API and runner artifacts rather than local ones, so nothing here can promote it. The reason is now a named cause with a named fix rather than an absence. |
| 67 | + |
| 68 | +`EVT-007` — the death of another robot reaches the survivors — is blocked by the same cause, which nobody had written down. It was recorded as merely unported. Any classic robot that tracks enemies through `onRobotDeath` is running blind under the bridge, which is a wider fidelity gap than one handler on one robot. |
0 commit comments