Skip to content

Commit 5db6eb0

Browse files
Implement CH-007 event conformance evidence
1 parent 8b1879f commit 5db6eb0

14 files changed

Lines changed: 521 additions & 19 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package conformance.probes;
2+
3+
import robocode.AdvancedRobot;
4+
import robocode.ScannedRobotEvent;
5+
6+
/** Keeps a radar sweep running while a scan handler makes a blocking move. */
7+
public class BlockingScanProbe extends AdvancedRobot {
8+
9+
@Override
10+
public void run() {
11+
setAdjustRadarForRobotTurn(true);
12+
while (true) {
13+
setTurnRadarRight(360);
14+
setAhead(20);
15+
execute();
16+
}
17+
}
18+
19+
@Override
20+
public void onScannedRobot(ScannedRobotEvent event) {
21+
out.println("BlockingScanDelivered!");
22+
ahead(100);
23+
}
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package conformance.probes;
2+
3+
import robocode.AdvancedRobot;
4+
import robocode.Condition;
5+
import robocode.CustomEvent;
6+
7+
/** Registers an always-true condition and removes it after its first delivery. */
8+
public class CustomEventRemovalProbe extends AdvancedRobot {
9+
10+
@Override
11+
public void run() {
12+
addCustomEvent(new Condition("one-shot", 80) {
13+
@Override
14+
public boolean test() {
15+
return true;
16+
}
17+
});
18+
while (true) {
19+
ahead(50);
20+
}
21+
}
22+
23+
@Override
24+
public void onCustomEvent(CustomEvent event) {
25+
out.println("CustomEventFired!");
26+
removeCustomEvent(event.getCondition());
27+
out.println("CustomEventRemoved!");
28+
}
29+
}
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.AdvancedRobot;
4+
import robocode.ScannedRobotEvent;
5+
import robocode.StatusEvent;
6+
7+
/** Reports the number of scan callbacks carried by each observed turn. */
8+
public class MeleeScanProbe extends AdvancedRobot {
9+
10+
private long currentTurn = -1;
11+
private int scansThisTurn;
12+
13+
@Override
14+
public void run() {
15+
setAdjustRadarForRobotTurn(true);
16+
while (true) {
17+
setTurnRadarRight(360);
18+
execute();
19+
}
20+
}
21+
22+
@Override
23+
public void onStatus(StatusEvent event) {
24+
if (currentTurn >= 0) {
25+
out.println("MeleeScanCount:" + currentTurn + ":" + scansThisTurn);
26+
}
27+
currentTurn = event.getTime();
28+
scansThisTurn = 0;
29+
}
30+
31+
@Override
32+
public void onScannedRobot(ScannedRobotEvent event) {
33+
scansThisTurn++;
34+
}
35+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package conformance.probes;
2+
3+
import robocode.AdvancedRobot;
4+
import robocode.SkippedTurnEvent;
5+
import robocode.StatusEvent;
6+
7+
/** Deliberately overruns a bounded number of status callbacks to produce skipped turns. */
8+
public class SkippedTurnProbe extends AdvancedRobot {
9+
10+
private int statusCallbacks;
11+
private int round;
12+
13+
@Override
14+
public void run() {
15+
while (true) {
16+
turnLeft(1);
17+
}
18+
}
19+
20+
@Override
21+
public void onStatus(StatusEvent event) {
22+
round = event.getStatus().getRoundNum();
23+
if (++statusCallbacks <= 10) {
24+
try {
25+
Thread.sleep(130);
26+
} catch (InterruptedException interrupted) {
27+
Thread.currentThread().interrupt();
28+
}
29+
}
30+
}
31+
32+
@Override
33+
public void onSkippedTurn(SkippedTurnEvent event) {
34+
out.println("SkippedTurnReported:" + round + ":" + event.getSkippedTurn());
35+
}
36+
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package conformance.probes;
2+
3+
import robocode.AdvancedRobot;
4+
import robocode.RobotStatus;
5+
import robocode.StatusEvent;
6+
7+
/** Records the three clocks visible when a new-turn status event reaches the robot. */
8+
public class TurnBoundaryProbe extends AdvancedRobot {
9+
10+
@Override
11+
public void run() {
12+
while (true) {
13+
setTurnRadarRight(360);
14+
execute();
15+
}
16+
}
17+
18+
@Override
19+
public void onStatus(StatusEvent event) {
20+
RobotStatus status = event.getStatus();
21+
out.println("TurnStatus:" + status.getRoundNum() + ":" + event.getTime() + ":"
22+
+ status.getTime() + ":" + getTime());
23+
}
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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-002 — pending same-priority scans survive a blocking handler. */
11+
class BlockingScanConformanceTest extends ConformanceTestBase {
12+
13+
private static final String ROBOT = "conformance.probes.BlockingScanProbe";
14+
private static final String ENEMY = "sample.Target";
15+
private static final String SCAN = "BlockingScanDelivered!";
16+
private static final Path SOURCE = ConformanceHarness.repoRoot().resolve(Path.of(
17+
"compat-test", "conformance-robots", "conformance", "probes", "BlockingScanProbe.java"));
18+
19+
@Test
20+
@DisplayName("EVT-002: pending same-priority scans are delivered after a blocking handler")
21+
void testEVT002_IntegrationPositive_PendingScansSurviveBlockingHandler() {
22+
assertOnBothEngines(ROBOT, SOURCE, ENEMY, (outcome, engine) ->
23+
assertTrue(outcome.countOf(SCAN) >= 2,
24+
() -> "fewer than two scan callbacks reached the blocking probe on " + engine
25+
+ " (" + outcome.summary() + ")"));
26+
}
27+
28+
@Test
29+
@DisplayName("EVT-002 negative: a completed bridge run does not lose all pending scans")
30+
void testEVT002_IntegrationNegative_BridgeDoesNotDiscardPendingScans() {
31+
assertOnBothEngines(ROBOT, SOURCE, ENEMY, (outcome, engine) ->
32+
assertTrue(outcome.anyConsoleContains(SCAN),
33+
() -> "the scan handler was never entered on " + engine
34+
+ " (" + outcome.summary() + ")"));
35+
}
36+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ BattleOutcome run(Engine engine, String robotClass, Path source) {
100100

101101
/** Runs a robot against a named opponent fixture when the source test requires one. */
102102
BattleOutcome run(Engine engine, String robotClass, Path source, String enemyClass) {
103+
return run(engine, robotClass, source, enemyClass, null);
104+
}
105+
106+
/** Runs a robot with an explicit participant count, such as the official melee setup. */
107+
BattleOutcome run(Engine engine, String robotClass, Path source, int participants) {
108+
return run(engine, robotClass, source, null, participants);
109+
}
110+
111+
private BattleOutcome run(Engine engine, String robotClass, Path source, String enemyClass,
112+
Integer participants) {
103113
List<String> command = new ArrayList<>(List.of(
104114
python,
105115
HARNESS.toString(),
@@ -116,6 +126,10 @@ BattleOutcome run(Engine engine, String robotClass, Path source, String enemyCla
116126
command.add("--enemy-class");
117127
command.add(enemyClass);
118128
}
129+
if (participants != null) {
130+
command.add("--participants");
131+
command.add(String.valueOf(participants));
132+
}
119133

120134
try {
121135
Process process = new ProcessBuilder(command)

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ void assertOnBothEngines(String robotClass, Path source, Expectation expectation
5252
assertOnBothEngines(robotClass, source, null, expectation);
5353
}
5454

55+
/** Runs a locally held probe with an explicit participant count on both engines. */
56+
void assertOnBothEngines(String robotClass, Path source, int participants,
57+
Expectation expectation) {
58+
assertOnBothEnginesFixture(robotClass, source, null, participants, expectation);
59+
}
60+
5561
/** Runs a robot against the same named opponent fixture on both engines. */
5662
void assertOnBothEngines(String robotClass, String enemyClass, Expectation expectation) {
5763
assertOnBothEngines(robotClass, null, enemyClass, expectation);
@@ -60,13 +66,19 @@ void assertOnBothEngines(String robotClass, String enemyClass, Expectation expec
6066
/** Runs a locally held probe against the same named opponent fixture on both engines. */
6167
void assertOnBothEngines(String robotClass, Path source, String enemyClass,
6268
Expectation expectation) {
63-
assertOnBothEnginesFixture(robotClass, source, enemyClass, expectation);
69+
assertOnBothEnginesFixture(robotClass, source, enemyClass, null, expectation);
6470
}
6571

6672
private void assertOnBothEnginesFixture(String robotClass, Path source, String enemyClass,
6773
Expectation expectation) {
74+
assertOnBothEnginesFixture(robotClass, source, enemyClass, null, expectation);
75+
}
76+
77+
private void assertOnBothEnginesFixture(String robotClass, Path source, String enemyClass,
78+
Integer participants, Expectation expectation) {
6879
for (Engine engine : Engine.values()) {
69-
BattleOutcome outcome = outcomeFor(engine, robotClass, source, enemyClass);
80+
BattleOutcome outcome = outcomeForFixture(engine, robotClass, source, enemyClass,
81+
participants);
7082
assertTrue(outcome.completed(),
7183
() -> "the battle did not complete on " + engine + " (" + outcome.summary() + ")");
7284
expectation.check(outcome, engine);
@@ -95,13 +107,29 @@ BattleOutcome outcomeFor(Engine engine, String robotClass, String enemyClass) {
95107

96108
/** Runs a probe against an opponent fixture, reusing that exact battle within this test. */
97109
BattleOutcome outcomeFor(Engine engine, String robotClass, Path source, String enemyClass) {
98-
return outcomeForFixture(engine, robotClass, source, enemyClass);
110+
return outcomeForFixture(engine, robotClass, source, enemyClass, null);
111+
}
112+
113+
/** Runs a probe with an explicit participant count, reusing the result within this test. */
114+
BattleOutcome outcomeFor(Engine engine, String robotClass, Path source, int participants) {
115+
return outcomeForFixture(engine, robotClass, source, null, participants);
99116
}
100117

101118
private BattleOutcome outcomeForFixture(Engine engine, String robotClass, Path source,
102119
String enemyClass) {
120+
return outcomeForFixture(engine, robotClass, source, enemyClass, null);
121+
}
122+
123+
private BattleOutcome outcomeForFixture(Engine engine, String robotClass, Path source,
124+
String enemyClass, Integer participants) {
103125
String key = engine.name() + " " + robotClass + " vs " + enemyClass;
104-
return ran.computeIfAbsent(key, ignored -> harness.run(engine, robotClass, source, enemyClass));
126+
if (participants != null) {
127+
key += " with " + participants + " participants";
128+
}
129+
Integer requestedParticipants = participants;
130+
return ran.computeIfAbsent(key, ignored -> requestedParticipants == null
131+
? harness.run(engine, robotClass, source, enemyClass)
132+
: harness.run(engine, robotClass, source, requestedParticipants));
105133
}
106134

107135
/** The number of rounds every battle in this run is configured for. */
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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-006 — custom events register, dispatch, and can be removed. */
11+
class CustomEventsConformanceTest extends ConformanceTestBase {
12+
13+
private static final String CLASSIC_ROBOT = "tested.robots.CustomEvents";
14+
private static final String REMOVAL_ROBOT = "conformance.probes.CustomEventRemovalProbe";
15+
private static final String ENEMY = "sample.Target";
16+
private static final Path SOURCE = ConformanceHarness.repoRoot().resolve(Path.of(
17+
"compat-test", "conformance-robots", "conformance", "probes",
18+
"CustomEventRemovalProbe.java"));
19+
20+
@Test
21+
@DisplayName("EVT-006: classic custom-event conditions dispatch on both engines")
22+
void testEVT006_IntegrationPositive_ClassicCustomEventsDispatch() {
23+
assertOnBothEngines(CLASSIC_ROBOT, ENEMY, (outcome, engine) -> {
24+
assertTrue(outcome.anyConsoleContains("onTick99"),
25+
() -> "priority-99 custom event was not delivered on " + engine);
26+
assertTrue(outcome.anyConsoleContains("onTick30"),
27+
() -> "priority-30 custom event was not delivered on " + engine);
28+
});
29+
}
30+
31+
@Test
32+
@DisplayName("EVT-006: removing a custom condition stops subsequent deliveries")
33+
void testEVT006_IntegrationPositive_RemovedCustomEventStopsFiring() {
34+
assertOnBothEngines(REMOVAL_ROBOT, SOURCE, ENEMY, (outcome, engine) -> {
35+
assertTrue(outcome.anyConsoleContains("CustomEventFired!"),
36+
() -> "custom event never fired on " + engine);
37+
assertTrue(outcome.anyConsoleContains("CustomEventRemoved!"),
38+
() -> "custom event was not removed on " + engine);
39+
});
40+
}
41+
42+
@Test
43+
@DisplayName("EVT-006 negative: a removed custom condition fires at most once per round")
44+
void testEVT006_IntegrationNegative_RemovedCustomEventDoesNotRepeat() {
45+
assertOnBothEngines(REMOVAL_ROBOT, SOURCE, ENEMY, (outcome, engine) -> {
46+
for (int fires : outcome.countsOf("CustomEventFired!")) {
47+
assertTrue(fires <= configuredRounds(),
48+
() -> "removed custom event fired " + fires + " times on " + engine
49+
+ ", more than once per configured round (" + outcome.summary() + ")");
50+
}
51+
});
52+
}
53+
}

0 commit comments

Comments
 (0)