Skip to content

Commit 09ea109

Browse files
committed
Add handling for submission validation
1 parent 5b33ad8 commit 09ea109

17 files changed

Lines changed: 235 additions & 148 deletions

File tree

codeclash/games/battlecode/battlecode.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import random
22
import re
33
from pathlib import Path
4-
from typing import Any
54

5+
from codeclash.agents.player import Player
66
from codeclash.constants import DIR_WORK, RESULT_TIE
77
from codeclash.games.game import CodeGame, RoundStats
88

@@ -23,7 +23,19 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2323
else:
2424
self.run_cmd_round += f" --{arg} {val}"
2525

26-
def get_results(self, agents: list[Any], round_num: int) -> RoundStats:
26+
def execute_round(self, agents: list[Player]):
27+
for agent in agents:
28+
src, dest = f"/{agent.name}/src/mysubmission/", str(DIR_WORK / "src" / agent.name)
29+
self.environment.execute(f"cp -r {src} {dest}")
30+
random.shuffle(agents) # Start position matters in BattleCode! Shuffle to be fair.
31+
args = [f"--p{idx + 1}-dir src --p{idx + 1} {agent.name}" for idx, agent in enumerate(agents)]
32+
cmd = f"{self.run_cmd_round} {' '.join(args)}"
33+
self.logger.info(f"Running game: {cmd}")
34+
35+
response = self.environment.execute(cmd + f" > {self.log_env / BC_LOG}")
36+
assert response["returncode"] == 0, response
37+
38+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
2739
winners = []
2840
with open(self.log_round(round_num) / BC_LOG) as f:
2941
lines = f.read().strip().split("\n")
@@ -39,19 +51,12 @@ def get_results(self, agents: list[Any], round_num: int) -> RoundStats:
3951
winners.append(winner)
4052
else:
4153
winners.append(RESULT_TIE)
42-
return RoundStats(
43-
winner=max(set(winners), key=winners.count),
44-
scores={agent.name: winners.count(agent.name) for agent in agents},
45-
)
4654

47-
def execute_round(self, agents: list[Any]):
48-
for agent in agents:
49-
src, dest = f"/{agent.name}/src/mysubmission/", str(DIR_WORK / "src" / agent.name)
50-
self.environment.execute(f"cp -r {src} {dest}")
51-
random.shuffle(agents) # Start position matters in BattleCode! Shuffle to be fair.
52-
args = [f"--p{idx + 1}-dir src --p{idx + 1} {agent.name}" for idx, agent in enumerate(agents)]
53-
cmd = f"{self.run_cmd_round} {' '.join(args)}"
54-
self.logger.info(f"Running game: {cmd}")
55+
stats.winner = max(set(winners), key=winners.count)
56+
stats.scores = {agent.name: winners.count(agent.name) for agent in agents}
57+
for player, score in stats.scores.items():
58+
stats.player_stats[player].score = score
5559

56-
response = self.environment.execute(cmd + f" > {self.log_env / BC_LOG}")
57-
assert response["returncode"] == 0, response
60+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
61+
# TODO: implement more checks
62+
return True, None

codeclash/games/battlesnake/battlesnake.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,14 @@ def _wait_for_ports(self, ports: list[int], timeout: float = 3.0) -> None:
3838

3939
time.sleep(0.1)
4040

41-
def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
42-
scores = {}
43-
for idx in range(self.game_config["sims_per_round"]):
44-
with open(self.log_round(round_num) / f"sim_{idx}.jsonl") as f:
45-
lines = f.read().strip().split("\n")
46-
results = json.loads(lines[-1]) # Get the last line which contains the game result
47-
winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
48-
scores[winner] = scores.get(winner, 0) + 1
49-
50-
winner = max(scores, key=scores.get)
51-
winner = RESULT_TIE if list(scores.values()).count(scores[winner]) > 1 else winner
52-
return RoundStats(winner=winner, scores=scores)
41+
def _run_single_simulation(self, cmd: str, idx: int) -> tuple[str, str]:
42+
"""Run a single battlesnake simulation and return log and result outputs."""
43+
assert_zero_exit_code(
44+
self.environment.execute(
45+
cmd + f" -o {self.log_env / f'sim_{idx}.jsonl'}",
46+
cwd=f"{self.environment.config.cwd}/game",
47+
)
48+
)
5349

