# chess_engine/api/game_controller.py import chess from typing import Dict, List, Optional, Tuple, Any from enum import Enum from dataclasses import dataclass from chess_engine.board import ChessBoard, MoveResult, GameState from chess_engine.ai.stockfish_wrapper import StockfishWrapper, DifficultyLevel from chess_engine.ai.evaluation import ChessEvaluator from chess_engine.promotion import PromotionMoveHandler @dataclass class GameOptions: """Game configuration options""" player_color: chess.Color = chess.WHITE difficulty: DifficultyLevel = DifficultyLevel.MEDIUM time_limit: float = 1.0 # seconds for AI to think use_opening_book: bool = True enable_analysis: bool = True stockfish_path: Optional[str] = None class GameController: """ Main game controller that handles game flow, user moves, and AI responses """ def __init__(self, options: Optional[GameOptions] = None): """ Initialize game controller Args: options: Game configuration options """ self.options = options or GameOptions() self.board = ChessBoard() self.engine = StockfishWrapper(stockfish_path=self.options.stockfish_path) self.evaluator = ChessEvaluator() self.game_active = False self.last_analysis = None def start_new_game(self, options: Optional[GameOptions] = None) -> Dict[str, Any]: """ Start a new game Args: options: Game options (optional, uses current options if None) Returns: Game state information """ if options: self.options = options # Reset board self.board.reset_board() # Initialize engine if not self.engine.is_initialized: engine_initialized = self.engine.initialize() if not engine_initialized: return { "status": "error", "message": "Failed to initialize chess engine", "board_state": self.board.get_board_state() } # Set difficulty self.engine.set_difficulty_level(self.options.difficulty) self.game_active = True # If AI plays white, make first move if self.options.player_color == chess.BLACK: ai_move = self._get_ai_move() if ai_move: self.board.make_move(ai_move) return { "status": "success", "message": "New game started", "board_state": self.board.get_board_state(), "player_color": "white" if self.options.player_color == chess.WHITE else "black" } def make_player_move(self, move_str: str) -> Dict[str, Any]: """ Process a player's move Args: move_str: Move in UCI notation (e.g., 'e2e4', 'e7e8q') or SAN (e.g., 'e4', 'e8=Q') Returns: Response with move result and updated game state """ if not self.game_active: return { "status": "error", "message": "No active game", "board_state": self.board.get_board_state() } # Check if it's player's turn if self.board.board.turn != self.options.player_color: return { "status": "error", "message": "Not your turn", "board_state": self.board.get_board_state() } # Make the move result, move_info = self.board.make_move(move_str) # Handle promotion requirement if result == MoveResult.INVALID and move_info and move_info.promotion_required: return { "status": "promotion_required", "message": "Pawn promotion requires piece selection", "promotion_details": { "from": move_info.uci[:2], "to": move_info.uci[2:4], "available_pieces": ["queen", "rook", "bishop", "knight"] }, "board_state": self.board.get_board_state() } if result != MoveResult.VALID: return { "status": "error", "message": f"Invalid move: {move_str}", "board_state": self.board.get_board_state() } # Check if game is over after player's move game_state = self._check_game_state() if game_state: return game_state # Make AI move ai_move = self._get_ai_move() if ai_move: self.board.make_move(ai_move) # Check if game is over after AI's move game_state = self._check_game_state() if game_state: return game_state # Analyze position if enabled analysis = None if self.options.enable_analysis: analysis = self._analyze_position() self.last_analysis = analysis return { "status": "success", "player_move": move_str, "ai_move": ai_move, "board_state": self.board.get_board_state(), "analysis": analysis } def _get_ai_move(self) -> Optional[str]: """ Get AI's move using Stockfish Returns: Move in UCI notation or None if error """ return self.engine.get_best_move( self.board.board, time_limit=self.options.time_limit ) def _check_game_state(self) -> Optional[Dict[str, Any]]: """ Check if game is over Returns: Game result dict if game is over, None otherwise """ board_state = self.board.get_board_state() game_state = board_state["game_state"] if game_state in [GameState.CHECKMATE, GameState.STALEMATE, GameState.DRAW]: self.game_active = False result = "draw" winner = None if game_state == GameState.CHECKMATE: # Winner is the opposite of who's turn it is winner = "black" if self.board.board.turn == chess.WHITE else "white" result = f"{winner}_win" return { "status": "game_over", "result": result, "winner": winner, "reason": game_state.value, "board_state": board_state } return None def _analyze_position(self) -> Dict[str, Any]: """ Analyze current position Returns: Analysis data """ # Get evaluation from our evaluator evaluation = self.evaluator.evaluate_position(self.board.board) # Get Stockfish evaluation stockfish_eval = self.engine.evaluate_position(self.board.board) # Get best moves with evaluation best_moves = self.engine.get_legal_moves_with_evaluation(self.board.board)[:3] return { "evaluation": { "total": evaluation.total_score / 100, # Convert to pawns "material": evaluation.material_score / 100, "positional": evaluation.positional_score / 100, "safety": evaluation.safety_score / 100, "mobility": evaluation.mobility_score / 100, "pawn_structure": evaluation.pawn_structure_score / 100, "endgame": evaluation.endgame_score / 100, "white_advantage": evaluation.white_advantage / 100, "stockfish": stockfish_eval }, "best_moves": best_moves } def get_hint(self) -> Dict[str, Any]: """ Get a hint for the current position Returns: Hint information """ if not self.game_active: return { "status": "error", "message": "No active game" } # Get best move from engine best_move = self._get_ai_move() if not best_move: return { "status": "error", "message": "Could not generate hint" } # Get move explanation explanation = self._get_move_explanation(best_move) return { "status": "success", "hint": best_move, "explanation": explanation, "board_state": self.board.get_board_state() } def _get_move_explanation(self, move_str: str) -> str: """ Generate a simple explanation for a move Args: move_str: Move in UCI notation Returns: Human-readable explanation """ try: move = chess.Move.from_uci(move_str) board = self.board.board from_square = chess.square_name(move.from_square) to_square = chess.square_name(move.to_square) piece = board.piece_at(move.from_square) if not piece: return "Unknown move" piece_name = chess.piece_name(piece.piece_type).capitalize() # Check if move is a capture capture = "" if board.is_capture(move): captured_piece = board.piece_at(move.to_square) if captured_piece: captured_name = chess.piece_name(captured_piece.piece_type).capitalize() capture = f", capturing {captured_name}" else: # En passant capture = ", capturing Pawn en passant" # Check if move gives check check = "" if board.gives_check(move): check = ", giving check" # Check if castling if board.is_castling(move): if chess.square_file(move.to_square) > chess.square_file(move.from_square): return "Kingside castling" else: return "Queenside castling" # Check if promotion promotion = "" if move.promotion: promoted_piece = chess.piece_name(move.promotion).capitalize() promotion = f", promoting to {promoted_piece}" elif piece.piece_type == chess.PAWN and self.board.is_promotion_move(from_square, to_square): # This is a promotion move but piece not specified promotion = ", promotion required" return f"{piece_name} from {from_square} to {to_square}{capture}{promotion}{check}" except Exception: return "Move analysis not available" def undo_move(self, count: int = 2) -> Dict[str, Any]: """ Undo moves (both player and AI) Args: count: Number of half-moves to undo (default 2 for one full move) Returns: Updated game state """ if not self.game_active: return { "status": "error", "message": "No active game", "board_state": self.board.get_board_state() } success = True for _ in range(count): if not self.board.undo_move(): success = False break return { "status": "success" if success else "partial", "message": f"Undid {count} moves" if success else "Could not undo all requested moves", "board_state": self.board.get_board_state() } def resign(self) -> Dict[str, Any]: """ Resign the current game Returns: Game result """ if not self.game_active: return { "status": "error", "message": "No active game" } self.game_active = False winner = "black" if self.options.player_color == chess.WHITE else "white" return { "status": "game_over", "result": f"{winner}_win", "winner": winner, "reason": "resignation", "board_state": self.board.get_board_state() } def get_game_state(self) -> Dict[str, Any]: """ Get current game state Returns: Game state information """ board_state = self.board.get_board_state() return { "status": "active" if self.game_active else "inactive", "player_color": "white" if self.options.player_color == chess.WHITE else "black", "board_state": board_state, "difficulty": self.options.difficulty.name, "last_analysis": self.last_analysis } def close(self): """Clean up resources""" if self.engine: self.engine.close()