diff --git a/reginaldCog/chess_addon.py b/reginaldCog/chess_addon.py new file mode 100644 index 0000000..402f017 --- /dev/null +++ b/reginaldCog/chess_addon.py @@ -0,0 +1,93 @@ +import chess +import chess.svg +import io +from cairosvg import svg2png +import discord + +class ChessHandler: + def __init__(self): + self.active_games = {} # {user_id: FEN string} + + def set_board(self, user_id: str, fen: str): + """Sets a board to a given FEN string for a user.""" + try: + board = chess.Board(fen) # Validate FEN + self.active_games[user_id] = fen + return f"Board state updated successfully:\n```{fen}```" + except ValueError: + return "⚠️ Invalid FEN format. Please provide a valid board state." + + def reset_board(self, user_id: str): + """Resets a user's board to the standard starting position.""" + self.active_games[user_id] = chess.STARTING_FEN + return "The board has been reset to the standard starting position." + + def get_board(self, user_id: str): + """Returns a chess.Board() instance based on stored FEN.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + return chess.Board(fen) + + def get_fen(self, user_id: str): + """Returns the current FEN of the user's board.""" + return self.active_games.get(user_id, chess.STARTING_FEN) + + def make_move(self, user_id: str, move: str): + """Attempts to execute a move and checks if the game is over.""" + board = self.get_board(user_id) + + try: + board.push_san(move) # Execute move in standard algebraic notation + self.active_games[user_id] = board.fen() # Store FEN string instead of raw board object + + if board.is_checkmate(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Checkmate!** 🎉" + elif board.is_stalemate(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Stalemate!** 🤝" + elif board.is_insufficient_material(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw due to insufficient material.**" + elif board.can_claim_threefold_repetition(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw by threefold repetition.**" + elif board.can_claim_fifty_moves(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw by 50-move rule.**" + + return f"Move executed: `{move}`.\nCurrent FEN:\n```{board.fen()}```" + except ValueError: + return "⚠️ Invalid move. Please enter a legal chess move." + + def resign(self, user_id: str): + """Handles player resignation.""" + if user_id in self.active_games: + del self.active_games[user_id] + return "**You have resigned. Well played!** 🏳️" + return "No active game to resign from." + + def get_board_state_text(self, user_id: str): + """Returns the current FEN as a message.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + return f"Current board state (FEN):\n```{fen}```" + + def get_board_state_image(self, user_id: str): + """Generates a chessboard image from the current FEN and returns it as a file.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + board = chess.Board(fen) + + try: + # Generate SVG representation of the board + svg_data = chess.svg.board(board) + + # Convert SVG to PNG using cairosvg + png_data = svg2png(bytestring=svg_data) + + # Store PNG in memory + image_file = io.BytesIO(png_data) + image_file.seek(0) # Ensure file pointer is reset before returning + + return discord.File(image_file, filename="chessboard.png") + + except Exception as e: + return f"⚠️ Error generating board image: {str(e)}" diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index d486ae1..290aed8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,6 +8,9 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError +from .chess_addon import ChessHandler + +chess_handler = ChessHandler() class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -271,24 +274,91 @@ class ReginaldCog(commands.Cog): return None # No strong fact found - async def generate_response(self, api_key, messages): + async def generate_response(self, api_key, messages, ctx): + """Handles AI responses and function calling for chess interactions.""" + model = await self.config.openai_model() + try: client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model=model, messages=messages, - max_tokens=4112, + max_tokens=1500, # Balanced token limit to allow function execution & flavor text temperature=0.7, presence_penalty=0.5, - frequency_penalty=0.5 + frequency_penalty=0.5, + functions=[ + { + "name": "set_board", + "description": "Sets up the chessboard to a given FEN string.", + "parameters": { + "type": "object", + "properties": { + "fen": {"type": "string", "description": "The FEN string representing the board state."} + }, + "required": ["fen"] + } + }, + { + "name": "make_move", + "description": "Executes a chess move for the current game.", + "parameters": { + "type": "object", + "properties": { + "move": {"type": "string", "description": "The move in SAN format (e.g., 'e2e4')."} + }, + "required": ["move"] + } + }, + { + "name": "reset_board", + "description": "Resets the chessboard to the default starting position.", + "parameters": {} + }, + { + "name": "resign", + "description": "Resigns from the current chess game.", + "parameters": {} + }, + { + "name": "get_board_state_text", + "description": "Retrieves the current board state as a FEN string.", + "parameters": { + "type": "object", + "properties": { + "user_id": {"type": "string", "description": "The user's unique ID."} + }, + "required": ["user_id"] + } + } + ] ) - response_text = response.choices[0].message.content.strip() - if response_text.startswith("Reginald:"): - response_text = response_text[len("Reginald:"):].strip() - return response_text - except OpenAIError as e: + response_data = response.choices[0].message + + # 🟢 Check if OpenAI returned a function call + if response_data.get("function_call"): + function_call = response_data["function_call"] + function_name = function_call["name"] + function_args = json.loads(function_call["arguments"]) # Convert JSON string to dict + + # 🟢 Call the appropriate function + if function_name == "set_board": + return chess_handler.set_board(ctx.author.id, function_args["fen"]) + elif function_name == "make_move": + return chess_handler.make_move(ctx.author.id, function_args["move"]) + elif function_name == "reset_board": + return chess_handler.reset_board(ctx.author.id) + elif function_name == "resign": + return chess_handler.resign(ctx.author.id) + elif function_name == "get_board_state_text": + return chess_handler.get_fen(ctx.author.id) # Returns FEN string of the board + + # 🟢 If no function was called, return AI-generated response with flavor text + return response_data.get("content", "I'm afraid I have nothing to say.") + + except openai.OpenAIError as e: error_message = f"OpenAI Error: {e}" reginald_responses = [ f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```",