Skip to content

Commit b12fef7

Browse files
Merge pull request #8 from robocode-dev/ch-003-own-death-reaches-on-death
Establish why a robot's own death never reaches onDeath
2 parents f90f816 + 51e3af9 commit b12fef7

10 files changed

Lines changed: 223 additions & 13 deletions

File tree

.clue/id-ledger.yaml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
counters:
22
ADR: "1"
3-
AN: "7"
3+
AN: "9"
44
API: "6"
55
ARCH: "3"
66
C: "7"
77
CAP: "8"
8-
CH: "2"
8+
CH: "3"
99
CRIT: "8"
1010
DES: "8"
11-
G: "1"
11+
G: "2"
1212
IDR: "2"
1313
OQ: "2"
1414
P: "1"
1515
PDR: "1"
1616
REH: "1"
1717
ROUTE: "12"
18-
TASKS: "2"
18+
TASKS: "3"
1919
entries:
2020
- id: ADR-001
2121
kind: numeric
@@ -57,6 +57,16 @@ entries:
5757
state: live
5858
prefix: AN
5959
component: "7"
60+
- id: AN-008
61+
kind: numeric
62+
state: live
63+
prefix: AN
64+
component: "8"
65+
- id: AN-009
66+
kind: numeric
67+
state: live
68+
prefix: AN
69+
component: "9"
6070
- id: API-001
6171
kind: numeric
6272
state: live
@@ -187,6 +197,11 @@ entries:
187197
state: live
188198
prefix: CH
189199
component: "2"
200+
- id: CH-003
201+
kind: numeric
202+
state: live
203+
prefix: CH
204+
component: "3"
190205
- id: CRIT-001
191206
kind: numeric
192207
state: live
@@ -272,6 +287,11 @@ entries:
272287
state: live
273288
prefix: G
274289
component: "1"
290+
- id: G-002
291+
kind: numeric
292+
state: live
293+
prefix: G
294+
component: "2"
275295
- id: IDR-001
276296
kind: numeric
277297
state: live
@@ -377,3 +397,8 @@ entries:
377397
state: live
378398
prefix: TASKS
379399
component: "2"
400+
- id: TASKS-003
401+
kind: numeric
402+
state: live
403+
prefix: TASKS
404+
component: "3"

compat-test/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,10 @@ therefore safe to run over a state file holding results from several eras.
224224
chunk it; checkpointing makes chunking free.
225225
- Per-side timeout (`--timeout`, default 600 s) kills the entire worker process tree
226226
(server/booter/bot JVMs included) and records the robot as failed.
227+
- **Comparing two Tank Royale server builds**: the runner jar carries an embedded server
228+
jar, and rebuilding it does not reliably pick up a change to the server's sources —
229+
Gradle's up-to-date check reused a stale shrunk server jar and produced byte-identical
230+
runner jars for two different source trees, which silently made a whole comparison
231+
meaningless. Clean the modules first (`gradlew :server:clean :runner:clean`) and check
232+
the runner jar's checksum between builds; if the checksum did not change, neither did
233+
the server.

conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/RoundOutcomeEventsConformanceTest.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,24 @@
1919
* routed through the Bot API's event queue, the bridge dispatched them from a hand-written
2020
* switch that had no case for the robot's own death, so {@code onDeath} was never called for
2121
* any robot. A manual switch fails silently on the branch nobody wrote, and nothing in a
22-
* score report shows it.
22+
* score report shows it. Routing through the queue did not fix it either, and AN-009 now has
23+
* the reason: the death never reaches the bot, so there is nothing for any dispatcher to route.
24+
*
25+
* Every passing test in this class is tagged for a criterion it does not prove -- the win-handler
26+
* test and both round-completion tests assert behaviour no criterion in CAP-001 covers. G-002
27+
* carries that; do not read a passing test here as evidence for the criterion in its name.
2328
*/
2429
class RoundOutcomeEventsConformanceTest extends ConformanceTestBase {
2530

2631
private static final String ROBOT = "tested.robots.BattleWin";
2732

2833
@Test
29-
@Disabled("Fails against the bridge: onDeath is never called. Confirmed and recorded in "
30-
+ "AN-006, which has the measured marker counts for both engines. Kept rather than "
31-
+ "deleted because it is the only thing that detects the defect, and disabled rather "
32-
+ "than left failing so the build stays honest. Re-enable in M-001, which owns the "
33-
+ "root cause in the Bot API's event queue.")
34+
@Disabled("Fails against the bridge: onDeath is never called, because no death event reaches "
35+
+ "any bot at all. AN-009 has the cause -- the Tank Royale server emits a death before "
36+
+ "the turn's bot snapshot exists, so it fans out over nobody -- and the repair is "
37+
+ "committed upstream and unreleased. Kept rather than deleted because it is the only "
38+
+ "thing that detects the defect, and disabled rather than left failing so the build "
39+
+ "stays honest. Re-enable when a Tank Royale release carries the repair.")
3440
@DisplayName("EVT-004: a robot's own death reaches onDeath on both engines")
3541
void testEVT004_IntegrationPositive_OwnDeathReachesTheDeathHandler() {
3642
assertOnBothEngines(ROBOT, (outcome, engine) ->
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
id: AN-008
3+
type: analysis
4+
status: active
5+
links: [CAP-001, AN-006, AN-009, PDR-001]
6+
title: Tank Royale declares more round winners than the battle has rounds
7+
provenance: inferred
8+
reversal-cost: low
9+
---
10+
11+
# AN-008 — Tank Royale declares more round winners than the battle has rounds
12+
13+
## What was investigated
14+
15+
Round outcomes, noticed while measuring [`AN-009`](AN-009-the-server-never-sends-a-death-to-any-bot.md). Classic's `BattleWin` robot prints a marker from `onWin` and from `onDeath`, so a battle's win and death totals can be counted from the two robot consoles. Once deaths began arriving under the bridge, the win totals became readable against them for the first time.
16+
17+
## What was found
18+
19+
A robot can win a round only once, so the wins in a battle can never exceed its rounds. Classic obeys that. Tank Royale does not.
20+
21+
Measured over five-round battles of `BattleWin` against a copy of itself, at the roborumble division, on 2026-08-30:
22+
23+
| Engine | Wins per battle | Deaths per battle |
24+
|---|---|---|
25+
| Classic Robocode | 5, 5, 5, 5, 5, 4, 5, 5, 4 | 5, 7, 6, 5, 5, 6, 6, 6, 6 |
26+
| Tank Royale, current server | 5, 5, 5, 6, 7, 7 | none delivered — `AN-009` |
27+
| Tank Royale, server with the `AN-009` repair | 4, 4, 5, 5, 6, 7 | equal to the wins, in every battle |
28+
29+
Classic never exceeded its round count. Tank Royale reached seven wins in a five-round battle, on both the repaired and the unrepaired server, so this is independent of the death defect and is not caused by its repair.
30+
31+
The likely shape: a round that ends in mutual destruction. Classic's deaths exceed its wins, which is what a mutual kill looks like when one robot is still declared the winner. Under the bridge the two totals are equal in every battle measured, which is what it looks like when both are declared winners.
32+
33+
## Why it was invisible until now
34+
35+
There was nothing to read the win totals against. With no deaths arriving, a battle's markers were wins and round ends only, and a win total of six in a five-round battle is not obviously wrong unless you are already counting. `AN-009`'s defect hid this one.
36+
37+
## What this does not establish
38+
39+
The cause. Nothing in the server has been traced; the shape above is inferred from the totals and from how classic's totals differ, not demonstrated. Whether the extra winner also receives the round's score, and therefore whether this moves rumble results, is unmeasured — and that is the question that decides how much it matters.
40+
41+
It is also not established which engine is right about the scoring rule, only that they differ. Classic is the specification, so classic is right by definition about what a robot observes; whether Tank Royale intends a different rule for its own bots is a question for that project.
42+
43+
## Where it goes
44+
45+
No criterion in `CAP-001` covers how many robots may be declared the winner of a round, so nothing currently fails because of this. Nor does any criterion cover round and battle completion reaching their handlers — the tests that assert it are tagged for criteria that say something else, which is what [`G-002`](../goals/G-002-conformance-evidence-proves-the-criterion-it-names.md) exists to repair. A criterion for the winner count belongs to `CAP-001` and needs the cause established first, because a criterion written against inferred behaviour would encode the guess.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.

docs/analysis/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ An incident where the corpus was green but later evidence contradicted it carrie
1515
- [AN-006 — A robot's own death still does not reach onDeath under the bridge](AN-006-own-death-still-does-not-reach-on-death.md) · `active` — Whether `EVT-004` holds: a robot's own death reaches its death handler.
1616
- [AN-004-extraction/](AN-004-extraction/README.md)
1717
- [AN-007 — Remaining turn had opposite signs depending on which path a robot read it through](AN-007-remaining-turn-sign-disagreed-between-two-paths.md) · `active` — Two conversion paths for one quantity disagreed, and the test written alongside AN-005 had certified the wrong one.
18+
- [AN-008 — Tank Royale declares more round winners than the battle has rounds](AN-008-tank-royale-declares-more-round-winners-than-rounds.md) · `active` — Round outcomes, noticed while measuring `AN-006`.
19+
- [AN-009 — The Tank Royale server never sends a death event to any bot, which is why onDeath never fires](AN-009-the-server-never-sends-a-death-to-any-bot.md) · `active` — The cause of `AN-006`, 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.
1820
<!-- clue:index:end -->

docs/capabilities/CAP-001-event-dispatch-parity/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ The physics the events describe. That a `ScannedRobotEvent` arrives at the right
3434
## Status
3535

3636
`draft`. The redesign that routed events through the Bot API's own event queue is implemented and believed correct, but it was verified by running battles and reading scores. Every criterion here is unproven in the sense that matters: nothing would tell us if it broke again. `M-001` is the plan door.
37+
38+
The conformance tier now reaches some of them, and the capability still holds at `draft` for two separate reasons. `EVT-004` and `EVT-007` have an established cause and an unreleased repair — [`AN-009`](../../analysis/AN-009-the-server-never-sends-a-death-to-any-bot.md): the Tank Royale server sends no death to any bot, so neither the dying robot nor the survivors are told. And the tests that do pass here are not all tagged for what they prove; [`G-002`](../../goals/G-002-conformance-evidence-proves-the-criterion-it-names.md) carries that, and until it is repaired a passing test in this capability is not on its own grounds for promoting the criterion it names.

docs/capabilities/CAP-001-event-dispatch-parity/criteria.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,31 @@ Feature: Event dispatch and timing parity
3838
Given a robot that turns its radar from inside onHitWall and marks when it is scanned
3939
When the same battle runs on both engines
4040
Then the robot reports being scanned on both engines
41-
# Proven by the ported InteruptibleEvent robot. Plan door: M-001.
41+
# The ported InteruptibleEvent robot is tested in InterruptibleEventConformanceTest and passes,
42+
# but it does not prove this scenario's name: the robot sets HitWallEvent to the SAME priority as
43+
# ScannedRobotEvent, so nothing higher-priority arrives, and the assertion cannot separate
44+
# re-entry from an ordinary later delivery. Promotion was attempted in CH-003 and reverted.
45+
# This is the same defect as the EVT-005 mistag; see G-002. Plan door: M-001.
4246
4347
@EVT-004 @draft
4448
Scenario: A robot's own death reaches its death handler
4549
Test-type: Integration
4650
Given a robot that reports from onDeath
4751
When the robot is destroyed on each engine
4852
Then the report appears on both engines
49-
# This dispatched nowhere at all before the redesign: no death case existed. Plan door: M-001.
53+
# Draft because the behaviour is missing, not because nothing tests it: the test exists and
54+
# is disabled. AN-009 establishes the cause -- the Tank Royale server emits a death before the
55+
# turn's bot snapshot exists, so it reaches no bot at all -- and the repair is committed
56+
# upstream and unreleased. Promote when a release carries it. Plan door: M-001.
5057
5158
@EVT-005 @draft
5259
Scenario: New-turn events arrive at the classic point in the turn
5360
Test-type: Integration
5461
Given a robot that records the turn number at each handler entry
5562
When the same battle runs on both engines
5663
Then each handler is entered at the same point in the turn on both engines
64+
# Draft, and not proven by the tests currently tagged EVT-005: those assert round and battle
65+
# completion, which is different behaviour and which no criterion here covers. See G-002.
5766
# Plan door: M-001.
5867
5968
@EVT-006 @draft
@@ -70,7 +79,8 @@ Feature: Event dispatch and timing parity
7079
Given a robot that reports each robot death it observes
7180
When the same battle runs on both engines
7281
Then the same deaths are reported in the same order on both engines
73-
# Proven by the ported RobotDeathEvents robot. Plan door: M-001.
82+
# Proven by the ported RobotDeathEvents robot. Blocked by the same cause as EVT-004: no death
83+
# event reaches any bot, so the survivors are not told either. See AN-009. Plan door: M-001.
7484
7585
@EVT-008 @draft
7686
Scenario: Skipped turns are reported to the robot

0 commit comments

Comments
 (0)