Skip to content

Commit 4a8e4a7

Browse files
authored
Make log copying more flexible (#52)
* Add Huskybench basic scaffolding * Remove RoundRecord; Add explicit copy logs method
1 parent 5721961 commit 4a8e4a7

17 files changed

Lines changed: 226 additions & 145 deletions

File tree

codeclash/agents/player.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(
2424
) -> None:
2525
self.config = config
2626
self.name = config["name"]
27-
self._player_unique_id = uuid.uuid4()
27+
self._player_unique_id = str(uuid.uuid4())
2828
"""Unique ID that doesn't clash even across multiple games. Used for git tags."""
2929
self.environment = environment
3030
self.game_context = game_context

codeclash/games/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from codeclash.games.corewar.corewar import CoreWarGame
66
from codeclash.games.dummy.dummy_game import DummyGame
77
from codeclash.games.game import CodeGame
8+
from codeclash.games.huskybench.huskybench import HuskyBenchGame
89
from codeclash.games.robocode.robocode import RoboCodeGame
910
from codeclash.games.robotrumble.robotrumble import RobotRumbleGame
1011

@@ -18,6 +19,7 @@ def get_game(config: dict, *, tournament_id: str, local_output_dir: Path) -> Cod
1819
BattleSnakeGame,
1920
CoreWarGame,
2021
DummyGame,
22+
HuskyBenchGame,
2123
RoboCodeGame,
2224
RobotRumbleGame,
2325
]

codeclash/games/battlecode/battlecode.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from tqdm.auto import tqdm
66

77
from codeclash.constants import DIR_WORK, RESULT_TIE
8-
from codeclash.games.game import CodeGame, RoundData, RoundStats
8+
from codeclash.games.game import CodeGame, RoundStats
9+
from codeclash.utils.environment import copy_from_container
910

1011

1112
class BattleCodeGame(CodeGame):
@@ -22,9 +23,18 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2223
else:
2324
self.run_cmd_round += f" --{arg} {val}"
2425

25-
def get_stats(self, result_outputs: list[str], agents: list[Any]) -> RoundStats:
26+
def copy_logs_from_env(self, round_num):
27+
super().copy_logs_from_env(round_num)
28+
copy_from_container(
29+
container=self.environment,
30+
src_path="/testbed/logs",
31+
dest_path=self.log_local / "rounds" / str(round_num),
32+
)
33+
34+
def get_stats(self, agents: list[Any]) -> RoundStats:
2635
winners = []
27-
for ro in result_outputs:
36+
for sim_file in [f"logs/sim_{idx}.log" for idx in range(self.game_config["sims_per_round"])]:
37+
ro = self.environment.execute(f"cat {sim_file}")["output"]
2838
lines = ro.strip().split("\n")
2939
# Get the third-to-last line which contains the winner info
3040
winner_line = lines[-3] if len(lines) >= 3 else ""
@@ -43,17 +53,15 @@ def get_stats(self, result_outputs: list[str], agents: list[Any]) -> RoundStats:
4353
scores={agent.name: winners.count(agent.name) for agent in agents},
4454
)
4555

46-
def execute_round(self, agents: list[Any]) -> RoundData:
56+
def execute_round(self, agents: list[Any]):
4757
for agent in agents:
4858
src, dest = f"/{agent.name}/src/mysubmission/", str(DIR_WORK / "src" / agent.name)
4959
self.environment.execute(f"cp -r {src} {dest}")
5060
args = [f"--p{idx + 1}-dir src --p{idx + 1} {agent.name}" for idx, agent in enumerate(agents)]
5161
cmd = f"{self.run_cmd_round} {' '.join(args)}"
5262
self.logger.info(f"Running game: {cmd}")
53-
outputs = []
54-
for _ in tqdm(range(self.game_config["sims_per_round"])):
55-
response = self.environment.execute(cmd)
63+
64+
self.environment.execute("rm -rf logs; mkdir logs")
65+
for idx in tqdm(range(self.game_config["sims_per_round"])):
66+
response = self.environment.execute(cmd + f" > logs/sim_{idx}.log")
5667
assert response["returncode"] == 0, response
57-
# For BattleCode, log_outputs and result_outputs are the same
58-
outputs.append(response["output"])
59-
return RoundData(logs=outputs, results=outputs)

