Skip to content

Commit fb84f4f

Browse files
committed
Added dataclasses for return type; added score tracking for games
1 parent c70fa5c commit fb84f4f

14 files changed

Lines changed: 165 additions & 147 deletions

File tree

codeclash/agents/abstract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from codeclash.agents.utils import GameContext
99
from codeclash.constants import GH_ORG
1010
from codeclash.tournaments.utils.git_utils import filter_git_diff
11-
from codeclash.utils.environment import assert_zero_exit_code, create_file_on_container
11+
from codeclash.utils.environment import assert_zero_exit_code, create_file_in_container
1212
from codeclash.utils.log import get_logger
1313

1414
load_dotenv()
@@ -101,7 +101,7 @@ def reset_and_apply_patch(
101101
self.logger.debug("No patch to apply, skipping")
102102
return
103103

104-
create_file_on_container(
104+
create_file_in_container(
105105
container=self.environment, # type: ignore
106106
content=patch,
107107
dest_path="tmp_patch.txt",

codeclash/constants.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,4 @@
33
DIR_LOGS = Path("logs")
44
DIR_WORK = Path("/testbed")
55
GH_ORG = "emagedoc"
6-
OUTPUTS_LOGS = "log_outputs"
7-
OUTPUTS_RESULTS = "result_outputs"
86
RESULT_TIE = "Tie"

codeclash/games/abstract.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,42 @@
22
import os
33
import subprocess
44
from abc import ABC, abstractmethod
5+
from dataclasses import dataclass
56
from pathlib import Path
7+
from typing import Any
68

79
from minisweagent.environments.docker import DockerEnvironment
810

911
from codeclash.agents.abstract import Player
10-
from codeclash.constants import (
11-
DIR_LOGS,
12-
DIR_WORK,
13-
GH_ORG,
14-
OUTPUTS_LOGS,
15-
OUTPUTS_RESULTS,
16-
)
12+
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG
1713
from codeclash.utils.environment import assert_zero_exit_code, copy_between_containers
1814
from codeclash.utils.log import get_logger
1915

2016

17+
@dataclass
18+
class RoundStats:
19+
winner: str
20+
scores: dict[
21+
str, float
22+
] # Map of player to game metric (e.g. # of wins, assets accumulated)
23+
details: dict[str, Any] = None # Optional, for game-specific info
24+
25+
def __str__(self) -> str:
26+
return "\n".join([f"- Winner: {self.winner}", f"- Scores: {self.scores}"])
27+
28+
29+
@dataclass
30+
class RoundData:
31+
logs: list[str]
32+
results: list[str]
33+
34+
35+
@dataclass
36+
class RoundRecord:
37+
data: RoundData
38+
stats: RoundStats
39+
40+
2141
class CodeGame(ABC):
2242
name: str
2343

@@ -144,48 +164,37 @@ def _pre_round_setup(self, agents: list[Player]):
144164
)
145165

146166
@abstractmethod
147-
def determine_winner(
148-
self, result_outputs: list[str], agents: list[Player]
149-
) -> dict[str, str]:
167+
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
150168
"""Determine the winner of the game based on the result output.
151169
152170
Args:
153171
result_outputs: The specific output(s) containing winning information
154172
agents: List of agents participating in the round
155173
156174
Returns:
157-
Dictionary with key "winner" containing the winner's name
175+
RoundStats object
158176
"""
159177
pass
160178

161179
@abstractmethod
162-
def execute_round(self, agents: list[Player]) -> dict[str, list[str]]:
180+
def execute_round(self, agents: list[Player]) -> RoundData:
163181
"""Subclasses implement their game-specific logic here.
164182
This is the low level implementation, you probably want to use run_round instead, which
165183
includes the pre-round setup, post-round setup, and winner determination.
166184
167185
Returns:
168-
Dictionary with keys "log_outputs" and "result_outputs"
186+
RoundData object
169187
"""
170188
pass
171189

172-
def run_round(self, agents: list[Player]) -> dict[str, str]:
190+
def run_round(self, agents: list[Player]) -> RoundRecord:
173191
"""
174192
Run a single round of the game with the given agents.
175193
176194
Returns the log output, result output, and winner name. All bookkeeping should be
177195
handled by the tournament class.
178196
"""
179197
self._pre_round_setup(agents)
180-
result = self.execute_round(agents)
181-
log_outputs = result[OUTPUTS_LOGS]
182-
result_outputs = result[OUTPUTS_RESULTS]
183-
184-
winner_result = self.determine_winner(result_outputs, agents)
185-
winner_name = winner_result["winner"]
186-
187-
return {
188-
OUTPUTS_LOGS: log_outputs,
189-
OUTPUTS_RESULTS: result_outputs,
190-
"winner": winner_name,
191-
}
198+
data = self.execute_round(agents)
199+
stats = self.get_stats(data.results, agents)
200+
return RoundRecord(data=data, stats=stats)

codeclash/games/battlecode/main.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from tqdm.auto import tqdm
66

7-
from codeclash.constants import DIR_WORK, OUTPUTS_LOGS, OUTPUTS_RESULTS, RESULT_TIE
8-
from codeclash.games.abstract import CodeGame
7+
from codeclash.constants import DIR_WORK, RESULT_TIE
8+
from codeclash.games.abstract import CodeGame, RoundData, RoundStats
99

1010

1111
class BattleCodeGame(CodeGame):
@@ -24,9 +24,7 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2424
else:
2525
self.run_cmd_round += f" --{arg} {val}"
2626

27-
def determine_winner(
28-
self, result_outputs: list[str], agents: list[Any]
29-
) -> dict[str, str]:
27+
def get_stats(self, result_outputs: list[str], agents: list[Any]) -> RoundStats:
3028
winners = []
3129
for ro in result_outputs:
3230
lines = ro.strip().split("\n")
@@ -44,10 +42,12 @@ def determine_winner(
4442
winners.append(winner)
4543
else:
4644
winners.append(RESULT_TIE)
47-
winner = max(set(winners), key=winners.count)
48-
return {"winner": winner}
45+
return RoundStats(
46+
winner=max(set(winners), key=winners.count),
47+
scores={agent.name: winners.count(agent.name) for agent in agents},
48+
)
4949

50-
def execute_round(self, agents: list[Any]) -> dict[str, list[str]]:
50+
def execute_round(self, agents: list[Any]) -> RoundData:
5151
for agent in agents:
5252
src, dest = f"/{agent.name}/src/mysubmission/", str(
5353
DIR_WORK / "src" / agent.name
@@ -58,11 +58,11 @@ def execute_round(self, agents: list[Any]) -> dict[str, list[str]]:
5858
for idx, agent in enumerate(agents)
5959
]
6060
cmd = f"{self.run_cmd_round} {' '.join(args)}"
61-
self.logger.info(f"Running command: {cmd}")
61+
self.logger.info(f"Running game: {cmd}")
6262
outputs = []
6363
for _ in tqdm(range(self.game_config["sims_per_round"])):
6464
response = self.environment.execute(cmd)
6565
assert response["returncode"] == 0, response
6666
# For BattleCode, log_outputs and result_outputs are the same
6767
outputs.append(response["output"])
68-
return {OUTPUTS_LOGS: outputs, OUTPUTS_RESULTS: outputs}
68+
return RoundData(logs=outputs, results=outputs)

codeclash/games/battlesnake/main.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import time
33
from pathlib import Path
44

5+
from tqdm.auto import tqdm
6+
57
from codeclash.agents.abstract import Player
6-
from codeclash.constants import OUTPUTS_LOGS, OUTPUTS_RESULTS
7-
from codeclash.games.abstract import CodeGame
8+
from codeclash.constants import RESULT_TIE
9+
from codeclash.games.abstract import CodeGame, RoundData, RoundStats
810
from codeclash.utils.environment import assert_zero_exit_code
911

1012

@@ -23,21 +25,25 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2325
else:
2426
self.run_cmd_round += f" --{arg} {val}"
2527

26-
def determine_winner(
27-
self, result_outputs: list[str], agents: list[Player]
28-
) -> dict[str, str]:
28+
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
2929
winners = []
3030
for ro in result_outputs:
3131
lines = ro.strip().split("\n")
32-
# Get the last line which contains the game result
33-
last_line = lines[-1] if lines else ""
34-
self.logger.debug(f"Last line: {last_line}")
32+
last_line = (
33+
lines[-1] if lines else ""
34+
) # Get the last line which contains the game result
3535
winner = json.loads(last_line)["winnerName"]
3636
winners.append(winner)
37-
winner = max(set(winners), key=winners.count)
38-
return {"winner": winner}
3937

40-
def execute_round(self, agents: list[Player]) -> dict[str, list[str]]:
38+
win_counts = {agent.name: winners.count(agent.name) for agent in agents}
39+
max_wins = max(win_counts.values())
40+
winners = [name for name, wins in win_counts.items() if wins == max_wins]
41+
return RoundStats(
42+
winner=RESULT_TIE if len(winners) > 1 else winners[0],
43+
scores=win_counts,
44+
)
45+
46+
def execute_round(self, agents: list[Player]) -> RoundData:
4147
cmd = []
4248
for idx, agent in enumerate(agents):
4349
port = 8001 + idx
@@ -51,15 +57,16 @@ def execute_round(self, agents: list[Player]) -> dict[str, list[str]]:
5157

5258
try:
5359
log_outputs, result_outputs = [], []
54-
for idx in range(self.game_config["sims_per_round"]):
60+
cmd = self.run_cmd_round + " " + " ".join(cmd)
61+
self.logger.info(f"Running game: {cmd}")
62+
for idx in tqdm(range(self.game_config["sims_per_round"])):
5563
# Create temporary output file for results
5664
output_file = f"battlesnake_output_{idx}_{int(time.time())}.json"
57-
cmd_str = " ".join(cmd) + f" -o {output_file}"
58-
self.logger.info(f"Running command: {self.run_cmd_round} {cmd_str}")
5965

66+
# Run game
6067
response = assert_zero_exit_code(
6168
self.environment.execute(
62-
f"{self.run_cmd_round} {cmd_str}",
69+
cmd + f" -o {output_file}",
6370
cwd=f"{self.environment.config.cwd}/game",
6471
)
6572
)
@@ -72,10 +79,9 @@ def execute_round(self, agents: list[Player]) -> dict[str, list[str]]:
7279

7380
# Clean up the output file
7481
self.environment.execute(f"rm -f game/{output_file}")
82+
time.sleep(0.05)
7583

76-
time.sleep(0.1)
77-
78-
return {OUTPUTS_LOGS: log_outputs, OUTPUTS_RESULTS: result_outputs}
84+
return RoundData(log_outputs, result_outputs)
7985
finally:
8086
# Kill all python servers when done
8187
self.environment.execute("pkill -f 'python main.py' || true")

codeclash/games/corewar/main.py

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

55
from codeclash.agents.abstract import Player
6-
from codeclash.constants import OUTPUTS_LOGS, OUTPUTS_RESULTS
7-
from codeclash.games.abstract import CodeGame
6+
from codeclash.games.abstract import CodeGame, RoundData, RoundStats
87

98

109
class CoreWarGame(CodeGame):
@@ -22,44 +21,43 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2221
else:
2322
self.run_cmd_round += f" -{arg} {val}"
2423

25-
def determine_winner(
26-
self, result_outputs: list[str], agents: list[Player]
27-
) -> dict[str, str]:
24+
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
2825
result_output = result_outputs[0] # Get the first (and only) element
2926
self.logger.debug(f"Determining winner from result output: {result_output}")
3027
scores = []
3128
n = len(agents) * 2
3229
lines = result_output.strip().split("\n")
30+
3331
# Get the last n lines which contain the scores (closer to original)
3432
relevant_lines = lines[-n:] if len(lines) >= n else lines
33+
relevant_lines = [l for l in relevant_lines if len(l.strip()) > 0]
3534
self.logger.debug(f"Relevant lines for scoring: {relevant_lines}")
3635

36+
# Go through each line; we assume score position is correlated with agent index
3737
for line in relevant_lines:
3838
match = re.search(r".*\sby\s.*\sscores\s(\d+)", line)
3939
if match:
4040
score = int(match.group(1))
4141
scores.append(score)
42-
self.logger.debug(f"Found score: {score} from line: {line}")
4342

44-
self.logger.debug(f"All scores: {scores}")
4543
if scores:
46-
max_score_index = scores.index(max(scores))
47-
winner = agents[max_score_index].name
48-
self.logger.debug(
49-
f"Concluding winner: {winner} with index {max_score_index}"
44+
if len(scores) != len(agents):
45+
self.logger.error(f"Have {len(scores)} scores but {len(agents)} agents")
46+
return RoundStats(
47+
winner=agents[scores.index(max(scores))].name,
48+
scores={agent.name: score for agent, score in zip(agents, scores)},
49+
details={"stdout": "\n".join(relevant_lines)},
5050
)
51-
return {"winner": winner}
5251
else:
5352
self.logger.debug("No scores found, returning unknown")
54-
return {"winner": "unknown"}
53+
return RoundStats(
54+
winner="unknown", scores={agent.name: 0 for agent in agents}
55+
)
5556

56-
def execute_round(self, agents: list[Player]) -> dict[str, list[str]]:
57+
def execute_round(self, agents: list[Player]) -> RoundData:
5758
args = [f"/{agent.name}/warriors/warrior.red" for agent in agents]
58-
cmd = f"{self.run_cmd_round} {shlex.join(args)}"
59-
cmd += f" -r {self.game_config['sims_per_round']}"
60-
self.logger.info(f"Running command: {cmd}")
59+
cmd = f"{self.run_cmd_round} {shlex.join(args)} -r {self.game_config['sims_per_round']}"
60+
self.logger.info(f"Running game: {cmd}")
6161
response = self.environment.execute(cmd)
6262
assert response["returncode"] == 0, response
63-
# For CoreWar, log_outputs and result_outputs are the same
64-
output = [response["output"]]
65-
return {OUTPUTS_LOGS: output, OUTPUTS_RESULTS: output}
63+
return RoundData([response["output"]], [response["output"]])

0 commit comments

Comments
 (0)