Chess_Engine / chess_engine /api /game_controller.py
electro-sb's picture
first commit
100a6dd
# 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()