codeclash/games/battlesnake/battlesnake.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import json
22
import time
3-
import uuid
43
from concurrent.futures import ThreadPoolExecutor, as_completed
54
from pathlib import Path
65

76
from tqdm.auto import tqdm
87

98
from codeclash.agents.player import Player
109
from codeclash.constants import RESULT_TIE
11-
from codeclash.games.game import CodeGame, RoundData, RoundStats
12-
from codeclash.utils.environment import assert_zero_exit_code
10+
from codeclash.games.game import CodeGame, RoundStats
11+
from codeclash.utils.environment import assert_zero_exit_code, copy_from_container
1312

1413

1514
class BattleSnakeGame(CodeGame):
@@ -39,9 +38,18 @@ def _wait_for_ports(self, ports: list[int], timeout: float = 3.0) -> None:
3938

4039
time.sleep(0.1)
4140

42-
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
41+
def copy_logs_from_env(self, round_num):
42+
super().copy_logs_from_env(round_num)
43+
copy_from_container(
44+
container=self.environment,
45+
src_path=f"{self.environment.config.cwd}/game/logs",
46+
dest_path=self.log_local / "rounds" / str(round_num),
47+
)
48+
49+
def get_stats(self, agents: list[Player]) -> RoundStats:
4350
scores = {}
44-
for ro in result_outputs:
51+
for idx in range(self.game_config["sims_per_round"]):
52+
ro = self.environment.execute(f"cat game/logs/sim_out_{idx}.json")["output"]
4553
lines = ro.strip().split("\n")
4654
results = json.loads(lines[-1]) if lines else {} # Get the last line which contains the game result
4755
winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
@@ -51,7 +59,7 @@ def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundSta
5159
winner = RESULT_TIE if list(scores.values()).count(scores[winner]) > 1 else winner
5260
return RoundStats(winner=winner, scores=scores)
5361

54-
def execute_round(self, agents: list[Player]) -> RoundData:
62+
def execute_round(self, agents: list[Player]):
5563
self.logger.debug("Starting game servers")
5664
cmd = []
5765
ports = []
@@ -68,46 +76,30 @@ def execute_round(self, agents: list[Player]) -> RoundData:
6876
self.logger.debug("All ports are ready")
6977

7078
try:
71-
log_outputs, result_outputs = [], []
7279
cmd = self.run_cmd_round + " " + " ".join(cmd)
7380
self.logger.info(f"Running game: {cmd}")
81+
self.environment.execute("rm -rf logs; mkdir logs", cwd=f"{self.environment.config.cwd}/game")
7482

7583
# Use ThreadPoolExecutor for parallel execution
7684
with ThreadPoolExecutor(20) as executor:
7785
# Submit all simulations to the thread pool
7886
futures = [
79-
executor.submit(self._run_single_simulation, cmd) for _ in range(self.game_config["sims_per_round"])
87+
executor.submit(self._run_single_simulation, cmd, idx)
88+
for idx in range(self.game_config["sims_per_round"])
8089
]
8190

8291
# Collect results as they complete
8392
for future in tqdm(as_completed(futures), total=len(futures)):
84-
log_output, result_output = future.result()
85-
log_outputs.append(log_output)
86-
result_outputs.append(result_output)
87-
88-
return RoundData(logs=log_outputs, results=result_outputs)
93+
future.result()
8994
finally:
9095
# Kill all python servers when done
9196
self.environment.execute("pkill -f 'python main.py' || true")
9297

