Skip to content

Commit e24cf3f

Browse files
authored
Fixes #29: Add a sims_per_round flag (#34)
* Add `sims_per_round` flag * Minor fix * Added dataclasses for return type; added score tracking for games * Move from black/isort to ruff linting
1 parent 2f72ff9 commit e24cf3f

30 files changed

Lines changed: 707 additions & 555 deletions

.pre-commit-config.yaml

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
ci:
2+
autoupdate_commit_msg: "chore: update pre-commit hooks"
3+
14
repos:
25
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.4.0
6+
rev: v6.0.0
47
hooks:
58
- id: trailing-whitespace
69
- id: end-of-file-fixer
@@ -9,14 +12,24 @@ repos:
912
- id: check-merge-conflict
1013
- id: debug-statements
1114

12-
- repo: https://github.com/psf/black
13-
rev: 23.3.0
15+
- repo: https://github.com/crate-ci/typos
16+
rev: v1
17+
hooks:
18+
- id: typos
19+
files: \.(py|md|rst|yaml|toml)
20+
exclude: pyproject.toml
21+
22+
- repo: https://github.com/astral-sh/ruff-pre-commit
23+
rev: v0.12.10
1424
hooks:
15-
- id: black
16-
language_version: python3
25+
# Run the linter.
26+
- id: ruff
27+
args: ["--fix"]
28+
# Run the formatter.
29+
- id: ruff-format
1730

18-
- repo: https://github.com/pycqa/isort
19-
rev: 5.12.0
31+
- repo: https://github.com/pre-commit/mirrors-prettier
32+
rev: "v4.0.0-alpha.8" # Use the sha or tag you want to point at
2033
hooks:
21-
- id: isort
22-
args: ["--profile", "black"]
34+
- id: prettier
35+
types_or: ["javascript", "css"]

codeclash/agents/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
from codeclash.agents.utils import GameContext
77

88

