Skip to content

Commit 3661779

Browse files
john-b-yangclaude
andcommitted
Fix ladder player names for RoboCode; bump make_robocode sims to 300
`ladder make` set the per-player name to the raw branch_init (e.g. `human/robocode/walls`), which the RoboCode arena turns into an on-disk dir, a Java package via `sed s/custom/<name>/`, and a robot-selector token. Slashes broke the sed (terminating the s/// command) and produced invalid robot names, so battles loaded 0 robots and get_results crashed on max() of an empty score dict. Sanitizing to `robocode_<bot>` still failed: RoboCode reserves the `robocode` package namespace for its engine, so its robot classloader refuses any robot whose package starts with it (ClassNotFoundException at battle time). Add `_player_slug()` (shared by make + run) that strips the `human/` prefix and a leading path segment equal to the game name, yielding a bare `<bot>` package (e.g. `human/robocode/walls` -> `walls`). Non-RoboCode branches (`human/<author>/<bot>`, `human/<bot>`) are unaffected -- their leading segment never matches the game name, so slugs are identical to before. Bump make_robocode.yaml sims_per_round 100 -> 300 for lower Elo variance (~41s/pair solo, ~25s/pair throughput under -w 6; still far under the 5-min/pair budget) and refresh the cost comment with measured numbers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1e00795 commit 3661779

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

codeclash/cli/ladder.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
logger = get_logger("ladder")
1919

2020

21+
def _player_slug(branch_init: str, game_name: str) -> str:
22+
"""Turn a ``human/<...>/<bot>`` init branch into a bare, package-safe player name.
23+
24+
The name is used as an on-disk directory and — in the RoboCode arena — as a Java package name
25+
and robot-selector token (``robots/<name>/``, ``sed s/custom/<name>/``,
26+
``selectedRobots=<name>.MyTank``), so it must contain no slashes. RoboCode additionally reserves
27+
the ``robocode`` package namespace for its engine and its robot classloader refuses to load any
28+
robot whose package starts with it, so a leading path segment equal to the game name (RoboCode
29+
branches are ``human/robocode/<bot>``) is dropped rather than folded into a ``robocode_<bot>``
30+
token that would raise ClassNotFoundException at battle time. Non-RoboCode branches
31+
(``human/<author>/<bot>``, ``human/<bot>``) are unaffected: their leading segment never matches
32+
the game name, so the result is the same ``<author>_<bot>`` slug as before.
33+
"""
34+
parts = [p for p in branch_init.split("/") if p]
35+
if parts and parts[0] == "human":
36+
parts = parts[1:]
37+
if parts and parts[0].lower() == game_name.lower():
38+
parts = parts[1:]
39+
return "_".join(parts)
40+
41+
2142
def _resolve_ladder_rules(ladder_rules: dict, rounds: int) -> tuple[float, int]:
2243
"""Validate the optional ``ladder_rules`` block and return ``(min_round_win_fraction, win_last_k)``.
2344
@@ -83,15 +104,17 @@ def make(
83104
players = config["players"]
84105
num_players = len(players)
85106

107+
game_name = config["game"]["name"]
108+
86109
# Build one fully independent (deep-copied) config per pair up front so concurrent runs
87110
# never share or mutate the same player/config dicts.
88111
jobs: list[tuple[dict, Path]] = []
89112
for i in range(num_players):
90113
for j in range(i + 1, num_players):
91114
player1 = copy.deepcopy(players[i])
92-
player1["name"] = player1["branch_init"]
115+
player1["name"] = _player_slug(player1["branch_init"], game_name)
93116
player2 = copy.deepcopy(players[j])
94-
player2["name"] = player2["branch_init"]
117+
player2["name"] = _player_slug(player2["branch_init"], game_name)
95118
pvp_config = {**copy.deepcopy(config), "players": [player1, player2]}
96119
vs = f"PvpTournament.{player1['name']}_vs_{player2['name']}".replace("/", "_")
97120
output_dir = LOCAL_LOG_DIR / "ladder" / config["game"]["name"] / vs
@@ -157,7 +180,7 @@ def run(
157180

158181
for idx, opponent in enumerate(ladder):
159182
opponent_rank = len(ladder) - idx
160-
opponent["name"] = opponent["branch_init"].replace("human/", "").replace("/", "_")
183+
opponent["name"] = _player_slug(opponent["branch_init"], config["game"]["name"])
161184
if "branch_init" in player and idx > 0:
162185
# After first opponent, remove branch_init so that player continues from previous tournament's codebase
163186
del player["branch_init"]

configs/ablations/ladder/make_robocode.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
# Elo/Bradley-Terry -> see robocode.yaml. rounds:0 = dummy players play baseline battles (no code
44
# edits). N bots -> N*(N-1)/2 pairs; each pair plays sims_per_round rounds (batched 10 at a time,
55
# sim_concurrency parallel). Bots live on CodeClash-ai/RoboCode human/* branches; Docker required.
6-
# COST (measured ~3.4s for a 50-round pair headless): at sims_per_round=100, 116 bots -> 6670 pairs
7-
# ~= 13 core-hours -> ~2 h on a laptop (--workers 6), ~26 min on a 32-core box (-w 30).
6+
# COST (measured on an 8-core box: a pair is ~11.8s fixed overhead + ~9.7s per 100 sims, so at
7+
# sims_per_round=300 a single pair is ~41s solo and ~25s/pair of throughput under --workers 6).
8+
# 116 bots -> 6670 pairs -> ~46 core-hours -> ~2 days on an 8-core box (-w 6). Resumable: skips
9+
# already-logged pairs, so a killed run can be re-launched. Scale --workers with cores (~cores-2).
810
# Run: uv run codeclash ladder make configs/ablations/ladder/make_robocode.yaml --workers 6
911
tournament:
1012
rounds: 0
1113
game:
1214
name: RoboCode
13-
sims_per_round: 100
15+
sims_per_round: 300
1416
sim_concurrency: 5
1517
args:
1618
nodisplay: true

0 commit comments

Comments
 (0)