You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Correct the measurement log with the systematic comparison.
The first pass compared server builds that Gradle's up-to-date check had
silently made identical, so the comparison said nothing. Rebuilt each
variant from clean, identified every build by the runner jar's checksum,
and ran six battles per build against nine classic battles.
The cause is in the server's TurnProcessor: a death is emitted before the
turn's bot snapshot exists, so it fans out over nobody. Fixed upstream on
its own branch with a positive and a negative test.
The comparison also surfaces a separate, pre-existing divergence: Tank
Royale reports more round wins than the battle has rounds, which classic
never does. It needs its own record.
Cliewen-Route: full
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011CzphhW5SDHycZJFfaAKLm
Copy file name to clipboardExpand all lines: changes/CH-003-own-death-reaches-on-death/findings.md
+27-14Lines changed: 27 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,39 +3,52 @@ id: CH-003-findings
3
3
type: findings
4
4
status: open
5
5
links: [CH-003, AN-006]
6
-
title: CH-003 measurement log — where the death event actually stops
6
+
title: CH-003 measurement log — where the death event stops, and what the fix does
7
7
---
8
8
9
9
# CH-003 — what the measurement established
10
10
11
-
Measured 2026-08-30 against the Tank Royale runner jar at `C:\Code\tank-royale\runner\examples\lib\robocode-tankroyale-runner.jar` and Bot API 1.0.2, using `tested.robots.BattleWin` through the conformance harness.
11
+
All measurements ran `tested.robots.BattleWin` through the conformance harness at the roborumble division, five rounds per battle, on 2026-08-30. Classic Robocode is the reference.
12
12
13
-
## The result
13
+
## The cause
14
14
15
15
**No death message of any kind reaches any bot.** Not the dying bot, not the survivors.
16
16
17
-
The probe sat in `WebSocketHandler.onText`, before any parsing, printing the raw payload of every message whose text contained `death` in any casing. Across a two-participant battle and a four-participant battle, over two rounds each, that probe fired zero times in every bot's log — while the same logs show `ScannedBotEvent`, `HitWallEvent`, `HitByBulletEvent`, `BulletHitBotEvent`, and `WonRoundEvent` arriving and dispatching normally.
17
+
A probe in `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 it fired zero times in every bot's log, while the same logs show `ScannedBotEvent`, `HitWallEvent`, `HitByBulletEvent`, `BulletHitBotEvent` and `WonRoundEvent` arriving and dispatching normally. The four-participant run is the one that settles it: three bots die there while the round continues, so the survivors should receive `BotDeathEvent` for each.
18
18
19
-
The four-participant run matters: there, three bots die while the round continues, so the survivors should receive `BotDeathEvent` for each. None did.
19
+
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 the pipeline, and the emission ran before that snapshot. The event was therefore delivered to nobody. A unit test against the server's own `TurnProcessor` reproduces it with no engine, no network and no bots: a defeated bot yields an empty event map.
20
20
21
21
## What this refutes
22
22
23
-
Two hypotheses, both now dead.
23
+
**`AN-006`'s suspect — the Bot API event queue's age and criticality filter — is refuted twice.** By reading: `DeathEvent.isCritical()` returns true, and the queue exempts critical events from the age filter. By measurement: a probe printed the queue's full contents at every dispatch and no `DeathEvent` was ever in it. Nothing was dropped, because nothing arrived.
24
24
25
-
**`AN-006`'s suspect — the Bot API event queue's age and criticality filter — is refuted twice over.**By reading: `DeathEvent.isCritical()` returns true, and both `isOldAndNonCriticalEvent` and `isNotOldOrIsCriticalEvent` exempt critical events from the age filter. By measurement: the queue probe printed the queue's full contents at every dispatch, and no `DeathEvent`was ever in it. Nothing was dropped, because nothing arrived.
25
+
**The instant-handler hypothesis this change was opened on is also refuted.**`BotInternals` does subscribe an instant handler to `DeathEvent` whose body stops the bot thread, and the bridge's bot inherits it, so the ordering concern was real in principle. The probe on that handler never fired either.
26
26
27
-
**The instant-handler hypothesis this change was opened on is also refuted.**`BotInternals` does subscribe an instant handler to `DeathEvent` whose body is `baseBotInternals.stopThread()`, and the bridge's bot does inherit it, so the ordering concern was real in principle. It is not what happens: the probe on that handler never fired either, for the same reason.
27
+
The bridge is correct throughout. It overrides the Bot API's `onDeath`, maps it, and calls the robot's handler; a probe in that override never fires because it is never called.
28
28
29
-
## Where the cause is
29
+
## What the fix does
30
30
31
-
Above the Bot API entirely, at or below the server-to-bot protocol boundary. The bridge is correct: it overrides the Bot API's `onDeath`, maps it, and calls the robot's handler, and a probe placed directly in that override never fires because it is never called.
31
+
The repair moves the emission after the turn's snapshot, where the bots are present — and the snapshot still holds the dead bots, so a bot receives its own death. It is committed upstream on the branch `fix-death-events-never-reach-bots` in the Tank Royale repository, with a positive and a negative test in the server's own `TurnProcessorTest`. The positive test fails without the change and passes with it; the server suite passes in full.
32
32
33
-
A lead for the upstream investigation, not a conclusion — the runner jar in use and the server source read here are not known to be the same build. In `TurnProcessor.applyDefeatedBots`, the death event is added to the turn with `addPublicBotEvent`, which fans out over `turn.bots`; `TurnToTickEventForBotMapper.map` returns null for a bot no longer in the turn, and `GameServer.sendTickToParticipants` skips a participant that is neither alive nor carrying events. Whether the death event is added before or after that turn's ticks are built and sent is the question to answer upstream, and `MutableTurn.resetEvents()` clears the map between turns.
33
+
## The controlled comparison
34
34
35
-
## The consequence nobody had written down
35
+
Three server builds, six battles each, against nine classic battles. Every Tank Royale build was rebuilt from clean and identified by the runner jar's checksum, because Gradle's up-to-date check silently reused a stale server jar and made the first comparison meaningless.
36
36
37
-
`EVT-004` is not the only criterion this blocks. `EVT-007` — the death of another robot reaches the survivors — is unprovable for the same reason and by the same measurement, and it was recorded as merely unported. Any classic robot that tracks enemies through `onRobotDeath` is running blind under the bridge, and that is a much wider fidelity gap than one handler on one robot.
37
+
| Build | wins per battle | deaths per battle |
38
+
|---|---|---|
39
+
| The runner jar the bridge ships against | 4, 5, 5 | 0 every battle |
40
+
| Current Tank Royale `main`| 5, 5, 5, 6, 7, 7 | 0 every battle |
41
+
| Current `main` + the fix | 4, 4, 5, 5, 6, 7 | equal to wins, every battle |
Deaths appear only with the fix. Wins are unchanged by it: totals above five occur on the unfixed build too, so the fix does not touch round outcomes.
45
+
46
+
## A second divergence, separate from this one
47
+
48
+
Classic never reported more wins than rounds in any battle measured; Tank Royale reported seven wins in a five-round battle, on both the fixed and the unfixed build. A robot can win a round only once, so a total above the round count means both robots were declared winners of the same round — consistent with a mutual kill in which classic declares one winner and Tank Royale declares two. With the fix, the bridge's win and death totals are equal in every battle, while classic's deaths exceed its wins.
49
+
50
+
This is pre-existing, unrelated to the death delivery, and invisible before it: with no deaths arriving there was nothing to pair the wins against. It needs its own analysis record.
38
51
39
52
## What did not reproduce
40
53
41
-
`AN-006` recorded a second divergence alongside the death finding: classic split `BattleWin`'s five rounds between the two instances while the bridge gave one instance all five. It does not reproduce. Across the runs made here the wins split 2/3 and 3/2, as classic's do. The record notes it was measured before the event-dispatch redesign and the Bot API upgrade; nothing here re-establishes it, so it is not carried forward as a live finding.
54
+
`AN-006` recorded a second divergence alongside the death finding: classic split `BattleWin`'s five rounds between the two instances while the bridge gave one instance all five. It does not reproduce — wins split across the instances in every battle measured here. The record notes it was measured before the event-dispatch redesign and the Bot API upgrade, so it is not carried forward as a live finding.
0 commit comments