Skip to content

Commit 548b6b4

Browse files
committed
Working evaluation for single player mode
1 parent 20f8cb2 commit 548b6b4

5 files changed

Lines changed: 108 additions & 83 deletions

File tree

codeclash/agents/abstract.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from codeclash.agents.utils import GameContext
99
from codeclash.constants import GH_ORG
10-
from codeclash.utils.environment import assert_zero_exit_code
10+
from codeclash.tournaments.utils.git_utils import filter_git_diff
11+
from codeclash.utils.environment import assert_zero_exit_code, create_file_on_container
1112
from codeclash.utils.log import get_logger
1213

1314
load_dotenv()
@@ -81,6 +82,43 @@ def push(self) -> None:
8182
f"Pushed {self.name} commit history to remote repository (branch {self._branch_name})"
8283
)
8384

85+
def reset_and_apply_patch(
86+
self, patch: str, *, base_commit: str = "", filter_patch: bool = True
87+
) -> None:
88+
"""Clean all uncommited changes. If base_commit is provided, reset to that commit.
89+
Then apply the patch to the codebase.
90+
"""
91+
# Need to clean before we copy over the patch (else it's gonna be removed by git clean)
92+
self.logger.debug(
93+
assert_zero_exit_code(
94+
self.environment.execute(
95+
f"git reset --hard {base_commit} && git clean -fd"
96+
)
97+
)
98+
)
99+
100+
patch = filter_git_diff(patch) if filter_patch else patch
101+
102+
if not patch.strip():
103+
self.logger.debug("No patch to apply, skipping")
104+
return
105+
106+
create_file_on_container(
107+
container=self.environment, # type: ignore
108+
content=patch,
109+
dest_path="tmp_patch.txt",
110+
)
111+
112+
self.logger.debug(f"Applying patch to agent's codebase: {patch}")
113+
114+
commands = ["git status", "git apply tmp_patch.txt", "rm -f tmp_patch.txt"]
115+
for cmd in commands:
116+
self.logger.debug(f"Executing command: {cmd}")
117+
out = assert_zero_exit_code(
118+
self.environment.execute(cmd), logger=self.logger
119+
)
120+
self.logger.debug(out)
121+
84122
# --- Helper methods ---
85123

86124
def _tag_round(self, round: int) -> None:

codeclash/tournaments/abstract.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import getpass
22
import time
3+
import traceback
34
from pathlib import Path
45

56
from codeclash.constants import DIR_LOGS
7+
from codeclash.utils.environment import create_file_on_container
68
from codeclash.utils.log import get_logger
79

810

@@ -24,3 +26,18 @@ def __init__(self, config: dict, *, name: str, **kwargs):
2426

2527
def get_metadata(self) -> dict:
2628
return self._metadata
29+
30+
def _copy_game_log_to_agent(self, agent, round_num: int, log_output: str) -> None:
31+
"""Copy round log to agent environment."""
32+
try:
33+
create_file_on_container(
34+
container=agent.environment,
35+
content=log_output,
36+
dest_path=f"logs/round_{round_num}.log",
37+
)
38+
except Exception:
39+
self.logger.error(
40+
f"Error creating round log in {agent.name}'s container: {traceback.format_exc()}"
41+
)
42+
else:
43+
self.logger.info(f"Created round log in {agent.name}'s container.")

codeclash/tournaments/pvp_training.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
PvP training mode where multiple agents compete against each other.
33
"""
44

5-
import traceback
6-
75
from codeclash.agents import get_agent
86
from codeclash.agents.abstract import Player
97
from codeclash.games import get_game
108
from codeclash.games.abstract import CodeGame
119
from codeclash.tournaments.abstract import AbstractTournament
12-
from codeclash.utils.environment import create_file_on_container
1310
from codeclash.utils.log import get_logger
1411

1512

@@ -55,35 +52,20 @@ def run_training_round(self, round_num: int) -> None:
5552
round_log_path.write_text(log_output)
5653

5754
# Copy log to agent environments
58-
self._post_round_setup(self.agents, round_num, log_output)
55+
for agent in self.agents:
56+
self._copy_game_log_to_agent(agent, round_num, log_output)
5957

6058
for agent in self.agents:
6159
self.run_agent(agent, round_num)
6260

61+
self.logger.info("Round completed.")
62+
6363
def run_agent(self, agent: Player, round_num: int) -> None:
6464
"""Run a single agent for the current round."""
6565
agent.pre_run_hook(new_round=round_num)
6666
agent.run()
6767
agent.post_run_hook(round=round_num)
6868

69-
def _post_round_setup(self, agents: list, round_num: int, log_output: str) -> None:
70-
"""Copy round logs to agent environments and local directory."""
71-
for agent in agents:
72-
try:
73-
create_file_on_container(
74-
container=agent.environment,
75-
content=log_output,
76-
dest_path=f"logs/round_{round_num}.log",
77-
)
78-
except Exception:
79-
self.logger.error(
80-
f"Error creating round log in {agent.name}'s container: {traceback.format_exc()}"
81-
)
82-
else:
83-
self.logger.info(f"Created round log in {agent.name}'s container.")
84-
85-
self.logger.info("Round completed.")
86-
8769
def cleanup(self) -> None:
8870
"""Clean up game resources and push agents if requested."""
8971
self.game.end(self.cleanup_on_end)

codeclash/tournaments/single_player_training.py

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
"""
44

55
import copy
6-
import traceback
76

87
from codeclash.agents import get_agent
98
from codeclash.agents.abstract import Player
109
from codeclash.games import get_game
1110
from codeclash.games.abstract import CodeGame
1211
from codeclash.tournaments.abstract import AbstractTournament
1312
from codeclash.tournaments.utils.git_utils import filter_git_diff
14-
from codeclash.utils.environment import assert_zero_exit_code, create_file_on_container
1513
from codeclash.utils.log import get_logger
1614

