Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions .clue/id-ledger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ counters:
ARCH: "3"
C: "7"
CAP: "8"
CH: "4"
CH: "5"
CRIT: "8"
DES: "8"
EVT: "13"
EVT: "14"
G: "2"
IDR: "3"
IDR: "4"
OQ: "2"
P: "1"
PDR: "1"
PDR: "2"
REH: "1"
ROUTE: "12"
TASKS: "3"
TASKS: "4"
entries:
- id: ADR-001
kind: numeric
Expand Down Expand Up @@ -213,6 +213,11 @@ entries:
state: live
prefix: CH
component: "4"
- id: CH-005
kind: numeric
state: live
prefix: CH
component: "5"
- id: CRIT-001
kind: numeric
state: live
Expand Down Expand Up @@ -325,7 +330,7 @@ entries:
component: "6"
- id: EVT-007
kind: numeric
state: live
state: retired
prefix: EVT
component: "7"
- id: EVT-008
Expand Down Expand Up @@ -358,6 +363,11 @@ entries:
state: live
prefix: EVT
component: "13"
- id: EVT-014
kind: numeric
state: live
prefix: EVT
component: "14"
- id: G-001
kind: numeric
state: live
Expand All @@ -383,14 +393,19 @@ entries:
state: live
prefix: IDR
component: "3"
- id: IDR-004
kind: numeric
state: live
prefix: IDR
component: "4"
- id: OQ-001
kind: numeric
state: live
prefix: OQ
component: "1"
- id: OQ-002
kind: numeric
state: reserved
state: live
prefix: OQ
component: "2"
- id: P-001
Expand All @@ -403,6 +418,11 @@ entries:
state: live
prefix: PDR
component: "1"
- id: PDR-002
kind: numeric
state: live
prefix: PDR
component: "2"
- id: REH-001
kind: numeric
state: live
Expand Down Expand Up @@ -483,3 +503,8 @@ entries:
state: live
prefix: TASKS
component: "3"
- id: TASKS-004
kind: numeric
state: live
prefix: TASKS
component: "4"
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ cd compat-test && python compat_test.py --trace # per-turn behavio

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=`.

**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.

**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`.

**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.
Expand Down
55 changes: 47 additions & 8 deletions compat-test/compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,38 @@
# ----------------------------------------------------------------------------------

BASE_DIR = Path(__file__).resolve().parent
TANK_ROYALE_HOME = Path(os.environ.get("COMPAT_TANK_ROYALE_HOME", r"C:\Code\tank-royale"))


def local_bot_api_jar():
"""Returns the locally built Bot API jar, or the expected path when it has not been built."""
libs = TANK_ROYALE_HOME / "bot-api" / "java" / "build" / "libs"
jars = [path for path in libs.glob("robocode-tankroyale-bot-api-*.jar")
if not path.name.endswith(("-javadoc.jar", "-sources.jar"))]
if jars:
return str(max(jars, key=lambda path: path.stat().st_mtime))
return str(libs / "robocode-tankroyale-bot-api-local.jar")

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


def compile_conformance_robot(opts, source: Path):
"""Compiles a bridge-owned conformance probe against the classic API."""
out_dir = WORK_DIR / "conformance-probe-classes"
clean_dir(out_dir)
if not source.exists():
return None, f"conformance probe source not found: {source}"

classpath = str(Path(opts.robocode_home) / "libs" / "*")
cmd = [javac_beside(opts.rc_java_exe), "-cp", classpath, "-d", str(out_dir), str(source)]
try:
completed = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
except (OSError, subprocess.SubprocessError) as e:
return None, f"could not compile conformance probe: {e}"
if completed.returncode != 0:
return None, f"conformance probe did not compile:\n{completed.stdout}\n{completed.stderr}"
return out_dir, None


