Skip to content

Commit e554021

Browse files
committed
RoboCode update
1 parent 1719bc5 commit e554021

4 files changed

Lines changed: 144 additions & 84 deletions

File tree

codeclash/games/robocode/robocode.py

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1+
import random
12
import re
3+
import subprocess
24
import time
5+
from concurrent.futures import ThreadPoolExecutor, as_completed
36
from pathlib import Path
47

8+
from tqdm.auto import tqdm
9+
510
from codeclash.agents.player import Player
611
from codeclash.games.game import CodeGame, RoundStats
7-
from codeclash.utils.environment import assert_zero_exit_code, create_file_in_container
12+
from codeclash.utils.environment import create_file_in_container
813

9-
RC_LOG = "scoreboard.txt"
1014
RC_FILE = Path("MyTank.java")
15+
SIMS_PER_RUN = 10
1116

1217

1318
class RoboCodeGame(CodeGame):
1419
name: str = "RoboCode"
15-
description: str = """Robocode (Tank Royale) is a programming game where your code is the tank: each turn your bot sends intents—speed plus body/gun/radar turn rates and firepower—based on the game state it perceives via radar.
16-
Your program decides how to move, aim, and fire in a deterministic, turn-based arena to outlast other bots."""
20+
description: str = f"""Robocode (Tank Royale) is a programming game where your code is the tank: each turn your bot sends intents—speed plus body/gun/radar turn rates and firepower—based on the game state it perceives via radar.
21+
Your program decides how to move, aim, and fire in a deterministic, turn-based arena to outlast other bots.
22+
Your bot logic must be written in Java and located in the `robots/custom/` directory.
23+
Keep the main bot class named `{str(RC_FILE)}`, but you can include additional Java files if you'd like."""
1724
default_args: dict = {
1825
"nodisplay": True,
1926
"nosound": True,
@@ -33,7 +40,7 @@ def __init__(self, config, **kwargs):
3340
def _get_battle_config(self) -> str:
3441
default_battle_config = {
3542
"battle": {
36-
"numRounds": self.game_config.get("sims_per_round", 100),
43+
"numRounds": SIMS_PER_RUN,
3744
"gunCoolingRate": 0.1,
3845
"rules": {"inactivityTime": 450, "hideEnemyNames": True},
3946
},
@@ -63,6 +70,24 @@ def dict_to_lines(d, prefix=""):
6370
dict_to_lines(default_battle_config)
6471
return "\n".join(battle_lines)
6572

73+
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str) -> str:
74+
rc_results = self.log_env / f"results_{idx}.txt"
75+
rc_record = self.log_env / f"record_{idx}.xml"
76+
cmd = f"{cmd} -results {rc_results}"
77+
if random.random() < self.game_config.get("record_ratio", 1):
78+
# Only record a fraction of simulations to save space
79+
cmd = f"{cmd} -recordXML {rc_record}"
80+
try:
81+
output = self.environment.execute(cmd, timeout=120)
82+
except subprocess.TimeoutExpired:
83+
self.logger.warning(f"RoboCode simulation {idx} timed out: {cmd}")
84+
return ""
85+
if output["returncode"] != 0:
86+
self.logger.warning(
87+
f"RoboCode simulation {idx} failed with exit code {output['returncode']}:\n{output['output']}"
88+
)
89+
return output["output"]
90+
6691
def execute_round(self, agents: list[Player]):
6792
for agent in agents:
6893
# Copy the agent codebase into the game codebase and compile it
@@ -85,30 +110,38 @@ def execute_round(self, agents: list[Player]):
85110
create_file_in_container(self.environment, content=battle_content, dest_path=f"battles/{battle_file}")
86111

87112
# Run battle with results output to file
88-
cmd = f"{self.run_cmd_round} -battle {battle_file} -results {self.log_env / RC_LOG}"
89-
self.logger.info(f"Running game: {cmd}")
90-
assert_zero_exit_code(self.environment.execute(cmd))
113+
cmd = f"{self.run_cmd_round} -battle {battle_file}"
114+
with ThreadPoolExecutor(5) as executor:
115+
# Submit all simulations to the thread pool
116+
futures = [
117+
executor.submit(self._run_single_simulation, agents, idx, cmd)
118+
for idx in range(self.game_config.get("sims_per_round", 100) // SIMS_PER_RUN)
119+
]
120+
121+
# Collect results as they complete
122+
for future in tqdm(as_completed(futures), total=len(futures)):
123+
future.result()
91124

92125
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
93-
with open(self.log_round(round_num) / RC_LOG) as f:
94-
result_output = f.read()
95-
print(result_output)
96-
lines = result_output.strip().split("\n")
97-
98-
scores = {}
99-
for line in lines:
100-
line = line.strip()
101-
if not re.match(r"^\d", line):
102-
continue
103-
match = re.search(r"(\d+)\S+\:\s(\S+)\s+(\d+)", line)
104-
if match:
105-
player = match.group(2).rsplit(".", 1)[0]
106-
score = int(match.group(3))
107-
scores[player] = score
108-
if int(match.group(1)) == 1:
109-
winner = player
110-
111-
stats.winner = winner
126+
for idx in range(self.game_config.get("sims_per_round", 100) // SIMS_PER_RUN):
127+
with open(self.log_round(round_num) / f"results_{idx}.txt") as f:
128+
result_output = f.read()
129+
lines = result_output.strip().split("\n")
130+
131+
scores = {}
132+
for line in lines:
133+
line = line.strip()
134+
if not re.match(r"^\d", line):
135+
continue
136+
match = re.search(r"(\d+)\S+\:\s(\S+)\s+(\d+)", line)
137+
if match:
138+
player = match.group(2).rsplit(".", 1)[0]
139+
score = int(match.group(3))
140+
if player not in scores:
141+
scores[player] = 0
142+
scores[player] += score
143+
144+
stats.winner = max(scores, key=scores.get)
112145
stats.scores = scores
113146
for player, score in scores.items():
114147
stats.player_stats[player].score = score

configs/scripts/generate_confs.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_name(p):
6767
return p["model_name"].split("/")[-1]
6868

6969

70-
def main(models, rounds, simulations, output: Path):
70+
def main(models, arenas, rounds: int, simulations: int, record_ratio: float, output: Path):
7171
# Get all unique pairs of models
7272
models = yaml.safe_load(open(models))
7373
output.mkdir(parents=True, exist_ok=True)
@@ -77,7 +77,12 @@ def main(models, rounds, simulations, output: Path):
7777
pairs.append((models[i], models[j]))
7878

7979
tracking_dict = {}
80-
for arena in ARENAS:
80+
arenas_list = ARENAS if arenas == "all" else [a for a in ARENAS if a.name in arenas.split(",")]
81+
if not arenas_list:
82+
print(f"No valid arenas found from {arenas}. Choose from {[a.name for a in ARENAS]}.")
83+
return # Stop execution if no valid arenas are found
84+
for arena in arenas_list:
85+
print(f"Generating {len(pairs)} configs for arena: {arena.name}")
8186
tracking_dict[arena.name] = {}
8287
for pair in pairs:
8388
config = {
@@ -99,6 +104,8 @@ def main(models, rounds, simulations, output: Path):
99104
],
100105
"prompts": {"game_description": prompt_game_desc(arena, rounds)},
101106
}
107+
if arena == RoboCodeGame:
108+
config["game"]["record_ratio"] = record_ratio
102109
pair_names = "__".join(sorted([get_name(pair[0]), get_name(pair[1])]))
103110
config_name = f"{arena.name}__{pair_names}__r{rounds}__s{simulations}.yaml"
104111
with open(output / config_name, "w") as f:
@@ -129,11 +136,17 @@ def main(models, rounds, simulations, output: Path):
129136
tracking_dict[arena.name][tracking_key] = {}
130137
tracking_dict[arena.name][tracking_key][pvp] = 0
131138

132-
with open("configs/scripts/main_tracker.json", "w") as f:
139+
tracking_path = "configs/scripts/main_tracker.json"
140+
if Path(tracking_path).exists():
141+
with open(tracking_path) as f:
142+
tracking_dict_current = json.load(f)
143+
tracking_dict.update(tracking_dict_current)
144+
145+
with open(tracking_path, "w") as f:
133146
json.dump(tracking_dict, f, indent=2)
134-
print("Wrote tracking file to 'configs/scripts/main_tracker.json'.")
147+
print(f"Wrote tracking file to '{tracking_path}'.")
135148

136-
print(f"Generated {len(pairs) * len(ARENAS)} configuration files in '{output}'.")
149+
print(f"Generated {len(pairs) * len(arenas)} configuration files in '{output}'.")
137150
print(f"- # Models: {len(models)}")
138151
print(f"- # Arenas: {len(ARENAS)}")
139152
print(f"- r (rounds) {rounds}")
@@ -155,6 +168,13 @@ def main(models, rounds, simulations, output: Path):
155168
default="configs/scripts/models.yaml",
156169
help="Path to model configurations.",
157170
)
171+
parser.add_argument(
172+
"-a",
173+
"--arenas",
174+
type=str,
175+
default="all",
176+
help="Comma separated list of arenas to generate configs for (default: all).",
177+
)
158178
parser.add_argument(
159179
"-r",
160180
"--rounds",
@@ -169,6 +189,12 @@ def main(models, rounds, simulations, output: Path):
169189
default=1000,
170190
help="Number of simulations to run per round (default: 1000).",
171191
)
192+
parser.add_argument(
193+
"--record_ratio",
194+
type=float,
195+
default=1,
196+
help="Fraction of simulations to record (default: 1 = all).",
197+
)
172198
parser.add_argument(
173199
"-o",
174200
"--output",

configs/scripts/main_tracker.json

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,44 @@
11
{
2+
"RoboCode": {
3+
"r15.s1000.p2": {
4+
"claude-sonnet-4-20250514.claude-sonnet-4-5-20250929": "0 (0 rounds)",
5+
"claude-sonnet-4-20250514.grok-code-fast-1": "0 (0 rounds)",
6+
"claude-sonnet-4-20250514.gemini-2.5-pro": "0 (0 rounds)",
7+
"claude-sonnet-4-20250514.glm-4-5": "0 (0 rounds)",
8+
"claude-sonnet-4-20250514.gpt-5": "0 (0 rounds)",
9+
"claude-sonnet-4-20250514.gpt-5-mini": "0 (0 rounds)",
10+
"claude-sonnet-4-20250514.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
11+
"claude-sonnet-4-20250514.o3": "0 (0 rounds)",
12+
"claude-sonnet-4-5-20250929.grok-code-fast-1": "0 (0 rounds)",
13+
"claude-sonnet-4-5-20250929.gemini-2.5-pro": "0 (0 rounds)",
14+
"claude-sonnet-4-5-20250929.glm-4-5": "0 (0 rounds)",
15+
"claude-sonnet-4-5-20250929.gpt-5": "0 (0 rounds)",
16+
"claude-sonnet-4-5-20250929.gpt-5-mini": "0 (0 rounds)",
17+
"claude-sonnet-4-5-20250929.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
18+
"claude-sonnet-4-5-20250929.o3": "0 (0 rounds)",
19+
"gemini-2.5-pro.grok-code-fast-1": "0 (0 rounds)",
20+
"glm-4-5.grok-code-fast-1": "0 (0 rounds)",
21+
"gpt-5.grok-code-fast-1": "0 (0 rounds)",
22+
"gpt-5-mini.grok-code-fast-1": "0 (0 rounds)",
23+
"grok-code-fast-1.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
24+
"grok-code-fast-1.o3": "0 (0 rounds)",
25+
"gemini-2.5-pro.glm-4-5": "0 (0 rounds)",
26+
"gemini-2.5-pro.gpt-5": "0 (0 rounds)",
27+
"gemini-2.5-pro.gpt-5-mini": "0 (0 rounds)",
28+
"gemini-2.5-pro.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
29+
"gemini-2.5-pro.o3": "0 (0 rounds)",
30+
"glm-4-5.gpt-5": "0 (0 rounds)",
31+
"glm-4-5.gpt-5-mini": "0 (0 rounds)",
32+
"glm-4-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
33+
"glm-4-5.o3": "0 (0 rounds)",
34+
"gpt-5.gpt-5-mini": "0 (0 rounds)",
35+
"gpt-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
36+
"gpt-5.o3": "0 (0 rounds)",
37+
"gpt-5-mini.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
38+
"gpt-5-mini.o3": "0 (0 rounds)",
39+
"o3.qwen3-coder-plus-2025-09-23": "0 (0 rounds)"
40+
}
41+
},
242
"BattleCode": {
343
"r15.s1000.p2": {
444
"claude-sonnet-4-20250514.claude-sonnet-4-5-20250929": "0 (0 rounds)",
@@ -159,51 +199,11 @@
159199
"o3.qwen3-coder-plus-2025-09-23": "0 (0 rounds)"
160200
}
161201
},
162-
"RoboCode": {
163-
"r15.s1000.p2": {
164-
"claude-sonnet-4-20250514.claude-sonnet-4-5-20250929": "0 (0 rounds)",
165-
"claude-sonnet-4-20250514.grok-code-fast-1": "0 (0 rounds)",
166-
"claude-sonnet-4-20250514.gemini-2.5-pro": "0 (0 rounds)",
167-
"claude-sonnet-4-20250514.glm-4-5": "0 (0 rounds)",
168-
"claude-sonnet-4-20250514.gpt-5": "0 (0 rounds)",
169-
"claude-sonnet-4-20250514.gpt-5-mini": "0 (0 rounds)",
170-
"claude-sonnet-4-20250514.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
171-
"claude-sonnet-4-20250514.o3": "0 (0 rounds)",
172-
"claude-sonnet-4-5-20250929.grok-code-fast-1": "0 (0 rounds)",
173-
"claude-sonnet-4-5-20250929.gemini-2.5-pro": "0 (0 rounds)",
174-
"claude-sonnet-4-5-20250929.glm-4-5": "0 (0 rounds)",
175-
"claude-sonnet-4-5-20250929.gpt-5": "0 (0 rounds)",
176-
"claude-sonnet-4-5-20250929.gpt-5-mini": "0 (0 rounds)",
177-
"claude-sonnet-4-5-20250929.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
178-
"claude-sonnet-4-5-20250929.o3": "0 (0 rounds)",
179-
"gemini-2.5-pro.grok-code-fast-1": "0 (0 rounds)",
180-
"glm-4-5.grok-code-fast-1": "0 (0 rounds)",
181-
"gpt-5.grok-code-fast-1": "0 (0 rounds)",
182-
"gpt-5-mini.grok-code-fast-1": "0 (0 rounds)",
183-
"grok-code-fast-1.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
184-
"grok-code-fast-1.o3": "0 (0 rounds)",
185-
"gemini-2.5-pro.glm-4-5": "0 (0 rounds)",
186-
"gemini-2.5-pro.gpt-5": "0 (0 rounds)",
187-
"gemini-2.5-pro.gpt-5-mini": "0 (0 rounds)",
188-
"gemini-2.5-pro.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
189-
"gemini-2.5-pro.o3": "0 (0 rounds)",
190-
"glm-4-5.gpt-5": "0 (0 rounds)",
191-
"glm-4-5.gpt-5-mini": "0 (0 rounds)",
192-
"glm-4-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
193-
"glm-4-5.o3": "0 (0 rounds)",
194-
"gpt-5.gpt-5-mini": "0 (0 rounds)",
195-
"gpt-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
196-
"gpt-5.o3": "0 (0 rounds)",
197-
"gpt-5-mini.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
198-
"gpt-5-mini.o3": "0 (0 rounds)",
199-
"o3.qwen3-coder-plus-2025-09-23": "0 (0 rounds)"
200-
}
201-
},
202202
"RobotRumble": {
203203
"r15.s1000.p2": {
204204
"claude-sonnet-4-20250514.claude-sonnet-4-5-20250929": "0 (0 rounds)",
205205
"claude-sonnet-4-20250514.grok-code-fast-1": "0 (0 rounds)",
206-
"claude-sonnet-4-20250514.gemini-2.5-pro": "0 (0 rounds)",
206+
"claude-sonnet-4-20250514.gemini-2.5-pro": "1 (10 rounds)",
207207
"claude-sonnet-4-20250514.glm-4-5": "0 (0 rounds)",
208208
"claude-sonnet-4-20250514.gpt-5": "0 (0 rounds)",
209209
"claude-sonnet-4-20250514.gpt-5-mini": "0 (0 rounds)",
@@ -218,24 +218,24 @@
218218
"claude-sonnet-4-5-20250929.o3": "0 (0 rounds)",
219219
"gemini-2.5-pro.grok-code-fast-1": "7 (94 rounds)",
220220
"glm-4-5.grok-code-fast-1": "0 (0 rounds)",
221-
"gpt-5.grok-code-fast-1": "0 (0 rounds)",
221+
"gpt-5.grok-code-fast-1": "10 (160 rounds)",
222222
"gpt-5-mini.grok-code-fast-1": "6 (81 rounds)",
223-
"grok-code-fast-1.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
224-
"grok-code-fast-1.o3": "0 (0 rounds)",
223+
"grok-code-fast-1.qwen3-coder-plus-2025-09-23": "10 (143 rounds)",
224+
"grok-code-fast-1.o3": "10 (143 rounds)",
225225
"gemini-2.5-pro.glm-4-5": "0 (0 rounds)",
226-
"gemini-2.5-pro.gpt-5": "0 (0 rounds)",
227-
"gemini-2.5-pro.gpt-5-mini": "0 (0 rounds)",
226+
"gemini-2.5-pro.gpt-5": "10 (153 rounds)",
227+
"gemini-2.5-pro.gpt-5-mini": "10 (160 rounds)",
228228
"gemini-2.5-pro.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
229-
"gemini-2.5-pro.o3": "0 (0 rounds)",
229+
"gemini-2.5-pro.o3": "10 (128 rounds)",
230230
"glm-4-5.gpt-5": "0 (0 rounds)",
231231
"glm-4-5.gpt-5-mini": "0 (0 rounds)",
232232
"glm-4-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
233233
"glm-4-5.o3": "0 (0 rounds)",
234234
"gpt-5.gpt-5-mini": "0 (0 rounds)",
235-
"gpt-5.qwen3-coder-plus-2025-09-23": "0 (0 rounds)",
236-
"gpt-5.o3": "0 (0 rounds)",
237-
"gpt-5-mini.qwen3-coder-plus-2025-09-23": "8 (108 rounds)",
238-
"gpt-5-mini.o3": "0 (0 rounds)",
235+
"gpt-5.qwen3-coder-plus-2025-09-23": "1 (8 rounds)",
236+
"gpt-5.o3": "10 (116 rounds)",
237+
"gpt-5-mini.qwen3-coder-plus-2025-09-23": "12 (172 rounds)",
238+
"gpt-5-mini.o3": "10 (151 rounds)",
239239
"o3.qwen3-coder-plus-2025-09-23": "7 (97 rounds)"
240240
}
241241
}

configs/test/players/2/robocode.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ tournament:
22
rounds: 3
33
game:
44
name: RoboCode
5-
sims_per_round: 1000
5+
sims_per_round: 250
6+
record_ratio: 0.2
67
battle:
78
battle:
89
gunCoolingRate: 0.1

0 commit comments

Comments
 (0)