93-
def _run_single_simulation(self, cmd: str) -> tuple[str, str]:
98+
def _run_single_simulation(self, cmd: str, idx: int) -> tuple[str, str]:
9499
"""Run a single battlesnake simulation and return log and result outputs."""
95-
# Create temporary output file for results
96-
output_file = f"battlesnake_output_{uuid.uuid4().hex}.json"
97-
98-
# Run game
99-
response = assert_zero_exit_code(
100+
assert_zero_exit_code(
100101
self.environment.execute(
101-
cmd + f" -o {output_file}",
102+
cmd + f" -o logs/sim_out_{idx}.json",
102103
cwd=f"{self.environment.config.cwd}/game",
103104
)
104105
)
105-
106-
# Read the output file for result information
107-
result_response = self.environment.execute(f"cat game/{output_file}")
108-
result_output = result_response["output"]
109-
110-
# Clean up the output file
111-
self.environment.execute(f"rm -f game/{output_file}")
112-
113-
return response["output"], result_output

codeclash/games/corewar/corewar.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from pathlib import Path
44

55
from codeclash.agents.player import Player
6-
from codeclash.games.game import CodeGame, RoundData, RoundStats
6+
from codeclash.games.game import CodeGame, RoundStats
7+
from codeclash.utils.environment import copy_from_container
78

89

910
class CoreWarGame(CodeGame):
@@ -19,8 +20,16 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
1920
else:
2021
self.run_cmd_round += f" -{arg} {val}"
2122

22-
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
23-
result_output = result_outputs[0] # Get the first (and only) element
23+
def copy_logs_from_env(self, round_num: int) -> None:
24+
super().copy_logs_from_env(round_num)
25+
copy_from_container(
26+
container=self.environment,
27+
src_path="/testbed/output.log",
28+
dest_path=self.log_local / "rounds" / str(round_num) / "output.log",
29+
)
30+
31+
def get_stats(self, agents: list[Player]) -> RoundStats:
32+
result_output = self.environment.execute("cat output.log")["output"]
2433
self.logger.debug(f"Determining winner from result output: {result_output}")
2534
scores = []
2635
n = len(agents) * 2
@@ -50,10 +59,9 @@ def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundSta
5059
self.logger.debug("No scores found, returning unknown")
5160
return RoundStats(winner="unknown", scores={agent.name: 0 for agent in agents})
5261

53-
def execute_round(self, agents: list[Player]) -> RoundData:
62+
def execute_round(self, agents: list[Player]):
5463
args = [f"/{agent.name}/warriors/warrior.red" for agent in agents]
55-
cmd = f"{self.run_cmd_round} {shlex.join(args)} -r {self.game_config['sims_per_round']}"
64+
cmd = f"{self.run_cmd_round} {shlex.join(args)} -r {self.game_config['sims_per_round']} > output.log;"
5665
self.logger.info(f"Running game: {cmd}")
5766
response = self.environment.execute(cmd)
5867
assert response["returncode"] == 0, response
59-
return RoundData(logs=[response["output"]], results=[response["output"]])

codeclash/games/dummy/dummy_game.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import re
22

33
from codeclash.agents.player import Player
4-
from codeclash.games.game import CodeGame, RoundData, RoundStats
4+
from codeclash.games.game import CodeGame, RoundStats
5+
from codeclash.utils.environment import assert_zero_exit_code, copy_from_container
56

67

78
class DummyGame(CodeGame):
89
name: str = "DummyGame"
910

10-
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
11-
result_output = result_outputs[0] # Get the first (and only) element
11+
def copy_logs_from_env(self, round_num):
12+
super().copy_logs_from_env(round_num)
13+
copy_from_container(
14+
container=self.environment,
15+
src_path="/testbed/result.log",
16+
dest_path=self.log_local / "rounds" / str(round_num) / "result.log",
17+
)
18+
19+
def get_stats(self, agents: list[Player]) -> RoundStats:
20+
result_output = self.environment.execute("cat result.log")["output"]
1221
lines = result_output.split("FINAL_RESULTS")[-1].splitlines()
1322

1423
scores = {}
@@ -25,10 +34,8 @@ def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundSta
2534
details={"dummy": True},
2635
)
2736

28-
def execute_round(self, agents: list[Player]) -> RoundData:
37+
def execute_round(self, agents: list[Player]) -> None:
2938
args = [f"/{agent.name}/main.py" for agent in agents]
30-
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']}"
39+
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > result.log;"
3140
self.logger.info(f"Running game: {cmd}")
32-
response = self.environment.execute(cmd)
33-
assert response["returncode"] == 0, response
34-
return RoundData(logs=[response["output"]], results=[response["output"]])
41+
assert_zero_exit_code(self.environment.execute(cmd))

codeclash/games/dummy/main.py

Whitespace-only changes.

codeclash/games/game.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,6 @@ def __str__(self) -> str:
2323
return "\n".join([f"- Winner: {self.winner}", f"- Scores: {self.scores}"])
2424