5450
def execute_round(self, agents: list[Player]):
5551
self.logger.debug("Starting game servers")
@@ -86,11 +82,23 @@ def execute_round(self, agents: list[Player]):
8682
# Kill all python servers when done
8783
self.environment.execute("pkill -f 'python main.py' || true")
8884

89-
def _run_single_simulation(self, cmd: str, idx: int) -> tuple[str, str]:
90-
"""Run a single battlesnake simulation and return log and result outputs."""
91-
assert_zero_exit_code(
92-
self.environment.execute(
93-
cmd + f" -o {self.log_env / f'sim_{idx}.jsonl'}",
94-
cwd=f"{self.environment.config.cwd}/game",
95-
)
96-
)
85+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
86+
scores = {}
87+
for idx in range(self.game_config["sims_per_round"]):
88+
with open(self.log_round(round_num) / f"sim_{idx}.jsonl") as f:
89+
lines = f.read().strip().split("\n")
90+
results = json.loads(lines[-1]) # Get the last line which contains the game result
91+
winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
92+
scores[winner] = scores.get(winner, 0) + 1
93+
94+
winner = max(scores, key=scores.get)
95+
winner = RESULT_TIE if list(scores.values()).count(scores[winner]) > 1 else winner
96+
stats.winner = winner
97+
stats.scores = scores
98+
for player, score in scores.items():
99+
if player != RESULT_TIE:
100+
stats.player_stats[player].score = score
101+
102+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
103+
# TODO: implement more checks
104+
return True, None

codeclash/games/corewar/corewar.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2121
else:
2222
self.run_cmd_round += f" -{arg} {val}"
2323

24-
def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
24+
def execute_round(self, agents: list[Player]):
25+
args = [f"/{agent.name}/warriors/warrior.red" for agent in agents]
26+
cmd = (
27+
f"{self.run_cmd_round} {shlex.join(args)} "
28+
f"-r {self.game_config['sims_per_round']} "
29+
f"> {self.log_env / COREWAR_LOG};"
30+
)
31+
self.logger.info(f"Running game: {cmd}")
32+
response = self.environment.execute(cmd)
33+
assert response["returncode"] == 0, response
34+
35+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
2536
with open(self.log_round(round_num) / COREWAR_LOG) as f:
2637
result_output = f.read()
2738
self.logger.debug(f"Determining winner from result output: {result_output}")
@@ -44,22 +55,16 @@ def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
4455
if scores:
4556
if len(scores) != len(agents):
4657
self.logger.error(f"Have {len(scores)} scores but {len(agents)} agents")
47-
return RoundStats(
48-
winner=agents[scores.index(max(scores))].name,
49-
scores={agent.name: score for agent, score in zip(agents, scores)},
50-
details={"stdout": "\n".join(relevant_lines)},
51-
)
58+
stats.winner = agents[scores.index(max(scores))].name
59+
stats.scores = {agent.name: score for agent, score in zip(agents, scores)}
5260
else:
5361
self.logger.debug("No scores found, returning unknown")
54-
return RoundStats(winner="unknown", scores={agent.name: 0 for agent in agents})
62+
stats.winner = "unknown"
63+
stats.scores = {agent.name: 0 for agent in agents}
5564

56-
def execute_round(self, agents: list[Player]):
57-
args = [f"/{agent.name}/warriors/warrior.red" for agent in agents]
58-
cmd = (
59-
f"{self.run_cmd_round} {shlex.join(args)} "
60-
f"-r {self.game_config['sims_per_round']} "
61-
f"> {self.log_env / COREWAR_LOG};"
62-
)
63-
self.logger.info(f"Running game: {cmd}")
64-
response = self.environment.execute(cmd)
65-
assert response["returncode"] == 0, response
65+
for player, score in stats.scores.items():
66+
stats.player_stats[player].score = score
67+
68+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
69+
# TODO: implement more checks
70+
return True, None

codeclash/games/dummy/dummy_game.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
class DummyGame(CodeGame):
1111
name: str = "DummyGame"
1212

