This commit is contained in:
CakeCN
2024-12-03 16:21:19 +08:00
commit 4198ca63b1
975 changed files with 333413 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
# -*- 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

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""The main script to start a Gomoku game between two agents and a board
agent."""
from board_agent import NAME_TO_PIECE, NAME_BLACK, NAME_WHITE, BoardAgent
from gomoku_agent import GomokuAgent
from agentscope import msghub
import agentscope
MAX_STEPS = 10
SYS_PROMPT_TEMPLATE = """
You're a skillful Gomoku player. You should play against your opponent according to the following rules:
Game Rules:
1. This Gomoku board is a 15*15 grid. Moves are made by specifying row and column indexes, with [0, 0] marking the top-left corner and [14, 14] indicating the bottom-right corner.
2. The goal is to be the first player to form an unbroken line of your pieces horizontally, vertically, or diagonally.
3. If the board is completely filled with pieces and no player has formed a row of five, the game is declared a draw.
Note:
1. Your pieces are represented by '{}', your opponent's by '{}'. 0 represents an empty spot on the board.
2. You should think carefully about your strategy and moves, considering both your and your opponent's subsequent moves.
3. Make sure you don't place your piece on a spot that has already been occupied.
4. Only an unbroken line of five same pieces will win the game. For example, "xxxoxx" won't be considered a win.
5. Note the unbroken line can be formed in any direction: horizontal, vertical, or diagonal.
""" # noqa
# Prepare the model configuration
YOUR_MODEL_CONFIGURATION_NAME = "{YOUR_MODEL_CONFIGURATION_NAME}"
YOUR_MODEL_CONFIGURATION = {
"config_name": YOUR_MODEL_CONFIGURATION_NAME,
# ...
}
# Initialize the agents
agentscope.init(model_configs=YOUR_MODEL_CONFIGURATION)
piece_black = NAME_TO_PIECE[NAME_BLACK]
piece_white = NAME_TO_PIECE[NAME_WHITE]
black = GomokuAgent(
NAME_BLACK,
model_config_name=YOUR_MODEL_CONFIGURATION_NAME,
sys_prompt=SYS_PROMPT_TEMPLATE.format(piece_black, piece_white),
)
white = GomokuAgent(
NAME_WHITE,
model_config_name=YOUR_MODEL_CONFIGURATION_NAME,
sys_prompt=SYS_PROMPT_TEMPLATE.format(piece_white, piece_black),
)
board = BoardAgent(name="Host")
# Start the game!
msg = None
i = 0
# Use a msg hub to share conversation between two players, e.g. white player
# can hear what black player says to the board
with msghub(participants=[black, white, board]):
while not board.game_end and i < MAX_STEPS:
for player in [black, white]:
# receive the move from the player, judge if the game ends and
# remind the player to make a move
msg = board(msg)
# end the game if draw or win
if board.game_end:
break
# make a move
msg = player(msg)
i += 1

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""A Gomoku agent that can play the game with another agent."""
from typing import Optional, Union, Sequence
import json
from agentscope.message import Msg
from agentscope.agents import AgentBase
from agentscope.models import ModelResponse
HINT_PROMPT = """
You should respond in the following format, which can be loaded by json.loads in Python:
{{
"thought": "analyze the present situation, and what move you should make",
"move": [row index, column index]
}}
""" # noqa
def parse_func(response: ModelResponse) -> ModelResponse:
"""Parse the response from the model into a dict with "move" and "thought"
keys."""
res_dict = json.loads(response.text)
if "move" in res_dict and "thought" in res_dict:
return ModelResponse(raw=res_dict)
else:
raise ValueError(
f"Invalid response format in parse_func "
f"with response: {response.text}",
)
class GomokuAgent(AgentBase):
"""A Gomoku agent that can play the game with another agent."""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
) -> None:
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
)
self.memory.add(Msg("system", sys_prompt, role="system"))
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
if self.memory:
self.memory.add(x)
msg_hint = Msg("system", HINT_PROMPT, role="system")
prompt = self.model.format(
self.memory.get_memory(),
msg_hint,
)
response = self.model(
prompt,
parse_func=parse_func,
max_retries=3,
).raw
# For better presentation, we print the response proceeded by
# json.dumps, this msg won't be recorded in memory
self.speak(
Msg(
self.name,
json.dumps(response, indent=4, ensure_ascii=False),
role="assistant",
),
)
if self.memory:
self.memory.add(Msg(self.name, response, role="assistant"))
# Hide thought from the response
return Msg(self.name, response["move"], role="assistant")