Skip to content

Commit 7115fc9

Browse files
Merge pull request #10 from robocode-dev/ch-005-death-event-evidence-boundary
CH-005: establish death-event evidence boundary
2 parents 3807542 + fd901fe commit 7115fc9

17 files changed

Lines changed: 254 additions & 46 deletions

File tree

.clue/id-ledger.yaml

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ counters:
55
ARCH: "3"
66
C: "7"
77
CAP: "8"
8-
CH: "4"
8+
CH: "5"
99
CRIT: "8"
1010
DES: "8"
11-
EVT: "13"
11+
EVT: "14"
1212
G: "2"
13-
IDR: "3"
13+
IDR: "4"
1414
OQ: "2"
1515
P: "1"
16-
PDR: "1"
16+
PDR: "2"
1717
REH: "1"
1818
ROUTE: "12"
19-
TASKS: "3"
19+
TASKS: "4"
2020
entries:
2121
- id: ADR-001
2222
kind: numeric
@@ -213,6 +213,11 @@ entries:
213213
state: live
214214
prefix: CH
215215
component: "4"
216+
- id: CH-005
217+
kind: numeric
218+
state: live
219+
prefix: CH
220+
component: "5"
216221
- id: CRIT-001
217222
kind: numeric
218223
state: live
@@ -325,7 +330,7 @@ entries:
325330
component: "6"
326331
- id: EVT-007
327332
kind: numeric
328-
state: live
333+
state: retired
329334
prefix: EVT
330335
component: "7"
331336
- id: EVT-008
@@ -358,6 +363,11 @@ entries:
358363
state: live
359364
prefix: EVT
360365
component: "13"
366+
- id: EVT-014
367+
kind: numeric
368+
state: live
369+
prefix: EVT
370+
component: "14"
361371
- id: G-001
362372
kind: numeric
363373
state: live
@@ -383,14 +393,19 @@ entries:
383393
state: live
384394
prefix: IDR
385395
component: "3"
396+
- id: IDR-004
397+
kind: numeric
398+
state: live
399+
prefix: IDR
400+
component: "4"
386401
- id: OQ-001
387402
kind: numeric
388403
state: live
389404
prefix: OQ
390405
component: "1"
391406
- id: OQ-002
392407
kind: numeric
393-
state: reserved
408+
state: live
394409
prefix: OQ
395410
component: "2"
396411
- id: P-001
@@ -403,6 +418,11 @@ entries:
403418
state: live
404419
prefix: PDR
405420
component: "1"
421+
- id: PDR-002
422+
kind: numeric
423+
state: live
424+
prefix: PDR
425+
component: "2"
406426
- id: REH-001
407427
kind: numeric
408428
state: live
@@ -483,3 +503,8 @@ entries:
483503
state: live
484504
prefix: TASKS
485505
component: "3"
506+
- id: TASKS-004
507+
kind: numeric
508+
state: live
509+
prefix: TASKS
510+
component: "4"

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ cd compat-test && python compat_test.py --trace # per-turn behavio
5454

5555
Tier 2 **skips** rather than fails when the environment is absent, so a clean checkout still builds. It needs a classic Robocode installation, the classic source repository's compiled test robots, and the Tank Royale runner jar; point it with `-Probocode.home=` and `-Probocode.source=`.
5656

57+
**Tank Royale conformance artifacts are local builds.** When a conformance check needs an unreleased upstream repair, build the Bot API and runner from the same Tank Royale revision, then rebuild this bridge with `-PtankRoyaleBotApiVersion=<that-local-version>` before running tier 2. Do not wait for, request, or create a Tank Royale release merely to run bridge evidence; `PDR-002` records the policy and `C-002` still requires a compatible pair.
58+
5759
**The classic side needs its own JDK.** Classic installs a `SecurityManager`, which JDK 24 removed outright, so `-Djava.security.manager=allow` is a fatal VM error there and classic cannot start at all. The harness auto-detects a JDK 23 or older; override with `COMPAT_RC_JAVA`.
5860

5961
**Evidence conventions.** A test carries its criterion, proof type, and direction in its own name — `testAPI001_UnitPositive_...`. Give each field of a wide positional constructor a distinct value in tests: two defects found so far were positional or dispatch faults that placeholder data would have passed. `PDR-001` explains why evidence is layered in three tiers and what each can prove.

compat-test/compat_test.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,38 @@
4343
# ----------------------------------------------------------------------------------
4444