def javac_beside(java_exe):
"""The javac that ships next to a given java executable.

Expand Down Expand Up @@ -1204,7 +1233,17 @@ def run_conformance(opts):
if opts.participants is not None:
setup["participants"] = opts.participants

if jar.is_dir():
if opts.conformance_source:
class_dir, error = compile_conformance_robot(opts, opts.conformance_source)
if error:
print(json.dumps({"ok": False, "fatal": error}))
return 2
packaged, error = package_test_robot_jar(class_dir, classname, WORK_DIR / "conformance")
if error:
print(json.dumps({"ok": False, "fatal": error}))
return 2
jar, version = packaged, "1.0"
elif jar.is_dir():
# Package the one robot under test, for both engines.
#
# The bridge side needs a real robot jar because the wrapper finds robots by their
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package conformance.probes;

import robocode.DeathEvent;
import robocode.Robot;
import robocode.RobotDeathEvent;
import robocode.ScannedRobotEvent;

/** A two-instance probe that reports its own and another robot's death handlers. */
public class DeathEventProbe extends Robot {

@Override
public void run() {
while (true) {
ahead(100);
turnGunRight(360);
back(100);
turnGunRight(360);
}
}

@Override
public void onScannedRobot(ScannedRobotEvent event) {
fire(2);
}

@Override
public void onRobotDeath(RobotDeathEvent event) {
out.println("OtherDeath!");
}

@Override
public void onDeath(DeathEvent event) {
out.println("OwnDeath!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ private ConformanceHarness(String python, Path robocodeHome, Path testRobotClass
this.rounds = rounds;
}

/** Repository root, for bridge-owned probe sources passed to the Python harness. */
static Path repoRoot() {
return REPO_ROOT;
}

/** The number of rounds every battle this harness runs is configured for. */
int rounds() {
return rounds;
Expand Down Expand Up @@ -89,7 +94,7 @@ static String missingEnvironment() {
*
* @param robotClass fully qualified, e.g. {@code tested.robots.InteruptibleEvent}
*/
BattleOutcome run(Engine engine, String robotClass) {
BattleOutcome run(Engine engine, String robotClass, Path source) {
List<String> command = new ArrayList<>(List.of(
python,
HARNESS.toString(),
Expand All @@ -98,6 +103,10 @@ BattleOutcome run(Engine engine, String robotClass) {
"--engine", engine.harnessName(),
"--rounds", String.valueOf(rounds),
"--robocode-home", robocodeHome.toString()));
if (source != null) {
command.add("--conformance-source");
command.add(source.toString());
}

try {
Process process = new ProcessBuilder(command)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.HashMap;
import java.util.Map;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
Expand Down Expand Up @@ -43,8 +44,13 @@ interface Expectation {
* Runs the robot on both engines and applies the same expectation to each.
*/
void assertOnBothEngines(String robotClass, Expectation expectation) {
assertOnBothEngines(robotClass, null, expectation);
}

/** Runs a locally held probe source on both engines after compiling it against classic. */
void assertOnBothEngines(String robotClass, Path source, Expectation expectation) {
for (Engine engine : Engine.values()) {
BattleOutcome outcome = outcomeFor(engine, robotClass);
BattleOutcome outcome = outcomeFor(engine, robotClass, source);
assertTrue(outcome.completed(),
() -> "the battle did not complete on " + engine + " (" + outcome.summary() + ")");
expectation.check(outcome, engine);
Expand All @@ -59,8 +65,12 @@ void assertOnBothEngines(String robotClass, Expectation expectation) {
* reason that has nothing to do with what it claims to check.
*/
BattleOutcome outcomeFor(Engine engine, String robotClass) {
return outcomeFor(engine, robotClass, null);
}

private BattleOutcome outcomeFor(Engine engine, String robotClass, Path source) {
return ran.computeIfAbsent(engine.name() + " " + robotClass,
key -> harness.run(engine, robotClass));
key -> harness.run(engine, robotClass, source));
}

/** The number of rounds every battle in this run is configured for. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.robocode.tankroyale.bridge.conformance;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertTrue;

/** Acceptance evidence for EVT-014 — a survivor receives another robot's death. */
class RobotDeathEventsConformanceTest extends ConformanceTestBase {

private static final String ROBOT = "conformance.probes.DeathEventProbe";
private static final Path SOURCE = ConformanceHarness.repoRoot().resolve(Path.of("compat-test",
"conformance-robots", "conformance", "probes", "DeathEventProbe.java"));
private static final String OTHER_DEATH = "OtherDeath!";

@Test
@DisplayName("EVT-014: a surviving robot reports another robot's death on both engines")
void testEVT014_IntegrationPositive_SurvivorReceivesAnotherRobotsDeath() {
assertOnBothEngines(ROBOT, SOURCE, (outcome, engine) ->
assertTrue(outcome.anyConsoleContains(OTHER_DEATH),
() -> "no survivor reported another robot's death on " + engine + " ("
+ outcome.summary() + ")"));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.robocode.tankroyale.bridge.conformance;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

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

@Test
@DisplayName("EVT-004 negative: each destruction reaches onDeath once, not repeatedly")
void testEVT004_IntegrationNegative_DeathHandlerDoesNotRepeatForOneDestruction() {
assertOnBothEngines(ROBOT, (outcome, engine) ->
assertTrue(outcome.countOf("Death!") == configuredRounds(),
() -> "onDeath was reported other than once per destroyed robot on " + engine
+ " (" + outcome.summary() + ")"));
}

@Test
@DisplayName("EVT-012: winning a round reaches onWin on both engines")
void testEVT012_IntegrationPositive_WinningARoundReachesTheWinHandler() {
Expand Down
2 changes: 1 addition & 1 deletion docs/capabilities/CAP-001-event-dispatch-parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ The physics the events describe. That a `ScannedRobotEvent` arrives at the right

`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.

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.
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.
Loading
Loading