1- import getpass
21import json
32import os
43import subprocess
5- import time
6- import traceback
74from abc import ABC , abstractmethod
85from collections import Counter
96from pathlib import Path
10- from typing import Any
117
128from minisweagent .environments .docker import DockerEnvironment
139
1410from codeclash .agents .abstract import Player
1511from codeclash .constants import DIR_LOGS , DIR_WORK , GH_ORG
16- from codeclash .utils .environment import (
17- assert_zero_exit_code ,
18- copy_between_containers ,
19- copy_file_from_container ,
20- )
12+ from codeclash .utils .environment import assert_zero_exit_code , copy_between_containers
2113from codeclash .utils .log import get_logger
2214
2315
2416class CodeGame (ABC ):
2517 name : str
2618
27- def __init__ (self , config : dict ):
19+ def __init__ (self , config : dict , * , tournament_id : str , local_output_dir : Path ):
20+ """The CodeGame class is responsible for running games, i.e., taking a list of code
21+ from different agents/players and running them against each other.
22+ It also provides the environments for the game and agents to run in.
23+
24+ The central method is `run_round`, which takes a list of agents and returns the winner of the round.
25+
26+ At the end of the the tournament, run the `end` method to clean up the game and agents and write the metadata.
27+ """
2828 self .url_gh : str = f"git@github.com:{ GH_ORG } /{ self .name } .git"
2929 self .artifacts : list [Path ] = []
3030 """Artifact objects that we might want to clean up after the game."""
3131 self .scoreboard : list [tuple [int , str ]] = []
3232 """List of (round number, winner (player id))"""
3333 self .game_config : dict = config ["game" ]
3434 self .config : dict = config
35- self .rounds : int = self .game_config .get ("rounds" , 1 )
36- self .round : int = 0
37- self .game_id : str = f"{ self .name } { time .strftime ('%y%m%d%H%M%S' )} "
35+ self .game_id : str = tournament_id
3836 self .log_env : Path = (DIR_WORK / DIR_LOGS / self .game_id ).resolve ()
39- self .log_local : Path = ( DIR_LOGS / getpass . getuser () / self . game_id ). resolve ()
37+ self .log_local : Path = local_output_dir
4038 self .logger = get_logger (
4139 self .name , log_path = self .log_local / "game.log" , emoji = "🏓"
4240 )
4341 self .environment : DockerEnvironment = self .get_environment ()
42+ """The running docker environment for executing the game"""
4443 # assert len(config["players"]) >= 2, "At least two players are required"
44+ """Total number of rounds to play"""
45+ self ._metadata : dict = {
46+ "name" : self .name ,
47+ "config" : self .config ,
48+ "game_id" : self .game_id ,
49+ }
4550
4651 @property
4752 def image_name (self ) -> str :
@@ -84,12 +89,7 @@ def get_metadata(self) -> dict:
8489 """This is what we write to metadata.json.
8590 You can subclass extend this to add more details for specific games.
8691 """
87- return {
88- "name" : self .name ,
89- "scoreboard" : self .scoreboard ,
90- "config" : self .config ,
91- "game_id" : self .game_id ,
92- }
92+ return self ._metadata
9393
9494 def end (self , cleanup : bool = False ):
9595 self .logger .info ("Overall score: %s" , Counter ([x [1 ] for x in self .scoreboard ]))
@@ -121,11 +121,7 @@ def get_environment(self, branch_name: str | None = None) -> DockerEnvironment:
121121 return environment
122122
123123 def _pre_round_setup (self , agents : list [Player ]):
124- """Copy agent codebases into game's container and make round log file"""
125- self .round += 1
126- # Notify agents of round update
127- self .logger .info (f"▶️ Running { self .name } round { self .round } ..." )
128-
124+ """Copy agent codebases into game's container"""
129125 # Copy agent codebases into game's container
130126 for agent in agents :
131127 self .logger .debug (f"Copying { agent .name } 's codebase" )
@@ -136,78 +132,55 @@ def _pre_round_setup(self, agents: list[Player]):
136132 dest_path = f"/{ agent .name } " ,
137133 )
138134
139- # Ensure the log path + file exists
135+ # Ensure the log directory exists
140136 assert_zero_exit_code (
141137 self .environment .execute (f"mkdir -p { self .log_env } " ),
142138 logger = self .logger ,
143139 )
144- assert_zero_exit_code (
145- self .environment .execute (f"touch { self .round_log_path } " ), logger = self .logger
146- )
147140
148141 @abstractmethod
149- def determine_winner (self , agents : list [Player ]) -> Any :
150- """Determine the winner of the game based on the round results,
151- Should update self.scoreboard
142+ def determine_winner (
143+ self , result_output : str , agents : list [Player ]
144+ ) -> dict [str , str ]:
145+ """Determine the winner of the game based on the result output.
146+
147+ Args:
148+ result_output: The specific output containing winning information
149+ agents: List of agents participating in the round
150+
151+ Returns:
152+ Dictionary with key "winner" containing the winner's name
152153 """
153154 pass
154155
155156 @abstractmethod
156- def execute_round (self , agents : list [Player ]):
157- """Subclasses implement their game-specific logic here, must write results to round_log_path .
157+ def execute_round (self , agents : list [Player ]) -> dict [ str , str ] :
158+ """Subclasses implement their game-specific logic here.
158159 This is the low level implementation, you probably want to use run_round instead, which
159160 includes the pre-round setup, post-round setup, and winner determination.
161+
162+ Returns:
163+ Dictionary with keys "log_output" and "result_output"
160164 """
161165 pass
162166
163- def _post_round_setup (self , agents : list [Player ]):
164- for agent in agents :
165- try :
166- copy_between_containers (
167- self .environment ,
168- agent .environment ,
169- self .round_log_path ,
170- f"{ agent .environment .config .cwd } /logs/round_{ self .round } .log" ,
171- )
172- except Exception :
173- self .logger .error (
174- f"Error copying round log to { agent .name } 's container: { traceback .format_exc ()} "
175- )
176- else :
177- self .logger .info (f"Copied round log to { agent .name } 's container." )
178-
179- try :
180- copy_file_from_container (
181- self .environment ,
182- self .round_log_path ,
183- self .log_local / self .round_log_path .name ,
184- )
185- except Exception :
186- self .logger .error (
187- f"Error copying round log to { agent .name } 's container: { traceback .format_exc ()} "
188- )
189- else :
190- self .logger .info (
191- f"Copied round log from { agent .name } 's container to local log dir."
192- )
193- self .logger .info (f"Round { self .round } completed." )
194-
195- def run_round (self , agents : list [Player ]):
167+ def run_round (self , agents : list [Player ]) -> dict [str , str ]:
196168 """
197169 Run a single round of the game with the given agents.
198170
199- Writes to directory containing logs and results of the round(s).
171+ Returns the log output, result output, and winner name. All bookkeeping should be
172+ handled by the tournament class.
200173 """
201174 self ._pre_round_setup (agents )
202- self .execute_round (agents )
203- self .determine_winner (agents )
204- last_winner = self .scoreboard [- 1 ][1 ]
205- self .logger .info (f"Round { self .round } winner: { last_winner } " )
206- self ._post_round_setup (agents )
175+ result = self .execute_round (agents )
176+ log_output = result ["log_output" ]
177+ result_output = result ["result_output" ]
207178
208- @property
209- def round_log_path (self ) -> Path :
210- """
211- Get the path to the current round's log file.
212- """
213- return self .log_env / f"round_{ self .round } .log"
179+ winner_result = self .determine_winner (result_output , agents )
180+ winner_name = winner_result ["winner" ]
181+
182+ return {
183+ "log_output" : log_output ,
184+ "result_output" : result_output ,
185+ "winner" : winner_name ,
186+ }
0 commit comments