| from arena.llm import LLM | |
| from arena.board import pieces, cols | |
| import json | |
| import random | |
| import logging | |
| class Player: | |
| """ | |
| This class represents one AI player in the game, and is responsible for managing the prompts | |
| Delegating to an LLM instance to connect to the LLM | |
| """ | |
| def __init__(self, model: str, color: int): | |
| """ | |
| Set up this instance for the given model and player color | |
| """ | |
| self.color = color | |
| self.model = model | |
| self.llm = LLM.create(self.model) | |
| self.evaluation = "" | |
| self.threats = "" | |
| self.opportunities = "" | |
| self.strategy = "" | |
| def system(self, board, legal_moves: str, illegal_moves: str) -> str: | |
| """ | |
| Return the system prompt for this move | |
| """ | |
| return f"""You are playing the board game Connect 4. | |
| Players take turns to drop counters into one of 7 columns A, B, C, D, E, F, G. | |
| The winner is the first player to get 4 counters in a row in any direction. | |
| You are {pieces[self.color]} and your opponent is {pieces[self.color * -1]}. | |
| You must pick a column for your move. You must pick one of the following legal moves: {legal_moves}. | |
| You should respond in JSON according to this spec: | |
| {{ | |
| "evaluation": "my assessment of the board", | |
| "threats": "any threats from my opponent that I should block", | |
| "opportunities": "my best chances to win", | |
| "strategy": "my thought process", | |
| "move_column": "one letter from this list of legal moves: {legal_moves}" | |
| }} | |
| You must pick one of these letters for your move_column: {legal_moves}{illegal_moves}""" | |
| def user(self, board, legal_moves: str, illegal_moves: str) -> str: | |
| """ | |
| Return the user prompt for this move | |
| """ | |
| return f"""It is your turn to make a move as {pieces[self.color]}. | |
| Here is the current board, with row 1 at the bottom of the board: | |
| {board.json()} | |
| Here's another way of looking at the board visually, where R represents a red counter, Y for a yellow counter, and _ represents an empty square. | |
| {board.alternative()} | |
| Your final response should be only in JSON strictly according to this spec: | |
| {{ | |
| "evaluation": "my assessment of the board", | |
| "threats": "any threats from my opponent that I should block", | |
| "opportunities": "my best chances to win", | |
| "strategy": "my thought process", | |
| "move_column": "one of {legal_moves} which are the legal moves" | |
| }} | |
| For example, the following could be a response: | |
| {{ | |
| "evaluation": "the board is equally balanced but I have a slight advantage", | |
| "threats": "my opponent has a threat but I can block it", | |
| "opportunities": "I've developed several promising 3 in a row opportunities", | |
| "strategy": "I must first block my opponent, then I can continue to develop", | |
| "move_column": "{random.choice(board.legal_moves())}" | |
| }} | |
| And this is another example of a well formed response: | |
| {{ | |
| "evaluation": "although my opponent has more threats, I can win immediately", | |
| "threats": "my opponent has several threats", | |
| "opportunities": "I can immediately win the game by making a diagonal 4", | |
| "strategy": "I will take the winning move", | |
| "move_column": "{random.choice(board.legal_moves())}" | |
| }} | |
| Now make your decision. | |
| You must pick one of these letters for your move_column: {legal_moves}{illegal_moves} | |
| """ | |
| def process_move(self, reply: str, board): | |
| """ | |
| Interpret the reply and make the move; if the move is illegal, then the current player loses | |
| """ | |
| try: | |
| if len(reply) == 3 and reply[0] == "{" and reply[2] == "}": | |
| reply = f'{{"move_column": "{reply[1]}"}}' | |
| result = json.loads(reply) | |
| move = result.get("move_column") or "missing" | |
| move = move.upper() | |
| col = cols.find(move) | |
| if not (0 <= col <= 6) or board.height(col) == 6: | |
| raise ValueError("Illegal move") | |
| board.move(col) | |
| self.evaluation = result.get("evaluation") or "" | |
| self.threats = result.get("threats") or "" | |
| self.opportunities = result.get("opportunities") or "" | |
| self.strategy = result.get("strategy") or "" | |
| except Exception as e: | |
| logging.error(f"Exception {e}") | |
| logging.exception(e) | |
| board.forfeit = True | |
| board.winner = -1 * board.player | |
| def move(self, board): | |
| """ | |
| Have the underlying LLM make a move, and process the result | |
| """ | |
| legal_moves = ", ".join(board.legal_moves()) | |
| if illegal := board.illegal_moves(): | |
| illegal_moves = ( | |
| "\nYou must NOT make any of these moves which are ILLEGAL: " | |
| + ", ".join(illegal) | |
| ) | |
| else: | |
| illegal_moves = "" | |
| system = self.system(board, legal_moves, illegal_moves) | |
| user = self.user(board, legal_moves, illegal_moves) | |
| reply = self.llm.send(system, user) | |
| self.process_move(reply, board) | |
| def thoughts(self): | |
| """ | |
| Return HTML to describe the inner thoughts | |
| """ | |
| result = '<div style="text-align: left;font-size:14px"><br/>' | |
| result += f"<b>Evaluation:</b><br/>{self.evaluation}<br/><br/>" | |
| result += f"<b>Threats:</b><br/>{self.threats}<br/><br/>" | |
| result += f"<b>Opportunities:</b><br/>{self.opportunities}<br/><br/>" | |
| result += f"<b>Strategy:</b><br/>{self.strategy}" | |
| result += "</div>" | |
| return result | |
| def switch_model(self, new_model_name: str): | |
| """ | |
| Change the underlying LLM to the new model | |
| """ | |
| self.llm = LLM.create(new_model_name) | |