4545
BASE_DIR = Path(__file__).resolve().parent
46+
TANK_ROYALE_HOME = Path(os.environ.get("COMPAT_TANK_ROYALE_HOME", r"C:\Code\tank-royale"))
47+
48+
49+
def local_bot_api_jar():
50+
"""Returns the locally built Bot API jar, or the expected path when it has not been built."""
51+
libs = TANK_ROYALE_HOME / "bot-api" / "java" / "build" / "libs"
52+
jars = [path for path in libs.glob("robocode-tankroyale-bot-api-*.jar")
53+
if not path.name.endswith(("-javadoc.jar", "-sources.jar"))]
54+
if jars:
55+
return str(max(jars, key=lambda path: path.stat().st_mtime))
56+
return str(libs / "robocode-tankroyale-bot-api-local.jar")
4657

4758
DEFAULTS = {
4859
"collection_dir": os.environ.get("COMPAT_COLLECTION_DIR", r"C:\Code\LiteRumble robots"),
4960
"robocode_home": os.environ.get("COMPAT_ROBOCODE_HOME", r"C:\robocode"),
5061
"runner_jar": os.environ.get(
5162
"COMPAT_RUNNER_JAR",
52-
r"C:\Code\tank-royale\runner\examples\lib\robocode-tankroyale-runner.jar"),
63+
str(TANK_ROYALE_HOME / "runner" / "examples" / "lib" / "robocode-tankroyale-runner.jar")),
5364
"bridge_api_jar": os.environ.get(
5465
"COMPAT_BRIDGE_API_JAR",
5566
r"C:\Code\robocode-api-bridge\robocode-api\build\libs\robocode-api-0.5.0.jar"),
5667
"wrapper_jar": os.environ.get(
5768
"COMPAT_WRAPPER_JAR",
5869
r"C:\Code\robocode-api-bridge\robots-wrapper\build\libs\robots-wrapper-0.3.1.jar"),
59-
# NOTE: must be protocol/API compatible with what the bridge's robocode-api jar was
60-
# compiled against AND with the server embedded in the runner jar. Publish it with
61-
# `gradlew :bot-api:java:publishToMavenLocal` in the tank-royale repository.
70+
# The bridge uses a locally built Bot API and runner from the same Tank Royale revision;
71+
# override only with another matched local pair. Build the API with
72+
# `gradlew :bot-api:java:publishToMavenLocal` and runner with `:runner:copyRunnerJar`.
6273
# (0.33.1 had an event-queue bug dropping deferred same-priority events, e.g. every
6374
# other scan event for bots that call blocking methods inside onScannedRobot.)
6475
"bot_api_jar": os.environ.get(
6576
"COMPAT_BOT_API_JAR",
66-
os.path.expanduser(r"~\.m2\repository\dev\robocode\tankroyale"
67-
r"\robocode-tankroyale-bot-api\1.0.2"
68-
r"\robocode-tankroyale-bot-api-1.0.2.jar")),
77+
local_bot_api_jar()),
6978
# Classic Robocode installs a SecurityManager to sandbox robots. JDK 24 removed
7079
# SecurityManager support outright, so -Djava.security.manager=allow is no longer a
7180
# deprecation warning but a fatal VM error, and the classic side cannot start at all.
@@ -880,6 +889,8 @@ def parse_args():
880889
conf.add_argument("--conformance", metavar="JAR",
881890
help="run one robot jar on one engine and print the result, "
882891
"including each participant's console output, as JSON")
892+
conf.add_argument("--conformance-source", type=Path,
893+
help="compile this bridge-owned robot source against classic before running it")
883894
conf.add_argument("--engine", choices=("rc", "tr"), default="rc",
884895
help="which engine --conformance drives")
885896
conf.add_argument("--robot-class",
@@ -1044,6 +1055,24 @@ def compile_trace_robot(opts):
10441055
return out_dir, None
10451056

10461057

1058+
def compile_conformance_robot(opts, source: Path):
1059+
"""Compiles a bridge-owned conformance probe against the classic API."""
1060+
out_dir = WORK_DIR / "conformance-probe-classes"
1061+
clean_dir(out_dir)
1062+
if not source.exists():
1063+
return None, f"conformance probe source not found: {source}"
1064+
1065+
classpath = str(Path(opts.robocode_home) / "libs" / "*")
1066+
cmd = [javac_beside(opts.rc_java_exe), "-cp", classpath, "-d", str(out_dir), str(source)]
1067+
try:
1068+
completed = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
1069+
except (OSError, subprocess.SubprocessError) as e:
1070+
return None, f"could not compile conformance probe: {e}"
1071+
if completed.returncode != 0:
1072+
return None, f"conformance probe did not compile:\n{completed.stdout}\n{completed.stderr}"
1073+
return out_dir, None
1074+
1075+
10471076
def javac_beside(java_exe):
10481077
"""The javac that ships next to a given java executable.
10491078
@@ -1204,7 +1233,17 @@ def run_conformance(opts):
12041233
if opts.participants is not None:
12051234
setup["participants"] = opts.participants
12061235

1207-
if jar.is_dir():
1236+
if opts.conformance_source:
1237+
class_dir, error = compile_conformance_robot(opts, opts.conformance_source)
1238+
if error:
1239+
print(json.dumps({"ok": False, "fatal": error}))
1240+
return 2
1241+
packaged, error = package_test_robot_jar(class_dir, classname, WORK_DIR / "conformance")
1242+
if error:
1243+
print(json.dumps({"ok": False, "fatal": error}))
1244+
return 2
1245+
jar, version = packaged, "1.0"
1246+
elif jar.is_dir():
12081247
# Package the one robot under test, for both engines.
12091248
#
12101249
# The bridge side needs a real robot jar because the wrapper finds robots by their
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package conformance.probes;
2+
3+
import robocode.DeathEvent;
4+
import robocode.Robot;
5+
import robocode.RobotDeathEvent;
6+
import robocode.ScannedRobotEvent;
7+
8+
/** A two-instance probe that reports its own and another robot's death handlers. */
9+
public class DeathEventProbe extends Robot {
10+
11+
@Override
12+
public void run() {
13+
while (true) {
14+
ahead(100);
15+
turnGunRight(360);
16+
back(100);
17+
turnGunRight(360);
18+
}
19+
}
20+
21+
@Override
22+
public void onScannedRobot(ScannedRobotEvent event) {
23+
fire(2);
24+
}
25+
26+
@Override
27+
public void onRobotDeath(RobotDeathEvent event) {
28+
out.println("OtherDeath!");
29+
}
30+
31+
@Override
32+
public void onDeath(DeathEvent event) {
33+
out.println("OwnDeath!");
34+
}
35+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ private ConformanceHarness(String python, Path robocodeHome, Path testRobotClass
3939
this.rounds = rounds;
4040
}
4141

42+
/** Repository root, for bridge-owned probe sources passed to the Python harness. */
43+
static Path repoRoot() {
44+
return REPO_ROOT;
45+
}
46+
4247
/** The number of rounds every battle this harness runs is configured for. */
4348
int rounds() {
4449
return rounds;
@@ -89,7 +94,7 @@ static String missingEnvironment() {
8994
*
9095
* @param robotClass fully qualified, e.g. {@code tested.robots.InteruptibleEvent}
9196
*/
92-
BattleOutcome run(Engine engine, String robotClass) {
97+
BattleOutcome run(Engine engine, String robotClass, Path source) {
9398
List<String> command = new ArrayList<>(List.of(
9499
python,
95100
HARNESS.toString(),
@@ -98,6 +103,10 @@ BattleOutcome run(Engine engine, String robotClass) {
98103
"--engine", engine.harnessName(),
99104
"--rounds", String.valueOf(rounds),
100105
"--robocode-home", robocodeHome.toString()));
106+
if (source != null) {
107+
command.add("--conformance-source");
108+
command.add(source.toString());
109+
}
101110

102111
try {
103112
Process process = new ProcessBuilder(command)

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.HashMap;
66
import java.util.Map;
7+
import java.nio.file.Path;
78

89
import static org.junit.jupiter.api.Assertions.assertTrue;
910
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -43,8 +44,13 @@ interface Expectation {
4344
* Runs the robot on both engines and applies the same expectation to each.
4445
*/
4546
void assertOnBothEngines(String robotClass, Expectation expectation) {
47+
assertOnBothEngines(robotClass, null, expectation);
48+
}
49+
50+
/** Runs a locally held probe source on both engines after compiling it against classic. */
51+
void assertOnBothEngines(String robotClass, Path source, Expectation expectation) {
4652
for (Engine engine : Engine.values()) {
47-
BattleOutcome outcome = outcomeFor(engine, robotClass);
53+
BattleOutcome outcome = outcomeFor(engine, robotClass, source);
4854
assertTrue(outcome.completed(),
4955
() -> "the battle did not complete on " + engine + " (" + outcome.summary() + ")");
5056
expectation.check(outcome, engine);
@@ -59,8 +65,12 @@ void assertOnBothEngines(String robotClass, Expectation expectation) {
5965
* reason that has nothing to do with what it claims to check.
6066
*/
6167
BattleOutcome outcomeFor(Engine engine, String robotClass) {
68+
return outcomeFor(engine, robotClass, null);
69+
}
70+
71+
private BattleOutcome outcomeFor(Engine engine, String robotClass, Path source) {
6272
return ran.computeIfAbsent(engine.name() + " " + robotClass,
63-
key -> harness.run(engine, robotClass));
73+
key -> harness.run(engine, robotClass, source));
6474
}
6575

6676
/** The number of rounds every battle in this run is configured for. */
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.robocode.tankroyale.bridge.conformance;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.nio.file.Path;
7+
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
/** Acceptance evidence for EVT-014 — a survivor receives another robot's death. */
11+
class RobotDeathEventsConformanceTest extends ConformanceTestBase {
12+
13+
private static final String ROBOT = "conformance.probes.DeathEventProbe";
14+
private static final Path SOURCE = ConformanceHarness.repoRoot().resolve(Path.of("compat-test",
15+
"conformance-robots", "conformance", "probes", "DeathEventProbe.java"));
16+
private static final String OTHER_DEATH = "OtherDeath!";
17+
18+
@Test
19+
@DisplayName("EVT-014: a surviving robot reports another robot's death on both engines")
20+
void testEVT014_IntegrationPositive_SurvivorReceivesAnotherRobotsDeath() {
21+
assertOnBothEngines(ROBOT, SOURCE, (outcome, engine) ->
22+
assertTrue(outcome.anyConsoleContains(OTHER_DEATH),
23+
() -> "no survivor reported another robot's death on " + engine + " ("
24+
+ outcome.summary() + ")"));
25+
}
26+
}

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package dev.robocode.tankroyale.bridge.conformance;
22

3-
import org.junit.jupiter.api.Disabled;
43
import org.junit.jupiter.api.DisplayName;
54
import org.junit.jupiter.api.Test;
65

@@ -33,12 +32,6 @@ class RoundOutcomeEventsConformanceTest extends ConformanceTestBase {
3332
private static final String ROBOT = "tested.robots.BattleWin";
3433

3534
@Test
36-
@Disabled("Fails against the bridge: onDeath is never called, because no death event reaches "
37-
+ "any bot at all. AN-009 has the cause -- the Tank Royale server emits a death before "
38-
+ "the turn's bot snapshot exists, so it fans out over nobody -- and the repair is "
39-
+ "committed upstream and unreleased. Kept rather than deleted because it is the only "
40-
+ "thing that detects the defect, and disabled rather than left failing so the build "
41-
+ "stays honest. Re-enable when a Tank Royale release carries the repair.")
4235
@DisplayName("EVT-004: a robot's own death reaches onDeath on both engines")
4336
void testEVT004_IntegrationPositive_OwnDeathReachesTheDeathHandler() {
4437
assertOnBothEngines(ROBOT, (outcome, engine) ->
@@ -47,6 +40,15 @@ void testEVT004_IntegrationPositive_OwnDeathReachesTheDeathHandler() {
4740
+ ", so onDeath was never called (" + outcome.summary() + ")"));
4841
}
4942

43+
@Test
44+
@DisplayName("EVT-004 negative: each destruction reaches onDeath once, not repeatedly")
45+
void testEVT004_IntegrationNegative_DeathHandlerDoesNotRepeatForOneDestruction() {
46+
assertOnBothEngines(ROBOT, (outcome, engine) ->
47+
assertTrue(outcome.countOf("Death!") == configuredRounds(),
48+
() -> "onDeath was reported other than once per destroyed robot on " + engine
49+
+ " (" + outcome.summary() + ")"));
50+
}
51+
5052
@Test
5153
@DisplayName("EVT-012: winning a round reaches onWin on both engines")
5254
void testEVT012_IntegrationPositive_WinningARoundReachesTheWinHandler() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ The physics the events describe. That a `ScannedRobotEvent` arrives at the right
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.
3737

38-
The conformance tier now reaches some of them. `EVT-011`, `EVT-012`, and `EVT-013` are active — the tests that already proved them were retagged after [`G-002`](../../goals/G-002-conformance-evidence-proves-the-criterion-it-names.md) found them mistagged, and `EVT-003` retired rather than be credited with evidence no available robot can produce ([`IDR-003`](../../decisions/IDR-003-evt-003-scoped-to-what-classic-actually-proves.md)). The capability still holds at `draft` because most criteria remain unproven, and `EVT-004`/`EVT-007` specifically 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.
38+
The conformance tier now reaches some of them. `EVT-004`, `EVT-011`, `EVT-012`, `EVT-013`, and `EVT-014` are active — the tests that already proved them were retagged after [`G-002`](../../goals/G-002-conformance-evidence-proves-the-criterion-it-names.md) found them mistagged, and `EVT-003`/`EVT-007` retired rather than be credited with evidence they cannot honestly claim ([`IDR-003`](../../decisions/IDR-003-evt-003-scoped-to-what-classic-actually-proves.md), [`IDR-004`](../../decisions/IDR-004-evt-007-scoped-to-observable-survivor-delivery.md)). `EVT-004` and `EVT-014` are proven with a locally built matched Tank Royale Bot API and runner pair under [`PDR-002`](../../decisions/PDR-002-locally-built-tank-royale-artifacts-for-conformance.md), which contains the server repair [`AN-009`](../../analysis/AN-009-the-server-never-sends-a-death-to-any-bot.md) identified. The capability still holds at `draft` because most criteria remain unproven.

0 commit comments

Comments
 (0)