2525

26-
class RoundData(BaseModel):
27-
logs: list[str]
28-
results: list[str]
29-
30-
31-
class RoundRecord(BaseModel):
32-
data: RoundData
33-
stats: RoundStats
34-
35-
3626
class CodeGame(ABC):
3727
name: str
3828

@@ -97,7 +87,7 @@ def build_image(self):
9787
result = subprocess.run(
9888
(
9989
"export $(cat .env | xargs);"
100-
f"docker build --build-arg GITHUB_TOKEN=$GITHUB_TOKEN -t {self.image_name} -f docker/{self.name}.Dockerfile ."
90+
f"docker build --no-cache --build-arg GITHUB_TOKEN=$GITHUB_TOKEN -t {self.image_name} -f docker/{self.name}.Dockerfile ."
10191
),
10292
shell=True,
10393
capture_output=True,
@@ -168,12 +158,15 @@ def _pre_round_setup(self, agents: list[Player]):
168158
logger=self.logger,
169159
)
170160

161+
def copy_logs_from_env(self, round_num: int) -> None:
162+
"""Copy logs from the game's environment to the local machine."""
163+
(self.log_local / "rounds" / str(round_num)).mkdir(parents=True, exist_ok=True)
164+
171165
@abstractmethod
172-
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
166+
def get_stats(self, agents: list[Player]) -> RoundStats:
173167
"""Determine the winner of the game based on the result output.
174168
175169
Args:
176-
result_outputs: The specific output(s) containing winning information
177170
agents: List of agents participating in the round
178171
179172
Returns:
@@ -182,24 +175,22 @@ def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundSta
182175
pass
183176

184177
@abstractmethod
185-
def execute_round(self, agents: list[Player]) -> RoundData:
178+
def execute_round(self, agents: list[Player]):
186179
"""Subclasses implement their game-specific logic here.
187180
This is the low level implementation, you probably want to use run_round instead, which
188181
includes the pre-round setup, post-round setup, and winner determination.
189-
190-
Returns:
191-
RoundData object
192182
"""
193183
pass
194184

195-
def run_round(self, agents: list[Player]) -> RoundRecord:
185+
def run_round(self, agents: list[Player], round_num: int) -> RoundStats:
196186
"""
197187
Run a single round of the game with the given agents.
198188
199189
Returns the log output, result output, and winner name. All bookkeeping should be
200190
handled by the tournament class.
201191
"""
202192
self._pre_round_setup(agents)
203-
data = self.execute_round(agents)
204-
stats = self.get_stats(data.results, agents)
205-
return RoundRecord(data=data, stats=stats)
193+
self.execute_round(agents)
194+
stats = self.get_stats(agents)
195+
self.copy_logs_from_env(round_num)
196+
return stats
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pathlib import Path
2+
3+
from codeclash.agents.player import Player
4+
from codeclash.games.game import CodeGame, RoundStats
5+
6+
7+
class HuskyBenchGame(CodeGame):
8+
name: str = "HuskyBench"
9+
10+
def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
11+
super().__init__(config, tournament_id=tournament_id, local_output_dir=local_output_dir)
12+
self.run_cmd_round: str = (
13+
f"python engine/main.py --port 8000 --sim --sim-rounds {self.game_config['sims_per_round']}"
14+
)
15+
for arg, val in self.game_config.get("args", {}).items():
16+
if isinstance(val, bool):
17+
if val:
18+
self.run_cmd_round += f" --{arg}"
19+
else:
20+
self.run_cmd_round += f" --{arg} {val}"
21+
22+
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
23+
return RoundStats(winner="N/A", scores={})
24+
25+
def execute_round(self, agents: list[Player]):
26+
try:
27+
self.logger.debug("Starting game servers")
28+
self.environment.execute(self.run_cmd_round + " > output.log &")
29+
for agent in agents:
30+
self.environment.execute("python client/main.py --port 8000 &", cwd=f"/{agent.name}")
31+
finally:
32+
# Kill all python servers when done
33+
self.environment.execute("pkill -f 'python client/main.py' || true")
34+
self.environment.execute("pkill -f 'python engine/main.py' || true")

0 commit comments

Comments
 (0)