diff --git a/.clue/id-ledger.yaml b/.clue/id-ledger.yaml index fc7d8f6..9136cad 100644 --- a/.clue/id-ledger.yaml +++ b/.clue/id-ledger.yaml @@ -5,11 +5,12 @@ counters: ARCH: "3" C: "7" CAP: "8" - CH: "3" + CH: "4" CRIT: "8" DES: "8" + EVT: "13" G: "2" - IDR: "2" + IDR: "3" OQ: "2" P: "1" PDR: "1" @@ -207,6 +208,11 @@ entries: state: live prefix: CH component: "3" + - id: CH-004 + kind: numeric + state: live + prefix: CH + component: "4" - id: CRIT-001 kind: numeric state: live @@ -287,6 +293,71 @@ entries: state: live prefix: DES component: "8" + - id: EVT-001 + kind: numeric + state: live + prefix: EVT + component: "1" + - id: EVT-002 + kind: numeric + state: live + prefix: EVT + component: "2" + - id: EVT-003 + kind: numeric + state: retired + prefix: EVT + component: "3" + - id: EVT-004 + kind: numeric + state: live + prefix: EVT + component: "4" + - id: EVT-005 + kind: numeric + state: live + prefix: EVT + component: "5" + - id: EVT-006 + kind: numeric + state: live + prefix: EVT + component: "6" + - id: EVT-007 + kind: numeric + state: live + prefix: EVT + component: "7" + - id: EVT-008 + kind: numeric + state: live + prefix: EVT + component: "8" + - id: EVT-009 + kind: numeric + state: live + prefix: EVT + component: "9" + - id: EVT-010 + kind: numeric + state: live + prefix: EVT + component: "10" + - id: EVT-011 + kind: numeric + state: live + prefix: EVT + component: "11" + - id: EVT-012 + kind: numeric + state: live + prefix: EVT + component: "12" + - id: EVT-013 + kind: numeric + state: live + prefix: EVT + component: "13" - id: G-001 kind: numeric state: live @@ -307,6 +378,11 @@ entries: state: live prefix: IDR component: "2" + - id: IDR-003 + kind: numeric + state: live + prefix: IDR + component: "3" - id: OQ-001 kind: numeric state: live diff --git a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceHarness.java b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceHarness.java index ba91b7d..7aa61f6 100644 --- a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceHarness.java +++ b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceHarness.java @@ -39,6 +39,11 @@ private ConformanceHarness(String python, Path robocodeHome, Path testRobotClass this.rounds = rounds; } + /** The number of rounds every battle this harness runs is configured for. */ + int rounds() { + return rounds; + } + /** Resolves the environment, or returns null when this machine cannot run the tier. */ static ConformanceHarness resolveOrNull() { if (!Files.isRegularFile(HARNESS)) { diff --git a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceTestBase.java b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceTestBase.java index 943020a..ceab7e7 100644 --- a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceTestBase.java +++ b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/ConformanceTestBase.java @@ -62,4 +62,9 @@ BattleOutcome outcomeFor(Engine engine, String robotClass) { return ran.computeIfAbsent(engine.name() + " " + robotClass, key -> harness.run(engine, robotClass)); } + + /** The number of rounds every battle in this run is configured for. */ + static int configuredRounds() { + return harness.rounds(); + } } diff --git a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/InterruptibleEventConformanceTest.java b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/InterruptibleEventConformanceTest.java index 478aa3a..49f2530 100644 --- a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/InterruptibleEventConformanceTest.java +++ b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/InterruptibleEventConformanceTest.java @@ -6,16 +6,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Acceptance evidence for EVT-003 — an interruptible handler is re-entered when a - * higher-priority event arrives. + * Acceptance evidence for EVT-013 — an interruptible handler is re-entered for a + * same-priority event once marked interruptible. * * Classic's {@code InteruptibleEvent} robot turns its radar from inside {@code onHitWall} - * and prints a marker if {@code onScannedRobot} is then entered. Classic's own suite - * asserts exactly this, and the expectation ports unchanged because it is about a marker - * the robot printed rather than about where the robot ended up. + * and prints a marker if {@code onScannedRobot} is then entered. The robot sets + * {@code HitWallEvent} to the same priority as {@code ScannedRobotEvent} ("make same as + * scan"), so this is same-priority re-entry, not pre-emption by a higher-priority event. + * Classic's own suite asserts exactly this, and the expectation ports unchanged because + * it is about a marker the robot printed rather than about where the robot ended up. * - * This is the criterion the earlier event-queue defect broke: with deferred same-priority - * events discarded, the scan never arrived and the marker never appeared. + * EVT-013 is the successor to EVT-003, retired because no robot in the source tree + * exercises genuine higher-priority re-entry (IDR-003). This is the criterion the earlier + * event-queue defect broke: with deferred same-priority events discarded, the scan never + * arrived and the marker never appeared. */ class InterruptibleEventConformanceTest extends ConformanceTestBase { @@ -23,8 +27,8 @@ class InterruptibleEventConformanceTest extends ConformanceTestBase { private static final String SCANNED = "Scanned!!!"; @Test - @DisplayName("EVT-003: turning the radar inside onHitWall leads to onScannedRobot on both engines") - void testEVT003_IntegrationPositive_ScanHandlerIsEnteredFromWithinTheWallHandler() { + @DisplayName("EVT-013: turning the radar inside onHitWall leads to onScannedRobot on both engines") + void testEVT013_IntegrationPositive_ScanHandlerIsEnteredFromWithinTheWallHandler() { assertOnBothEngines(ROBOT, (outcome, engine) -> assertTrue(outcome.anyConsoleContains(SCANNED), () -> "the robot never reported being scanned on " + engine @@ -33,8 +37,8 @@ void testEVT003_IntegrationPositive_ScanHandlerIsEnteredFromWithinTheWallHandler } @Test - @DisplayName("EVT-003 negative: the bridge does not report a scan the classic engine never saw") - void testEVT003_IntegrationNegative_DoesNotReportAScanClassicDidNotSee() { + @DisplayName("EVT-013 negative: the bridge does not report a scan the classic engine never saw") + void testEVT013_IntegrationNegative_DoesNotReportAScanClassicDidNotSee() { BattleOutcome classic = outcomeFor(Engine.CLASSIC, ROBOT); BattleOutcome bridge = outcomeFor(Engine.BRIDGE, ROBOT); @@ -49,8 +53,8 @@ void testEVT003_IntegrationNegative_DoesNotReportAScanClassicDidNotSee() { } @Test - @DisplayName("EVT-003: neither engine throws while dispatching the interrupted handler") - void testEVT003_IntegrationNegative_DispatchingAnInterruptedHandlerThrowsOnNeitherEngine() { + @DisplayName("EVT-013: neither engine throws while dispatching the interrupted handler") + void testEVT013_IntegrationNegative_DispatchingAnInterruptedHandlerThrowsOnNeitherEngine() { assertOnBothEngines(ROBOT, (outcome, engine) -> assertTrue(outcome.errors().isEmpty(), () -> "errors on " + engine + ": " + outcome.errors())); diff --git a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/RoundOutcomeEventsConformanceTest.java b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/RoundOutcomeEventsConformanceTest.java index 2d35820..ac310d3 100644 --- a/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/RoundOutcomeEventsConformanceTest.java +++ b/conformance-test/src/test/java/dev/robocode/tankroyale/bridge/conformance/RoundOutcomeEventsConformanceTest.java @@ -7,8 +7,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Acceptance evidence for EVT-004 — a robot's own death reaches its death handler — and for - * the round and battle completion events alongside it. + * Acceptance evidence for EVT-004 (own death reaches the death handler), EVT-012 (winning a + * round reaches the win handler), and EVT-011 (round and battle completion each reach their + * handler exactly once). * * Classic's {@code BattleWin} robot prints a marker from each of {@code onWin}, * {@code onDeath}, {@code onRoundEnded}, and {@code onBattleEnded}. In a robot-against-itself @@ -22,9 +23,10 @@ * score report shows it. Routing through the queue did not fix it either, and AN-009 now has * the reason: the death never reaches the bot, so there is nothing for any dispatcher to route. * - * Every passing test in this class is tagged for a criterion it does not prove -- the win-handler - * test and both round-completion tests assert behaviour no criterion in CAP-001 covers. G-002 - * carries that; do not read a passing test here as evidence for the criterion in its name. + * The win-handler and round-completion tests were originally tagged EVT-004 and EVT-005; G-002 + * found neither tag matched what the test asserted, since no criterion in CAP-001 covered + * round or battle completion at all. EVT-012 and EVT-011 were minted for what these tests + * actually prove, and the tests below are retagged and renamed accordingly. */ class RoundOutcomeEventsConformanceTest extends ConformanceTestBase { @@ -46,16 +48,16 @@ void testEVT004_IntegrationPositive_OwnDeathReachesTheDeathHandler() { } @Test - @DisplayName("EVT-004: winning a round reaches onWin on both engines") - void testEVT004_IntegrationPositive_WinningARoundReachesTheWinHandler() { + @DisplayName("EVT-012: winning a round reaches onWin on both engines") + void testEVT012_IntegrationPositive_WinningARoundReachesTheWinHandler() { assertOnBothEngines(ROBOT, (outcome, engine) -> assertTrue(outcome.anyConsoleContains("Win!"), () -> "no robot reported winning on " + engine + " (" + outcome.summary() + ")")); } @Test - @DisplayName("EVT-005: round and battle completion reach their handlers on both engines") - void testEVT005_IntegrationPositive_RoundAndBattleCompletionReachTheirHandlers() { + @DisplayName("EVT-011: round and battle completion reach their handlers on both engines") + void testEVT011_IntegrationPositive_RoundAndBattleCompletionReachTheirHandlers() { assertOnBothEngines(ROBOT, (outcome, engine) -> { assertTrue(outcome.anyConsoleContains("RoundEnded!"), () -> "onRoundEnded was not reported on " + engine + " (" + outcome.summary() + ")"); @@ -65,21 +67,23 @@ void testEVT005_IntegrationPositive_RoundAndBattleCompletionReachTheirHandlers() } @Test - @DisplayName("EVT-005 negative: round completion is reported once per round, not once per participant") - void testEVT005_IntegrationNegative_RoundCompletionIsNotReportedMoreThanOncePerRound() { + @DisplayName("EVT-011 negative: round completion is reported once per round, not once per participant") + void testEVT011_IntegrationNegative_RoundCompletionIsNotReportedMoreThanOncePerRound() { + int expectedRounds = configuredRounds(); assertOnBothEngines(ROBOT, (outcome, engine) -> { // Each participant sees each round end exactly once. A dispatcher that delivered // the event once per participant to every participant would satisfy the positive - // test above and fail here. + // test above and fail here, because it would double (or worse) the count below + // rather than merely clear a lower bound. for (String console : outcome.consoles()) { int rounds = countIn(console, "RoundEnded!"); int battles = countIn(console, "BattleEnded!"); assertTrue(battles == 1, () -> "a participant reported the battle ending " + battles + " times on " + engine); - assertTrue(rounds >= battles, + assertTrue(rounds == expectedRounds, () -> "a participant reported " + rounds + " round endings on " + engine - + ", fewer than the battles it saw end"); + + ", expected exactly " + expectedRounds); } }); } diff --git a/docs/capabilities/CAP-001-event-dispatch-parity/README.md b/docs/capabilities/CAP-001-event-dispatch-parity/README.md index c7b7b52..4384292 100644 --- a/docs/capabilities/CAP-001-event-dispatch-parity/README.md +++ b/docs/capabilities/CAP-001-event-dispatch-parity/README.md @@ -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, 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. +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. diff --git a/docs/capabilities/CAP-001-event-dispatch-parity/criteria.md b/docs/capabilities/CAP-001-event-dispatch-parity/criteria.md index 8ab5883..8a570f8 100644 --- a/docs/capabilities/CAP-001-event-dispatch-parity/criteria.md +++ b/docs/capabilities/CAP-001-event-dispatch-parity/criteria.md @@ -11,7 +11,7 @@ reversal-cost: low # CAP-001 — acceptance criteria -Every criterion here is `@draft` against `M-001`. The behaviour is implemented; the evidence is not. Each names the classic test robot that will prove it, because classic's own conformance suite already encodes these expectations and the conformance tier restates them against both engines. +Most criteria here are `@draft` against `M-001`; three (`EVT-011`, `EVT-012`, `EVT-013`) are active. Each names the classic test robot that will prove it, because classic's own conformance suite already encodes these expectations and the conformance tier restates them against both engines. ```gherkin Feature: Event dispatch and timing parity @@ -32,17 +32,15 @@ Feature: Event dispatch and timing parity Then every scan event the classic run delivered is also delivered under the bridge # The defect this criterion exists for discarded exactly these events. Plan door: M-001. - @EVT-003 @draft + @EVT-003 @retired Scenario: An interruptible handler is re-entered when a higher-priority event arrives Test-type: Integration Given a robot that turns its radar from inside onHitWall and marks when it is scanned When the same battle runs on both engines Then the robot reports being scanned on both engines - # The ported InteruptibleEvent robot is tested in InterruptibleEventConformanceTest and passes, - # but it does not prove this scenario's name: the robot sets HitWallEvent to the SAME priority as - # ScannedRobotEvent, so nothing higher-priority arrives, and the assertion cannot separate - # re-entry from an ordinary later delivery. Promotion was attempted in CH-003 and reverted. - # This is the same defect as the EVT-005 mistag; see G-002. Plan door: M-001. + # Retired: no robot in the source tree exercises genuine higher-priority re-entry, and + # classic's own InteruptibleEvent deliberately uses the SAME priority. See IDR-003. + # Superseded by EVT-013. @EVT-004 @draft Scenario: A robot's own death reaches its death handler @@ -61,9 +59,8 @@ Feature: Event dispatch and timing parity Given a robot that records the turn number at each handler entry When the same battle runs on both engines Then each handler is entered at the same point in the turn on both engines - # Draft, and not proven by the tests currently tagged EVT-005: those assert round and battle - # completion, which is different behaviour and which no criterion here covers. See G-002. - # Plan door: M-001. + # Draft: no test proves per-turn handler timing yet. The two tests once tagged EVT-005 + # proved round/battle completion instead and are now EVT-011; see G-002. Plan door: M-001. @EVT-006 @draft Scenario: Custom events fire and can be removed @@ -107,4 +104,33 @@ Feature: Event dispatch and timing parity Then the per-turn scan counts match on both engines # The division the harness has never run, and the one that carries the most # same-priority events per turn. Plan door: M-001. + + @EVT-011 + Scenario: Round and battle completion each reach their handler exactly once + Test-type: Integration + Given a robot that reports from onRoundEnded and onBattleEnded + When the same battle runs on classic Robocode and on Tank Royale through the bridge + Then both handlers are reported on both engines + And a round ending is reported exactly once per round the robot saw end + # Proven by the ported BattleWin robot in RoundOutcomeEventsConformanceTest. Named for what + # those tests actually assert, after G-002 found them mistagged EVT-005. Plan door: M-001. + + @EVT-012 + Scenario: Winning a round reaches the win handler + Test-type: Integration + Given a robot that reports from onWin + When the robot wins a round on each engine + Then the report appears on both engines + # Proven by the ported BattleWin robot in RoundOutcomeEventsConformanceTest. Named for what + # that test actually asserts, after G-002 found it mistagged EVT-004. Plan door: M-001. + + @EVT-013 + Scenario: An interruptible handler is re-entered for a same-priority event once marked interruptible + Test-type: Integration + Given a robot that turns its radar from inside onHitWall, at the same event priority as a scan, + and has called setInterruptible(true) + When the same battle runs on both engines + Then the robot reports being scanned on both engines + # Proven by the ported InteruptibleEvent robot in InterruptibleEventConformanceTest. Successor + # to the retired EVT-003; see IDR-003 for why the claim is scoped to same-priority re-entry. ``` diff --git a/docs/capabilities/CAP-003-robocode-api-surface-fidelity/criteria.md b/docs/capabilities/CAP-003-robocode-api-surface-fidelity/criteria.md index 0d0f4d4..c162ca6 100644 --- a/docs/capabilities/CAP-003-robocode-api-surface-fidelity/criteria.md +++ b/docs/capabilities/CAP-003-robocode-api-surface-fidelity/criteria.md @@ -17,13 +17,17 @@ These are the corpus's only machine-proven criteria. Each is proven by unit test Feature: Robocode API surface fidelity @API-001 - Scenario: Angles convert between the Robocode and Tank Royale conventions + Scenario: Angles convert from the Tank Royale convention to the Robocode convention (single-direction) Test-type: Unit - Given an angle expressed in one engine's convention - When it is converted to the other - Then the result is the same direction expressed in the target convention - And converting it back yields the original angle + Given an angle expressed in Tank Royale's convention + When it is converted to Robocode's convention + Then the result is the same direction expressed in Robocode's convention And the discontinuity where the circle wraps is handled rather than producing a reflected angle + # The adapter converts only this direction: outbound turn commands are relative rotations + # in the same winding sense on both engines and need no conversion, so nothing in the + # bridge ever converts a Robocode-convention angle back to Tank Royale's. AN-010 found the + # round-trip clause this scenario once had was untestable because no such method exists, + # and the asymmetry is architectural rather than a gap. @API-002 Scenario: Colours convert between Robocode's Color and Tank Royale's representation @@ -44,7 +48,7 @@ Feature: Robocode API surface fidelity @API-004 Scenario: Bullets map to Robocode's Bullet with owner, power, and heading preserved Test-type: Unit - Given a Tank Royale bullet + Given a Tank Royale bullet whose owner id does not match any bot currently in the battle When it is mapped for a robot Then its power, heading, and owning robot are preserved And a bullet whose owner is no longer in the battle still maps rather than failing diff --git a/docs/decisions/IDR-003-evt-003-scoped-to-what-classic-actually-proves.md b/docs/decisions/IDR-003-evt-003-scoped-to-what-classic-actually-proves.md new file mode 100644 index 0000000..69966cb --- /dev/null +++ b/docs/decisions/IDR-003-evt-003-scoped-to-what-classic-actually-proves.md @@ -0,0 +1,29 @@ +--- +id: IDR-003 +type: decision +status: inferred +author: agent +accepted-by: [] +links: [CAP-001, G-002] +title: EVT-003's higher-priority claim is retired; interruptible re-entry evidence is scoped to what classic's own robot proves +--- + +# IDR-003 — EVT-003's higher-priority claim is retired; interruptible re-entry evidence is scoped to what classic's own robot proves + +## Decision + +`EVT-003` retires. Its successor, `EVT-013`, claims that an interruptible handler is re-entered for a **same-priority** event once `setInterruptible(true)` is called — not a higher-priority one. `InterruptibleEventConformanceTest` is retagged to `EVT-013` unchanged; it already proves this. + +## Context + +`EVT-003` said a higher-priority event re-enters an interruptible handler. The only robot in the source tree written to exercise this, classic's own `tested.robots.InteruptibleEvent`, does the opposite on purpose: `setEventPriority("HitWallEvent", getEventPriority("ScannedRobotEvent"))` — same priority, by explicit comment ("make same as scan"). Classic's own suite (`TestInteruptibleEvent`) asserts only that a scan marker appears, the identical weak assertion the bridge's copy carries. The higher-priority claim was never classic's claim; it was invented when `CAP-001` was drafted, and no robot exists to prove it. + +`G-002` named this as needing a decision rather than a mechanical retag: either narrow the name to match the test (retiring `EVT-003`), or strengthen the assertion to separate re-entry from ordinary delivery (keeping it). Strengthening is not available here — the robot's source is fixed (classic is the specification, `ARCH-002`), so the priorities cannot be changed to produce a genuine higher-priority case, and no substitute robot exercises one. + +## Why this way + +A criterion this repository can never gather evidence for, because no available robot exercises it, is worse than a narrower criterion the repository actually enforces. `EVT-013`'s scope is exactly what the fixed evidence source can prove, so a passing test means what it claims for as long as it passes. + +## Consequences + +Genuine higher-priority interruption re-entry is now uncovered rather than falsely covered. If a future analysis finds or writes a robot that produces one, mint a new criterion for it rather than reopening `EVT-013` — that would be a new behaviour, not a repair to this one. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 5227314..2de4dbe 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -25,4 +25,5 @@ A decision that changes a methodology contract inventories every live carrier th - [IDR-001 — Route robot events through the Bot API's event queue](IDR-001-route-events-through-the-bot-api-event-queue.md) · `verified` — Why the bridge delegates dispatch instead of holding a second implementation of the engine's event semantics. - [IDR-002 — getDataDirectory resolves through the same robot data lookup getDataFile uses](IDR-002-data-directory-resolves-through-robot-data.md) · `verified` — One question about where a robot's data lives should have one answer. Alignment, not confinement. - [PDR-001 — Evidence comes in three tiers, separated by cost and by what each can prove](PDR-001-three-tier-evidence-strategy.md) · `verified` — Unit, conformance, and sweep: what each tier can establish, and what is rejected because Tank Royale has no seed. +- [IDR-003 — EVT-003's higher-priority claim is retired; interruptible re-entry evidence is scoped to what classic's own robot proves](IDR-003-evt-003-scoped-to-what-classic-actually-proves.md) · `inferred` — `EVT-003` retires. diff --git a/docs/goals/G-002-conformance-evidence-proves-the-criterion-it-names.md b/docs/goals/G-002-conformance-evidence-proves-the-criterion-it-names.md index 77342f7..78b4bab 100644 --- a/docs/goals/G-002-conformance-evidence-proves-the-criterion-it-names.md +++ b/docs/goals/G-002-conformance-evidence-proves-the-criterion-it-names.md @@ -37,14 +37,12 @@ It also costs evidence in the other direction: the round-outcome behaviour these `AN-010` found the unit tier does not have this problem at the same severity — every `API` and `ROUTE` criterion has a test, no test carries a nonexistent ID, and assertions name the specific call and argument rather than a loose marker, which is the vocabulary that let the conformance-tier mismatches through unnoticed. It still has two narrow gaps: `API-001`'s round-trip clause is untested because the adapter has no reverse angle conversion to test it against, and `API-004`'s owner-departed case has no test — the test carrying its tag proves a null-victim case instead, a different field. Neither is the promote-on-false-evidence failure the conformance tier produced; both are gaps in an otherwise-tight net. -## What it would take +## What was done -Mint criteria for what the tests actually assert — round and battle completion reaching their handlers, and the once-per-round shape — and retag the tests to them. `EVT-004` and `EVT-005` keep their meaning and stay `@draft`; nothing is retired, because neither criterion's meaning changes. +`CH-004` closed every instance this goal named. `EVT-011` and `EVT-012` were minted for what the round-completion and win-handler tests actually assert, and the tests retagged to them. `EVT-003` was retired rather than repaired — no robot in the source tree exercises genuine higher-priority re-entry, so its successor `EVT-013` claims only the same-priority re-entry the evidence can actually show (`IDR-003`). `API-001` was narrowed to the one direction the adapter implements; `API-004` gained the owner-departed test it was missing. -`EVT-003` needs a decision rather than a retag: either its name comes down to what the robot does, which changes the criterion's meaning and therefore retires it and mints a successor, or the assertion grows teeth that separate re-entry from ordinary later delivery. The second keeps the promise and costs a test; the first keeps the test and costs the promise. +## What remains -The unit tier's two gaps are smaller repairs and do not need a criterion-meaning decision: either add the `API-001` round-trip test or narrow the scenario to the one direction the adapter implements, and either add the `API-004` owner-departed case or narrow the scenario to what `testAPI004_UnitNegative_MapsABulletThatHasNotHitAnything` already proves and rename it. +Only `InterruptibleEventConformanceTest`, `RoundOutcomeEventsConformanceTest`, and the unit tier's thirteen test classes under `robocode-api/src/test/java/.../bridge/` have been read against their criteria. Nothing else in the corpus has been — the conformance classes that will be added for `CAP-002`'s physics criteria (`M-111`–`M-118`) carry the same risk this goal exists to catch, and each should be checked as it lands rather than assumed clean by analogy. -The conformance tier's `InterruptibleEventConformanceTest` and `RoundOutcomeEventsConformanceTest` and the unit tier's thirteen test classes under `robocode-api/src/test/java/.../bridge/` are now read against their criteria; nothing else in the corpus has been. - -`M-001` should not close before this is done, because its fourth clause is about criteria being active rather than `@draft`, and a mistag is the one thing that makes that clause easy to satisfy dishonestly. +`M-001` should not close while any capability's criteria have not been checked this way, because its fourth clause is about criteria being active rather than `@draft`, and a mistag is the one thing that makes that clause easy to satisfy dishonestly. diff --git a/docs/plans/P-001-bridge-parity-campaign.md b/docs/plans/P-001-bridge-parity-campaign.md index cb50e44..0196f14 100644 --- a/docs/plans/P-001-bridge-parity-campaign.md +++ b/docs/plans/P-001-bridge-parity-campaign.md @@ -42,7 +42,7 @@ They are bookkeeping rather than a second plan. A door closes when its criterion |---|---|---|---| | M-101 | `EVT-001` | `EVT-001` is active, with evidence attributable to it. Work lands under M-001. | todo | | M-102 | `EVT-002` | `EVT-002` is active, with evidence attributable to it. Work lands under M-001. | todo | -| M-103 | `EVT-003` | `EVT-003` is active, with evidence attributable to it. Work lands under M-001. | todo | +| M-103 | `EVT-003` | Dropped: `EVT-003` retired (`IDR-003`); see `M-141` for its successor `EVT-013`. | dropped | | M-104 | `EVT-004` | `EVT-004` is active, with evidence attributable to it. Work lands under M-001. | todo | | M-105 | `EVT-005` | `EVT-005` is active, with evidence attributable to it. Work lands under M-001. | todo | | M-106 | `EVT-006` | `EVT-006` is active, with evidence attributable to it. Work lands under M-001. | todo | @@ -78,6 +78,9 @@ They are bookkeeping rather than a second plan. A door closes when its criterion | M-136 | `HARN-005` | `HARN-005` is active, with evidence attributable to it. Work lands under M-001. | todo | | M-137 | `HARN-006` | `HARN-006` is active, with evidence attributable to it. Work lands under M-001. | todo | | M-138 | `HARN-007` | `HARN-007` is active, with evidence attributable to it. Work lands under M-001. | todo | +| M-139 | `EVT-011` | `EVT-011` is active, with evidence attributable to it. Work lands under M-001. | done | +| M-140 | `EVT-012` | `EVT-012` is active, with evidence attributable to it. Work lands under M-001. | done | +| M-141 | `EVT-013` | `EVT-013` is active, with evidence attributable to it. Successor to `EVT-003` (`M-103`). Work lands under M-001. | done | ## Why this order diff --git a/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/AngleConverterTest.java b/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/AngleConverterTest.java index 2b5af2a..aa9aab3 100644 --- a/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/AngleConverterTest.java +++ b/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/AngleConverterTest.java @@ -9,14 +9,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Acceptance evidence for API-001 — angles convert between the Robocode and Tank Royale - * conventions. + * Acceptance evidence for API-001 — angles convert from the Tank Royale convention to the + * Robocode convention (single-direction). *

* The two engines disagree about where zero is and which way is positive. Tank Royale * measures direction counter-clockwise from east; Robocode measures clockwise from north. * A conversion that is right in the middle of the range and wrong at the wrap is the * failure this class is written to catch, which is why the negative direction concentrates * on the discontinuity rather than on rejected input. + *

+ * There is no reverse conversion to test: outbound turn commands are relative rotations in + * the same winding sense on both engines, so nothing in the bridge ever converts a + * Robocode-convention angle back to Tank Royale's. */ class AngleConverterTest { diff --git a/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/BulletMapperTest.java b/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/BulletMapperTest.java index f8049a2..628df93 100644 --- a/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/BulletMapperTest.java +++ b/robocode-api/src/test/java/dev/robocode/tankroyale/bridge/BulletMapperTest.java @@ -61,6 +61,17 @@ void testAPI004_UnitNegative_MapsABulletThatHasNotHitAnything() { assertEquals(1.5, mapped.getPower(), EPSILON); } + @Test + @DisplayName("API-004 negative: a bullet whose owner has left the battle still maps") + void testAPI004_UnitNegative_MapsABulletWhoseOwnerHasLeftTheBattle() { + // BulletMapper.map has no roster to check the owner id against, so a departed + // owner's id is indistinguishable from any other -- it is carried across as-is. + Bullet mapped = BulletMapper.map(bullet(11, 999, 3.0, 5.0, 6.0, 0), "Target"); + + assertNotNull(mapped, "a bullet whose owner has left the battle must still map"); + assertEquals("999", mapped.getName(), "the departed owner's id is carried across unchanged"); + } + @Test @DisplayName("API-004 negative: an out-of-range direction is normalised rather than carried through") void testAPI004_UnitNegative_NormalisesAnOutOfRangeDirection() {