|
| 1 | +import re |
| 2 | + |
| 3 | +from codeclash.agents.player import Player |
| 4 | +from codeclash.arenas.arena import CodeArena, RoundStats |
| 5 | +from codeclash.constants import RESULT_TIE |
| 6 | +from codeclash.utils.environment import assert_zero_exit_code |
| 7 | + |
| 8 | +GOMOKU_LOG = "result.log" |
| 9 | + |
| 10 | + |
| 11 | +class GomokuArena(CodeArena): |
| 12 | + name: str = "Gomoku" |
| 13 | + submission: str = "main.py" |
| 14 | + description: str = """Your bot (`main.py`) controls a Gomoku player on a 15x15 board. |
| 15 | +Players take turns placing stones. Win by connecting 5 stones in a row (horizontally, vertically, or diagonally). |
| 16 | +Black plays first. |
| 17 | +
|
| 18 | +Your bot must implement: |
| 19 | + def get_move(board: list[list[int]], color: str) -> tuple[int, int] |
| 20 | +
|
| 21 | +Board representation: 0=empty, 1=black, 2=white |
| 22 | +Color: "black" or "white" |
| 23 | +""" |
| 24 | + |
| 25 | + def __init__(self, config, **kwargs): |
| 26 | + super().__init__(config, **kwargs) |
| 27 | + assert len(config["players"]) == 2, "Gomoku is a two-player game" |
| 28 | + |
| 29 | + def execute_round(self, agents: list[Player]) -> None: |
| 30 | + args = [f"/{agent.name}/{self.submission}" for agent in agents] |
| 31 | + cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / GOMOKU_LOG};" |
| 32 | + self.logger.info(f"Running game: {cmd}") |
| 33 | + assert_zero_exit_code(self.environment.execute(cmd)) |
| 34 | + |
| 35 | + def get_results(self, agents: list[Player], round_num: int, stats: RoundStats): |
| 36 | + with open(self.log_round(round_num) / GOMOKU_LOG) as f: |
| 37 | + round_log = f.read() |
| 38 | + lines = round_log.split("FINAL_RESULTS")[-1].splitlines() |
| 39 | + |
| 40 | + scores = {} |
| 41 | + for line in lines: |
| 42 | + match = re.search(r"Bot\_(\d)\_main:\s(\d+)\srounds\swon", line) |
| 43 | + if match: |
| 44 | + bot_id = match.group(1) |
| 45 | + rounds_won = int(match.group(2)) |
| 46 | + scores[agents[int(bot_id) - 1].name] = rounds_won |
| 47 | + |
| 48 | + # Handle draws |
| 49 | + draw_match = re.search(r"Draws:\s(\d+)", round_log) |
| 50 | + if draw_match: |
| 51 | + draws = int(draw_match.group(1)) |
| 52 | + if draws > 0: |
| 53 | + scores[RESULT_TIE] = draws |
| 54 | + |
| 55 | + stats.winner = max(scores, key=scores.get) if scores else "unknown" |
| 56 | + # Check for tie (equal scores) |
| 57 | + if scores: |
| 58 | + max_score = max(scores.values()) |
| 59 | + winners_with_max = [k for k, v in scores.items() if v == max_score and k != RESULT_TIE] |
| 60 | + if len(winners_with_max) > 1: |
| 61 | + stats.winner = RESULT_TIE |
| 62 | + |
| 63 | + stats.scores = scores |
| 64 | + for player, score in scores.items(): |
| 65 | + if player != RESULT_TIE: |
| 66 | + stats.player_stats[player].score = score |
| 67 | + |
| 68 | + def validate_code(self, agent: Player) -> tuple[bool, str | None]: |
| 69 | + if self.submission not in agent.environment.execute("ls")["output"]: |
| 70 | + return False, f"No {self.submission} file found in the root directory" |
| 71 | + |
| 72 | + bot_content = agent.environment.execute(f"cat {self.submission}")["output"] |
| 73 | + |
| 74 | + if "def get_move(" not in bot_content: |
| 75 | + return ( |
| 76 | + False, |
| 77 | + f"{self.submission} must define a get_move(board, color) function. " |
| 78 | + "See the game description for the required signature." |
| 79 | + ) |
| 80 | + |
| 81 | + return True, None |
0 commit comments