13-
def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
13+
def execute_round(self, agents: list[Player]) -> None:
14+
args = [f"/{agent.name}/main.py" for agent in agents]
15+
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / DUMMY_LOG};"
16+
self.logger.info(f"Running game: {cmd}")
17+
assert_zero_exit_code(self.environment.execute(cmd))
18+
19+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
1420
with open(self.log_round(round_num) / DUMMY_LOG) as f:
1521
round_log = f.read()
1622
lines = round_log.split("FINAL_RESULTS")[-1].splitlines()
@@ -23,14 +29,11 @@ def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
2329
rounds_won = int(match.group(2))
2430
scores[agents[int(bot_id) - 1].name] = rounds_won
2531

26-
return RoundStats(
27-
winner=max(scores, key=scores.get) if scores else "unknown",
28-
scores=scores,
29-
details={"dummy": True},
30-
)
32+
stats.winner = max(scores, key=scores.get) if scores else "unknown"
33+
stats.scores = scores
34+
for player, score in scores.items():
35+
stats.player_stats[player].score = score
3136

32-
def execute_round(self, agents: list[Player]) -> None:
33-
args = [f"/{agent.name}/main.py" for agent in agents]
34-
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / DUMMY_LOG};"
35-
self.logger.info(f"Running game: {cmd}")
36-
assert_zero_exit_code(self.environment.execute(cmd))
37+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
38+
# TODO: implement more checks
39+
return True, None

codeclash/games/game.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,55 @@
66
from typing import Any
77

88
from minisweagent.environments.docker import DockerEnvironment
9-
from pydantic import BaseModel
109

1110
from codeclash.agents.player import Player
1211
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG
1312
from codeclash.utils.environment import assert_zero_exit_code, copy_between_containers, copy_from_container
1413
from codeclash.utils.log import get_logger
1514

1615

17-
class RoundStats(BaseModel):
16+
class PlayerStats:
17+
def __init__(self, name: str):
18+
self.name = name
19+
self.invalid_reason: str | None = None
20+
self.score: float | None = None
21+
self.valid_submit = False
22+
23+
def to_dict(self) -> dict[str, Any]:
24+
return {
25+
"name": self.name,
26+
"invalid_reason": self.invalid_reason,
27+
"score": self.score,
28+
"valid_submit": self.valid_submit,
29+
}
30+
31+
32+
class RoundStats:
1833
winner: str
19-
scores: dict[str, float] # Map of player to game metric (e.g. # of wins, assets accumulated)
20-
details: dict[str, Any] | None = None # Optional, for game-specific info
34+
round_num: int
35+
scores: dict[str, float]
36+
player_stats: dict[str, PlayerStats] | None = None
37+
38+
def __init__(self, round_num: int, agents: list[Player]):
39+
self.winner = None
40+
self.round_num = round_num
41+
# Map of player to game metric (e.g. # of wins, assets accumulated)
42+
self.scores: dict[str, float] = {}
43+
self.player_stats: dict[str, PlayerStats] = {agent.name: PlayerStats(name=agent.name) for agent in agents}
2144

2245
def __str__(self) -> str:
2346
return "\n".join([f"- Winner: {self.winner}", f"- Scores: {self.scores}"])
2447

48+
def to_dict(self) -> dict[str, Any]:
49+
result = {
50+
"round_num": self.round_num,
51+
"winner": self.winner,
52+
"scores": self.scores,
53+
}
54+
if self.player_stats:
55+
result["player_stats"] = {name: stats.to_dict() for name, stats in self.player_stats.items()}
56+
return result
57+
2558

