174 lines
5.5 KiB
Python
174 lines
5.5 KiB
Python
# -*- 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
|