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
37 changes: 31 additions & 6 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: "5"
CH: "6"
CRIT: "8"
DES: "8"
EVT: "14"
EVT: "15"
G: "2"
IDR: "4"
OQ: "2"
IDR: "5"
OQ: "3"
P: "1"
PDR: "2"
REH: "1"
ROUTE: "12"
TASKS: "4"
TASKS: "5"
entries:
- id: ADR-001
kind: numeric
Expand Down Expand Up @@ -218,6 +218,11 @@ entries:
state: live
prefix: CH
component: "5"
- id: CH-006
kind: numeric
state: live
prefix: CH
component: "6"
- id: CRIT-001
kind: numeric
state: live
Expand Down Expand Up @@ -300,7 +305,7 @@ entries:
component: "8"
- id: EVT-001
kind: numeric
state: live
state: retired
prefix: EVT
component: "1"
- id: EVT-002
Expand Down Expand Up @@ -368,6 +373,11 @@ entries:
state: live
prefix: EVT
component: "14"
- id: EVT-015
kind: numeric
state: live
prefix: EVT
component: "15"
- id: G-001
kind: numeric
state: live
Expand Down Expand Up @@ -398,6 +408,11 @@ entries:
state: live
prefix: IDR
component: "4"
- id: IDR-005
kind: numeric
state: live
prefix: IDR
component: "5"
- id: OQ-001
kind: numeric
state: live
Expand All @@ -408,6 +423,11 @@ entries:
state: live
prefix: OQ
component: "2"
- id: OQ-003
kind: numeric
state: live
prefix: OQ
component: "3"
- id: P-001
kind: numeric
state: live
Expand Down Expand Up @@ -508,3 +528,8 @@ entries:
state: live
prefix: TASKS
component: "4"
- id: TASKS-005
kind: numeric
state: live
prefix: TASKS
component: "5"
47 changes: 43 additions & 4 deletions compat-test/RcBattleWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import robocode.control.BattlefieldSpecification;
import robocode.control.BattleSpecification;
import robocode.control.RobocodeEngine;
import robocode.control.RandomFactory;
import robocode.control.RobotSpecification;
import robocode.control.events.BattleAdaptor;
import robocode.control.events.BattleCompletedEvent;
Expand Down Expand Up @@ -51,6 +52,7 @@ public static void main(String[] args) throws Exception {
int height = Integer.parseInt(opt.getOrDefault("height", "600"));
int participants = Integer.parseInt(opt.getOrDefault("participants", "2"));
String select = opt.get("select");
String enemySelect = opt.get("enemy-select");
Path outFile = Path.of(require(opt, "out"));
int timeoutSecs = Integer.parseInt(opt.getOrDefault("timeout", "0"));

Expand All @@ -61,7 +63,11 @@ public static void main(String[] args) throws Exception {
Map<String, Object> result = new HashMap<>();
int exitCode;
try {
runBattle(home, rounds, width, height, participants, select, result);
if (Boolean.parseBoolean(opt.getOrDefault("deterministic", "false"))) {
// Match RobocodeTestBed's deterministic setup used by the authoritative source test.
RandomFactory.resetDeterministic(0);
}
runBattle(home, rounds, width, height, participants, select, enemySelect, result);
exitCode = Boolean.TRUE.equals(result.get("ok")) ? 0 : 1;
} catch (Throwable t) {
result.put("ok", false);
Expand All @@ -74,14 +80,15 @@ public static void main(String[] args) throws Exception {
}

private static void runBattle(File home, int rounds, int width, int height, int participantCount,
String select, Map<String, Object> result) {
String select, String enemySelect, Map<String, Object> result) {
Collector collector = new Collector();
RobocodeEngine engine = new RobocodeEngine(home);
try {
engine.addBattleListener(collector);
engine.setVisible(false);

RobotSpecification[] participants = selectParticipants(engine, select, participantCount);
RobotSpecification[] participants = selectParticipants(
engine, select, participantCount, enemySelect);
if (participants == null || participants.length == 0) {
result.put("ok", false);
result.put("fatal", "No robot found in repository matching: " + select);
Expand Down Expand Up @@ -148,7 +155,16 @@ private static void runBattle(File home, int rounds, int width, int height, int
*
* The count is the division's official participant count: two for 1-vs-1, ten for melee.
*/
private static RobotSpecification[] selectParticipants(RobocodeEngine engine, String select, int count) {
private static RobotSpecification[] selectParticipants(RobocodeEngine engine, String select, int count,
String enemySelect) {
if (enemySelect != null && !enemySelect.isBlank()) {
RobotSpecification chosen = selectOne(engine, select);
RobotSpecification enemy = selectOne(engine, enemySelect);
if (chosen == null || enemy == null) {
return null;
}
return new RobotSpecification[]{chosen, enemy};
}
int wanted = Math.max(2, count);
if (select != null && !select.isBlank()) {
String selection = String.join(", ", java.util.Collections.nCopies(wanted, select));
Expand Down Expand Up @@ -176,6 +192,29 @@ private static RobotSpecification[] selectParticipants(RobocodeEngine engine, St
return participants;
}

private static RobotSpecification selectOne(RobocodeEngine engine, String select) {
if (select != null && !select.isBlank()) {
RobotSpecification[] specs = engine.getLocalRepository(select);
if (specs != null && specs.length > 0) {
return specs[0];
}
}
RobotSpecification[] all = engine.getLocalRepository();
if (all == null || all.length == 0) {
return null;
}
if (select != null && !select.isBlank()) {
String className = select.split(" ")[0];
for (RobotSpecification spec : all) {
if (className.equals(spec.getClassName())) {
return spec;
}
}
return null;
}
return all[0];
}

/** Collects results, errors and per-robot console output from battle events. */
private static class Collector extends BattleAdaptor {
final List<String> battleErrors = new ArrayList<>();
Expand Down
88 changes: 68 additions & 20 deletions compat-test/compat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ def save_state(state):
# Classic Robocode side
# ----------------------------------------------------------------------------------

def run_rc_battle(jar_path: Path, classname, version, opts, setup):
def run_rc_battle(jar_path: Path, classname, version, opts, setup,
enemy_jar_path=None, enemy_class=None):
"""Runs one classic Robocode battle for the jar at the division setup; returns a summary."""
home_dir = WORK_DIR / "rc-home"
(home_dir / "config").mkdir(parents=True, exist_ok=True)
Expand All @@ -394,6 +395,8 @@ def run_rc_battle(jar_path: Path, classname, version, opts, setup):
robots_dir = WORK_DIR / "rc-robots"
clean_dir(robots_dir)
shutil.copyfile(jar_path, robots_dir / jar_path.name)
if enemy_jar_path is not None:
shutil.copyfile(enemy_jar_path, robots_dir / Path(enemy_jar_path).name)

out_file = WORK_DIR / "rc-result.json"
out_file.unlink(missing_ok=True)
Expand All @@ -420,6 +423,8 @@ def run_rc_battle(jar_path: Path, classname, version, opts, setup):
"--out", str(out_file),
"--timeout", str(max(30, opts.timeout - 15)),
]
if enemy_class:
cmd.extend(["--enemy-select", enemy_class, "--deterministic", "true"])
started = time.time()
returncode, output, timed_out = run_java(cmd, cwd=home_dir, timeout=opts.timeout)
elapsed = time.time() - started
Expand Down Expand Up @@ -485,9 +490,16 @@ def summarize_worker_result(out_file, returncode, output, timed_out, elapsed, en
# Tank Royale side
# ----------------------------------------------------------------------------------

def ensure_tr_lib(opts):
lib_dir = WORK_DIR / "tr-bots" / "lib"
def ensure_tr_lib(opts, staging):
lib_dir = staging / "lib"
lib_dir.mkdir(parents=True, exist_ok=True)
# A previous run may have staged a different Bot API version. The generated boot scripts
# use a wildcard classpath, so retaining both versions silently selects the wrong one.
for entry in list(lib_dir.iterdir()):
if entry.is_dir():
shutil.rmtree(entry, ignore_errors=True)
else:
entry.unlink(missing_ok=True)
for jar in (opts.bridge_api_jar, opts.bot_api_jar, opts.wrapper_jar):
src = Path(jar)
dst = lib_dir / src.name
Expand Down Expand Up @@ -536,11 +548,11 @@ def stage_bot_dirs(bot_dir: Path, participants):
return dirs


def wrap_jar_for_tr(jar_path: Path, classname, version, opts):
def wrap_jar_for_tr(jar_path: Path, classname, version, opts, staging_name="tr-bots"):
"""Stages the jar, runs the robots-wrapper, returns (bot_dir, error_message)."""
staging = WORK_DIR / "tr-bots"
staging = WORK_DIR / staging_name
staging.mkdir(parents=True, exist_ok=True)
ensure_tr_lib(opts)
ensure_tr_lib(opts, staging)

# Remove artifacts from the previous robot (keep lib/)
for entry in staging.iterdir():
Expand Down Expand Up @@ -572,7 +584,8 @@ def wrap_jar_for_tr(jar_path: Path, classname, version, opts):
return chosen, None


def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures=None):
def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures=None,
enemy_jar_path=None, enemy_class=None):
"""Wraps the jar and runs one Tank Royale battle at the division setup.

`rc_signatures` is the set of exception signatures the classic side produced for this
Expand All @@ -587,7 +600,18 @@ def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures
"error_count": 1, "log_text": "=== Wrapping failed ===\n" + wrap_error,
}

bot_dirs = stage_bot_dirs(bot_dir, setup["participants"])
bot_dirs = [bot_dir] if enemy_jar_path is not None else stage_bot_dirs(
bot_dir, setup["participants"])
if enemy_jar_path is not None:
enemy_dir, enemy_error = wrap_jar_for_tr(
enemy_jar_path, enemy_class, None, opts, staging_name="tr-bots-enemy")
if enemy_error:
return {
"ok": False, "score": None, "scores": [], "elapsed": 0.0,
"errors": ["HARNESS: " + enemy_error.splitlines()[0]],
"error_count": 1, "log_text": "=== Wrapping opponent failed ===\n" + enemy_error,
}
bot_dirs.append(enemy_dir)
out_file = WORK_DIR / "tr-result.json"
out_file.unlink(missing_ok=True)

Expand Down Expand Up @@ -896,6 +920,8 @@ def parse_args():
conf.add_argument("--robot-class",
help="robot class to select, when it cannot be derived from the "
"jar name (classic's test robot jars hold many robots)")
conf.add_argument("--enemy-class",
help="run the selected robot against this opponent fixture instead of copies")
conf.add_argument("--participants", type=int, default=None,
help="participant count for --conformance")

Expand Down Expand Up @@ -1232,6 +1258,11 @@ def run_conformance(opts):
setup["rounds"] = opts.rounds
if opts.participants is not None:
setup["participants"] = opts.participants
if opts.enemy_class:
# An explicit opponent is a 1-vs-1 fixture, matching classic Robocode's test bed.
setup["participants"] = 2

enemy_jar = None

if opts.conformance_source:
class_dir, error = compile_conformance_robot(opts, opts.conformance_source)
Expand Down Expand Up @@ -1260,16 +1291,33 @@ def run_conformance(opts):
return 2
jar, version = packaged, "1.0"

if opts.enemy_class:
# The installed classic robots are an external, read-only fixture (C-007). Package
# only the requested opponent into a temporary jar so both engines receive the same
# class and descriptor without modifying the installation.
enemy_classes = Path(opts.robocode_home) / "robots"
packaged, error = package_test_robot_jar(
enemy_classes, opts.enemy_class, WORK_DIR / "conformance-enemy")
if error:
print(json.dumps({"ok": False, "fatal": error}))
return 2
enemy_jar = packaged

if opts.engine == "rc":
result = run_rc_battle(jar, classname, version, opts, setup)
result = run_rc_battle(jar, classname, version, opts, setup,
enemy_jar, opts.enemy_class)
consoles = result.pop("consoles", None)
if consoles is None:
consoles = read_worker_consoles(WORK_DIR / "rc-result.json")
result["consoles"] = consoles
else:
result = run_tr_battle(jar, classname, version, opts, setup)
result = run_tr_battle(jar, classname, version, opts, setup,
enemy_jar_path=enemy_jar, enemy_class=opts.enemy_class)
# The bridge side's console output is whatever the bot processes wrote.
result["consoles"] = collect_bot_consoles()
staging_dirs = [WORK_DIR / "tr-bots"]
if opts.enemy_class:
staging_dirs.append(WORK_DIR / "tr-bots-enemy")
result["consoles"] = collect_bot_consoles(staging_dirs)
result.pop("log_text", None)
result["setup"] = setup
print(json.dumps(result))
Expand All @@ -1284,18 +1332,18 @@ def read_worker_consoles(out_file: Path):
return []


def collect_bot_consoles():
def collect_bot_consoles(staging_dirs=None):
"""Each staged bot instance writes its own stdout/stderr; return them per instance."""
consoles = []
staging = WORK_DIR / "tr-bots"
if not staging.exists():
return consoles
for d in sorted(staging.iterdir()):
if not d.is_dir() or d.name == "lib":
for staging in staging_dirs or [WORK_DIR / "tr-bots"]:
if not staging.exists():
continue
text = read_capped(d / "stdout.log") + read_capped(d / "stderr.log")
if text.strip():
consoles.append(text)
for d in sorted(staging.iterdir()):
if not d.is_dir() or d.name == "lib":
continue
text = read_capped(d / "stdout.log") + read_capped(d / "stderr.log")
if text.strip():
consoles.append(text)
return consoles


Expand Down
Loading
Loading