2659
class CodeGame(ABC):
2760
name: str
@@ -177,20 +210,32 @@ def run_round(self, agents: list[Player], round_num: int) -> RoundStats:
177210
Returns the log output, result output, and winner name. All bookkeeping should be
178211
handled by the tournament class.
179212
"""
180-
self._pre_round_setup(agents)
181-
self.execute_round(agents)
182-
self.copy_logs_from_env(round_num)
183-
return self.get_results(agents, round_num)
213+
stats = RoundStats(round_num, agents)
214+
validated = []
215+
for agent in agents:
216+
is_valid, error = self.validate_code(agent)
217+
if not is_valid:
218+
self.logger.warning(f"Agent {agent.name} failed verification: {error}")
219+
stats.player_stats[agent.name].invalid_reason = error
220+
continue
221+
stats.player_stats[agent.name].valid_submit = True
222+
validated.append(agent)
223+
224+
run_game = len(validated) > 1
225+
if run_game:
226+
self._pre_round_setup(validated)
227+
self.execute_round(validated)
228+
self.copy_logs_from_env(round_num)
229+
self.get_results(validated, round_num, stats)
230+
return stats
184231

185232
@abstractmethod
186-
def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
233+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
187234
"""Determine the winner of the game based on the result output.
235+
Modifies the stats object in place.
188236
189237
Args:
190238
agents: List of agents participating in the round
191-
192-
Returns:
193-
RoundStats object
194239
"""
195240
pass
196241

@@ -201,3 +246,16 @@ def execute_round(self, agents: list[Player]):
201246
includes the pre-round setup, post-round setup, and winner determination.
202247
"""
203248
pass
249+
250+
@abstractmethod
251+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
252+
"""Verify that the given agent can be run by the game.
253+
254+
Args:
255+
agent: The agent to verify
256+
257+
Returns:
258+
Boolean indicating whether the agent passed verification
259+
Optional string indicating reason for failure
260+
"""
261+
pass

codeclash/games/huskybench/huskybench.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2525
else:
2626
self.run_cmd_round += f" --{arg} {val}"
2727

28-
def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
28+
def execute_round(self, agents: list[Player]):
29+
try:
30+
cmd = f"{self.run_cmd_round} > {self.log_env / HB_LOG_ENGINE} &"
31+
self.logger.debug(f"Starting game engine with command: {cmd}")
32+
self.environment.execute(cmd)
33+
for agent in agents:
34+
cmd = f"python client/main.py --port 8000 > {self.log_env / f'{agent.name}.log'} &"
35+
self.logger.debug(f"Adding agent with command: {cmd}")
36+
self.environment.execute(cmd, cwd=f"/{agent.name}")
37+
finally:
38+
# Kill all python servers when done
39+
self.environment.execute("pkill -f 'python client/main.py' || true")
40+
self.environment.execute("pkill -f 'python engine/main.py' || true")
41+
42+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
2943
map_id_to_agent = {}
3044
for agent in agents:
3145
with open(self.log_round(round_num) / f"{agent.name}.log") as f:
@@ -41,19 +55,13 @@ def get_results(self, agents: list[Player], round_num: int) -> RoundStats:
4155
]
4256
map_id_to_score = {k: v for k, v in score_updates[-self.num_players :]}
4357
self.logger.info("Final Scores: " + str(map_id_to_score))
44-
agent_to_score = {map_id_to_agent[agent_id]: score for agent_id, score in map_id_to_score.items()}
45-
return RoundStats(winner=max(agent_to_score, key=agent_to_score.get), scores=agent_to_score)
58+
scores = {map_id_to_agent[agent_id]: score for agent_id, score in map_id_to_score.items()}
4659

47-
def execute_round(self, agents: list[Player]):
48-
try:
49-
cmd = f"{self.run_cmd_round} > {self.log_env / HB_LOG_ENGINE} &"
50-
self.logger.debug(f"Starting game engine with command: {cmd}")
51-
self.environment.execute(cmd)
52-
for agent in agents:
53-
cmd = f"python client/main.py --port 8000 > {self.log_env / f'{agent.name}.log'} &"
54-
self.logger.debug(f"Adding agent with command: {cmd}")
55-
self.environment.execute(cmd, cwd=f"/{agent.name}")
56-
finally:
57-
# Kill all python servers when done
58-
self.environment.execute("pkill -f 'python client/main.py' || true")
59-
self.environment.execute("pkill -f 'python engine/main.py' || true")
60+
stats.winner = max(scores, key=scores.get)
61+
stats.scores = scores
62+
for player, score in scores.items():
63+
stats.player_stats[player].score = score
64+
65+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
66+
# TODO: implement more checks
67+
return True, None

0 commit comments

Comments
 (0)