Skip to content

Commit da1ad3e

Browse files
committed
Remove RoundRecord; Add explicit copy logs method
1 parent 9e5892d commit da1ad3e

13 files changed

Lines changed: 145 additions & 148 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/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/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

codeclash/games/huskybench/huskybench.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pathlib import Path
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
55

66

77
class HuskyBenchGame(CodeGame):
@@ -22,17 +22,12 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2222
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
2323
return RoundStats(winner="N/A", scores={})
2424

25-
def execute_round(self, agents: list[Player]) -> RoundData:
25+
def execute_round(self, agents: list[Player]):
2626
try:
2727
self.logger.debug("Starting game servers")
28-
self.environment.execute(self.run_cmd_round + " > engine/output/std_out.log &")
28+
self.environment.execute(self.run_cmd_round + " > output.log &")
2929
for agent in agents:
3030
self.environment.execute("python client/main.py --port 8000 &", cwd=f"/{agent.name}")
31-
32-
# Save logs to response
33-
self.logger.info(self.environment.execute("cat engine/output/std_out.log")["output"])
34-
35-
return RoundData(logs=[], results=[])
3631
finally:
3732
# Kill all python servers when done
3833
self.environment.execute("pkill -f 'python client/main.py' || true")

0 commit comments

Comments
 (0)