9-
def get_agent(
10-
config: dict, game_context: GameContext, environment: DockerEnvironment
11-
) -> Player:
9+
def get_agent(config: dict, game_context: GameContext, environment: DockerEnvironment) -> Player:
1210
agents = {
1311
"dummy": Dummy,
1412
"mini": MiniSWEAgent,

codeclash/agents/abstract.py

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

1515
load_dotenv()
@@ -25,7 +25,7 @@ def __init__(
2525
self.config = config
2626
self.name = config["name"]
2727
self._player_unique_id = uuid.uuid4()
28-
"""Unique ID that doesn't clash even accross multiple games. Used for git tags."""
28+
"""Unique ID that doesn't clash even across multiple games. Used for git tags."""
2929
self.environment = environment
3030
self.game_context = game_context
3131
self.logger = get_logger(
@@ -54,9 +54,7 @@ def post_run_hook(self, *, round: int) -> None:
5454
"""Should be called after we called the run method."""
5555
self._commit()
5656
self._metadata["diff"][round] = self._get_round_diff(round)
57-
self._metadata["incremental_diff"][round] = self._get_round_diff(
58-
round, incremental=True
59-
)
57+
self._metadata["incremental_diff"][round] = self._get_round_diff(round, incremental=True)
6058

6159
@abstractmethod
6260
def run(self) -> None:
@@ -79,23 +77,15 @@ def push(self) -> None:
7977
"git push origin --tags",
8078
]:
8179
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
82-
self.logger.info(
83-
f"Pushed {self.name} commit history to remote repository (branch {self._branch_name})"
84-
)
80+
self.logger.info(f"Pushed {self.name} commit history to remote repository (branch {self._branch_name})")
8581

86-
def reset_and_apply_patch(
87-
self, patch: str, *, base_commit: str = "", filter_patch: bool = True
88-
) -> None:
89-
"""Clean all uncommited changes. If base_commit is provided, reset to that commit.
82+
def reset_and_apply_patch(self, patch: str, *, base_commit: str = "", filter_patch: bool = True) -> None:
83+
"""Clean all uncommitted changes. If base_commit is provided, reset to that commit.
9084
Then apply the patch to the codebase.
9185
"""
9286
# Need to clean before we copy over the patch (else it's gonna be removed by git clean)
9387
self.logger.debug(
94-
assert_zero_exit_code(
95-
self.environment.execute(
96-
f"git reset --hard {base_commit} && git clean -fd"
97-
)
98-
)
88+
assert_zero_exit_code(self.environment.execute(f"git reset --hard {base_commit} && git clean -fd"))
9989
)
10090

10191
patch = filter_git_diff(patch) if filter_patch else patch
@@ -104,7 +94,7 @@ def reset_and_apply_patch(
10494
self.logger.debug("No patch to apply, skipping")
10595
return
10696

107-
create_file_on_container(
97+
create_file_in_container(
10898
container=self.environment, # type: ignore
10999
content=patch,
110100
dest_path="tmp_patch.txt",
@@ -115,19 +105,15 @@ def reset_and_apply_patch(
115105
commands = ["git status", "git apply tmp_patch.txt", "rm -f tmp_patch.txt"]
116106
for cmd in commands:
117107
self.logger.debug(f"Executing command: {cmd}")
118-
out = assert_zero_exit_code(
119-
self.environment.execute(cmd), logger=self.logger
120-
)
108+
out = assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
121109
self.logger.debug(out)
122110

123111
# --- Helper methods ---
124112

125113
def _tag_round(self, round: int) -> None:
126114
"""Git tag the codebase at the given round."""
127115
assert_zero_exit_code(
128-
self.environment.execute(
129-
f"git tag -a {self._get_round_tag_name(round)} -m 'Round {round} Update'"
130-
),
116+
self.environment.execute(f"git tag -a {self._get_round_tag_name(round)} -m 'Round {round} Update'"),
131117
logger=self.logger,
132118
)
133119

@@ -164,9 +150,7 @@ def _get_round_diff(self, round: int, *, incremental: bool = False) -> str:
164150
previous_round_tag = self._get_round_tag_name(0)
165151
current_round_tag = self._get_round_tag_name(round)
166152
out = assert_zero_exit_code(
167-
self.environment.execute(
168-
f"git diff {previous_round_tag}..{current_round_tag}"
169-
),
153+
self.environment.execute(f"git diff {previous_round_tag}..{current_round_tag}"),
170154
logger=self.logger,
171155
)
172156
return out["output"]

codeclash/agents/minisweagent.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from codeclash.agents.abstract import Player
1919
from codeclash.agents.utils import GameContext, resolve_api_key
20-
from codeclash.utils.environment import copy_file_to_container
20+
from codeclash.utils.environment import copy_to_container
2121

2222

2323
class ClashAgent(DefaultAgent):
@@ -47,9 +47,7 @@ def add_message(self, role: str, content: str, **kwargs):
4747
super().add_message(role, content, **kwargs)
4848
self.logger.debug(f"[{role}] {content}", extra={"highlighter": None})
4949
if role == "assistant":
50-
self.logger.info(
51-
f"Step taken (step {self.model.n_calls}, cost {self.model.cost:.2f})"
52-
)
50+
self.logger.info(f"Step taken (step {self.model.n_calls}, cost {self.model.cost:.2f})")
5351

5452
def render_template(self, template: str, **kwargs) -> str:
5553
cs = (
@@ -69,9 +67,7 @@ def run(self) -> tuple[str, str]:
6967
class MiniSWEAgent(Player):
7068
"""Player with agentic code editing capabilities"""
7169

72-
def __init__(
73-
self, config: dict, environment: DockerEnvironment, game_context: GameContext
74-
):
70+
def __init__(self, config: dict, environment: DockerEnvironment, game_context: GameContext):
7571
super().__init__(config, environment=environment, game_context=game_context)
7672

7773
def run(self):
@@ -96,18 +92,15 @@ def run(self):
9692
result = exc_message
9793
print(exc_message)
9894
finally:
99-
traj_path = (
100-
self.game_context.log_local
101-
/ f"{self.name}_r{self.game_context.round}.traj.json"
102-
)
95+
traj_path = self.game_context.log_local / f"{self.name}_r{self.game_context.round}.traj.json"
10396
save_traj(
10497
self.agent, # type: ignore
10598
traj_path,
10699
exit_status=exit_status,
107100
result=result,
108101
print_fct=self.logger.debug,
109102
)
110-
copy_file_to_container(
103+
copy_to_container(
111104
self.environment,
112105
traj_path,
113106
self.game_context.log_env / traj_path.name,

codeclash/agents/utils.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def resolve_api_key(model: str) -> str:
1313
return os.getenv("ANTHROPIC_API_KEY")
1414
if "gpt" in model:
1515
return os.getenv("OPENAI_API_KEY")
16+
return ""
1617

1718

1819
@dataclass
@@ -38,10 +39,7 @@ class GameContext:
3839

3940
def _render_prompt_templates(self) -> dict:
4041
context = asdict(self)
41-
return {
42-
key: Template(template_str).render(**context)
43-
for key, template_str in self.prompts.items()
44-
}
42+
return {key: Template(template_str).render(**context) for key, template_str in self.prompts.items()}
4543

4644
def to_template_vars(self) -> dict[str, str]:
4745
"""Convert the GameContext to a dictionary for rendering prompts in the agent"""

codeclash/games/abstract.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import subprocess
33
import time
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

@@ -12,6 +14,28 @@
1214
from codeclash.utils.log import get_logger
1315

1416

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

@@ -41,9 +65,7 @@ def __init__(self, config: dict, *, tournament_id: str, local_output_dir: Path):
4165
}
4266
self.log_env: Path = (DIR_WORK / DIR_LOGS / self.game_id).resolve()
4367
self.log_local: Path = local_output_dir
44-
self.logger = get_logger(
45-
self.name, log_path=self.log_local / "game.log", emoji="🏓"
46-
)
68+
self.logger = get_logger(self.name, log_path=self.log_local / "game.log", emoji="🏓")
4769
self.environment: DockerEnvironment = self.get_environment()
4870
"""The running docker environment for executing the game"""
4971

@@ -87,9 +109,7 @@ def build_image(self):
87109
if result.returncode == 0:
88110
self.logger.info(f"✅ Built Docker image {self.image_name}")
89111
else:
90-
self.logger.error(
91-
f"❌ Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}"
92-
)
112+
self.logger.error(f"❌ Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}")
93113
raise RuntimeError(f"Failed to build Docker image: {result.stderr}")
94114

95115
def get_metadata(self) -> dict:
@@ -146,48 +166,37 @@ def _pre_round_setup(self, agents: list[Player]):
146166
)
147167

148168
@abstractmethod
149-
def determine_winner(
150-
self, result_output: str, agents: list[Player]
151-
) -> dict[str, str]:
169+
def get_stats(self, result_outputs: list[str], agents: list[Player]) -> RoundStats:
152170
"""Determine the winner of the game based on the result output.
153171
154172
Args:
155-
result_output: The specific output containing winning information
173+
result_outputs: The specific output(s) containing winning information
156174
agents: List of agents participating in the round
157175
158176
Returns:
159-
Dictionary with key "winner" containing the winner's name
177+
RoundStats object
160178
"""
161179
pass
162180

163181
@abstractmethod
164-
def execute_round(self, agents: list[Player]) -> dict[str, str]:
182+
def execute_round(self, agents: list[Player]) -> RoundData:
165183
"""Subclasses implement their game-specific logic here.
166184
This is the low level implementation, you probably want to use run_round instead, which
167185
includes the pre-round setup, post-round setup, and winner determination.
168186
169187
Returns:
170-
Dictionary with keys "log_output" and "result_output"
188+
RoundData object
171189
"""
172190
pass
173191

174-
def run_round(self, agents: list[Player]) -> dict[str, str]:
192+
def run_round(self, agents: list[Player]) -> RoundRecord:
175193
"""
176194
Run a single round of the game with the given agents.
177195
178196
Returns the log output, result output, and winner name. All bookkeeping should be
179197
handled by the tournament class.
180198
"""
181199
self._pre_round_setup(agents)
182-
result = self.execute_round(agents)
183-
log_output = result["log_output"]
184-
result_output = result["result_output"]
185-
186-
winner_result = self.determine_winner(result_output, agents)
187-
winner_name = winner_result["winner"]
188-
189-
return {
190-
"log_output": log_output,
191-
"result_output": result_output,
192-
"winner": winner_name,
193-
}
200+
data = self.execute_round(agents)
201+
stats = self.get_stats(data.results, agents)
202+
return RoundRecord(data=data, stats=stats)

0 commit comments

Comments
 (0)