1715

@@ -45,6 +43,7 @@ def run(self):
4543
try:
4644
for round_num in range(1, self.rounds + 1):
4745
self.run_training_round(round_num)
46+
self.evaluate()
4847
finally:
4948
self.cleanup()
5049

@@ -65,76 +64,65 @@ def run_training_round(self, round_num: int) -> None:
6564
round_log_path.write_text(log_output)
6665

6766
# Copy log to main agent environment only
68-
self._copy_game_log_to_agent([self.agent], round_num, log_output)
67+
self._copy_game_log_to_agent(self.agent, round_num, log_output)
6968

7069
self.run_main_agent(round_num)
71-
self.run_mirror_agent(round_num)
70+
mirror_agent_state = round_num - 1 if round_num > 1 else 0
71+
self.set_mirror_state_to_round(mirror_agent_state)
72+
73+
self.logger.info("Round completed.")
7274

7375
def run_main_agent(self, round_num: int):
7476
"""Run the main agent for the current round."""
7577
self.agent.pre_run_hook(new_round=round_num)
7678
self.agent.run()
7779
self.agent.post_run_hook(round=round_num)
7880

79-
def run_mirror_agent(self, round_num: int):
81+
def set_mirror_state_to_round(self, round_num: int):
8082
"""Update mirror agent's codebase with the main agent's changes."""
81-
if round_num == 1:
82-
self.logger.info("Skipping updating mirror agent for round 1")
83-
return
84-
85-
# Set mirror agent's codebase to the main agent's codebase of the previous round
86-
full_diff = self.agent.get_metadata()["diff"][round_num - 1]
87-
88-
full_diff = filter_git_diff(full_diff)
89-
90-
if full_diff.strip():
91-
self.logger.debug(
92-
assert_zero_exit_code(
93-
self.mirror_agent.environment.execute(
94-
"git reset --hard && git clean -fd"
95-
)
96-
)
97-
)
98-
99-
create_file_on_container(
100-
container=self.mirror_agent.environment, # type: ignore
101-
content=full_diff,
102-
dest_path="tmp_patch.txt",
103-
)
104-
105-
self.logger.info("Applying patch to mirror agent's codebase")
106-
self.logger.debug(f"Full diff: {full_diff}")
107-
108-
commands = ["git status", "git apply tmp_patch.txt", "rm -f tmp_patch.txt"]
109-
for cmd in commands:
110-
self.logger.debug(f"Executing command: {cmd}")
111-
out = assert_zero_exit_code(
112-
self.mirror_agent.environment.execute(cmd), logger=self.logger
113-
)
114-
self.logger.debug(out)
83+
if round_num == 0:
84+
full_diff = ""
11585
else:
116-
self.logger.info("No diff found for mirror agent, skipping update")
117-
118-
def _copy_game_log_to_agent(
119-
self, agents: list, round_num: int, log_output: str
120-
) -> None:
121-
"""Copy round logs to agent environments and local directory."""
122-
for agent in agents:
123-
try:
124-
create_file_on_container(
125-
container=agent.environment,
126-
content=log_output,
127-
dest_path=f"logs/round_{round_num}.log",
128-
)
129-
except Exception:
130-
self.logger.error(
131-
f"Error creating round log in {agent.name}'s container: {traceback.format_exc()}"
132-
)
133-
else:
134-
self.logger.info(f"Created round log in {agent.name}'s container.")
86+
full_diff = self.agent.get_metadata()["diff"][round_num]
87+
full_diff = filter_git_diff(full_diff)
13588

136-
self.logger.info("Round completed.")
89+
self.mirror_agent.reset_and_apply_patch(full_diff)
13790

13891
def cleanup(self):
13992
"""Clean up game resources."""
14093
self.game.end(self.cleanup_on_end)
94+
95+
def evaluate(self, n_repetitions: int = 3):
96+
"""Evaluate the agent's performance by
97+
calculating the matrix of every round against each other.
98+
"""
99+
p1 = get_agent(self.config["player"], self.config["prompts"], self.game)
100+
p1.name = "p1"
101+
p2 = get_agent(self.config["player"], self.config["prompts"], self.game)
102+
p2.name = "p2"
103+
matrix = {
104+
p1_round: {p2_round: [] for p2_round in range(0, self.rounds + 1)}
105+
for p1_round in range(0, self.rounds + 1)
106+
}
107+
for p1_round in range(0, self.rounds + 1):
108+
for p2_round in range(0, self.rounds + 1):
109+
self.logger.info(
110+
f"Evaluating agent at round {p1_round} against agent at round {p2_round}"
111+
)
112+
p1_patch = (
113+
self.agent.get_metadata()["diff"][p1_round] if p1_round > 0 else ""
114+
)
115+
p2_patch = (
116+
self.agent.get_metadata()["diff"][p2_round] if p2_round > 0 else ""
117+
)
118+
p1.reset_and_apply_patch(p1_patch)
119+
p2.reset_and_apply_patch(p2_patch)
120+
for i_repetition in range(n_repetitions):
121+
result = self.game.run_round([p1, p2])
122+
winner = result["winner"]
123+
self.logger.info(
124+
f"Round {p1_round} vs {p2_round} repetition {i_repetition} winner: {winner}"
125+
)
126+
matrix[p1_round][p2_round].append(winner)
127+
self.logger.info(f"Evaluation matrix: {matrix}")
128+
return matrix

configs/battlesnake_single_player.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
game:
22
name: BattleSnake
3-
rounds: 5
3+
rounds: 4
44
args:
55
width: 11
66
height: 11

0 commit comments

Comments
 (0)