# -*- coding: utf-8 -*- """A board agent class that can host a Gomoku game, and a function to convert the board to an image.""" from typing import Optional, Union, Sequence import numpy as np from matplotlib import pyplot as plt, patches from agentscope.message import Msg from agentscope.agents import AgentBase CURRENT_BOARD_PROMPT_TEMPLATE = """The current board is as follows: {board} {player}, it's your turn.""" NAME_BLACK = "Alice" NAME_WHITE = "Bob" # The mapping from name to piece NAME_TO_PIECE = { NAME_BLACK: "o", NAME_WHITE: "x", } EMPTY_PIECE = "0" def board2img(board: np.ndarray, save_path: str) -> str: """Convert the board to an image and save it to the specified path.""" size = board.shape[0] fig, ax = plt.subplots(figsize=(10, 10)) ax.set_xlim(0, size - 1) ax.set_ylim(0, size - 1) for i in range(size): ax.axhline(i, color="black", linewidth=1) ax.axvline(i, color="black", linewidth=1) for y in range(size): for x in range(size): if board[y, x] == NAME_TO_PIECE[NAME_BLACK]: # black player circle = patches.Circle( (x, y), 0.45, edgecolor="black", facecolor="black", zorder=10, ) ax.add_patch(circle) elif board[y, x] == NAME_TO_PIECE[NAME_WHITE]: # white player circle = patches.Circle( (x, y), 0.45, edgecolor="black", facecolor="white", zorder=10, ) ax.add_patch(circle) # Hide the axes and invert the y-axis ax.set_xticks(range(size)) ax.set_yticks(range(size)) ax.set_xticklabels(range(size)) ax.set_yticklabels(range(size)) ax.invert_yaxis() plt.savefig(save_path, bbox_inches="tight", pad_inches=0.1) plt.close(fig) # Close the figure to free memory return save_path class BoardAgent(AgentBase): """A board agent that can host a Gomoku game.""" def __init__(self, name: str) -> None: super().__init__(name=name, use_memory=False) # Init the board self.size = 15 self.board = np.full((self.size, self.size), EMPTY_PIECE) # Record the status of the game self.game_end = False def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg: if x is None: # Beginning of the game content = ( "Welcome to the Gomoku game! Black player goes first. " "Please make your move." ) else: row, col = x.content self.assert_valid_move(row, col) # change the board self.board[row, col] = NAME_TO_PIECE[x.name] # check if the game ends if self.check_draw(): content = "The game ends in a draw!" self.game_end = True else: next_player_name = ( NAME_BLACK if x.name == NAME_WHITE else NAME_WHITE ) content = CURRENT_BOARD_PROMPT_TEMPLATE.format( board=self.board2text(), player=next_player_name, ) if self.check_win(row, col, NAME_TO_PIECE[x.name]): content = f"The game ends, {x.name} wins!" self.game_end = True msg_host = Msg(self.name, content, role="assistant") self.speak(msg_host) # Note: we disable the image display here to avoid too many images # img = plt.imread(board2img(self.board, 'current_board.png')) # plt.imshow(img) # plt.axis('off') # plt.show() return msg_host def assert_valid_move(self, x: int, y: int) -> None: """Check if the move is valid.""" if not (0 <= x < self.size and 0 <= y < self.size): raise RuntimeError(f"Invalid move: {[x, y]} out of board range.") if not self.board[x, y] == EMPTY_PIECE: raise RuntimeError( f"Invalid move: {[x, y]} is already " f"occupied by {self.board[x, y]}.", ) def check_win(self, x: int, y: int, piece: str) -> bool: """Check if the player wins the game.""" xline = self._check_line(self.board[x, :], piece) yline = self._check_line(self.board[:, y], piece) diag1 = self._check_line(np.diag(self.board, y - x), piece) diag2 = self._check_line( np.diag(np.fliplr(self.board), self.size - 1 - x - y), piece, ) return xline or yline or diag1 or diag2 def check_draw(self) -> bool: """Check if the game ends in a draw.""" return np.all(self.board != EMPTY_PIECE) def board2text(self) -> str: """Convert the board to a text representation.""" return "\n".join( [ str(_)[1:-1].replace("'", "").replace(" ", "") for _ in self.board ], ) def _check_line(self, line: np.ndarray, piece: str) -> bool: """Check if the player wins in a line.""" count = 0 for i in line: if i == piece: count += 1 if count == 5: return True else: count = 0 return False