""" Endgame Detection and Special Handling Research: Nalimov/Syzygy Tablebases, Stockfish endgame evaluation """ import chess from typing import Optional class EndgameDetector: """ Detect endgame phase and apply special handling """ # Material thresholds for endgame detection ENDGAME_MATERIAL = { 'pawn_endgame': 0, # Only pawns + kings 'minor_endgame': 660, # 2 minor pieces or less 'major_endgame': 1320, # Rooks/Queens but limited material } def __init__(self): self.phase = 'middlegame' def detect_phase(self, board: chess.Board) -> str: """ Detect game phase based on material Returns: 'opening', 'middlegame', or 'endgame' """ # Count material (excluding kings) total_material = 0 piece_values = { chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9 } for piece_type in piece_values: count_white = len(board.pieces(piece_type, chess.WHITE)) count_black = len(board.pieces(piece_type, chess.BLACK)) total_material += (count_white + count_black) * piece_values[piece_type] # Phase detection if board.fullmove_number < 10: self.phase = 'opening' elif total_material <= 16: # Rough endgame threshold self.phase = 'endgame' else: self.phase = 'middlegame' return self.phase def is_known_draw(self, board: chess.Board) -> bool: """ Check for known theoretical draws Returns: True if position is known draw """ # Insufficient material if board.is_insufficient_material(): return True # Fifty-move rule if board.halfmove_clock >= 100: return True # Specific endgame draws if self._is_kxk(board): return True return False def _is_kxk(self, board: chess.Board) -> bool: """Check for King vs King (or with insufficient material)""" pieces = board.piece_map() # Count non-king pieces non_king_pieces = sum(1 for p in pieces.values() if p.piece_type != chess.KING) # K vs K if non_king_pieces == 0: return True # K+B vs K or K+N vs K (insufficient) if non_king_pieces == 1: for piece in pieces.values(): if piece.piece_type in [chess.BISHOP, chess.KNIGHT]: return True return False def adjust_evaluation(self, board: chess.Board, eval_score: float) -> float: """ Adjust evaluation based on endgame knowledge Args: board: Current position eval_score: Raw evaluation score Returns: Adjusted evaluation """ phase = self.detect_phase(board) # Known draws if self.is_known_draw(board): return 0.0 # Endgame adjustments if phase == 'endgame': # King activity bonus in endgame king_activity_bonus = self._king_activity_bonus(board) eval_score += king_activity_bonus # Pawn endgame evaluation if self._is_pawn_endgame(board): pawn_eval = self._evaluate_pawn_endgame(board) eval_score = eval_score * 0.7 + pawn_eval * 0.3 return eval_score def _king_activity_bonus(self, board: chess.Board) -> float: """ Calculate king activity bonus in endgame Active king is crucial in endgame """ bonus = 0.0 for color in [chess.WHITE, chess.BLACK]: king_sq = board.king(color) if king_sq is None: continue # Center proximity (Manhattan distance from center) rank, file = divmod(king_sq, 8) center_distance = abs(rank - 3.5) + abs(file - 3.5) # Closer to center = better activity = (7 - center_distance) * 5 if color == chess.WHITE: bonus += activity else: bonus -= activity return bonus def _is_pawn_endgame(self, board: chess.Board) -> bool: """Check if position is pure pawn endgame""" for piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]: if len(board.pieces(piece_type, chess.WHITE)) > 0: return False if len(board.pieces(piece_type, chess.BLACK)) > 0: return False return True def _evaluate_pawn_endgame(self, board: chess.Board) -> float: """ Special evaluation for pawn endgames Focus on: passed pawns, king proximity, pawn races """ eval = 0.0 # Passed pawn evaluation for color in [chess.WHITE, chess.BLACK]: for pawn_sq in board.pieces(chess.PAWN, color): if self._is_passed_pawn(board, pawn_sq, color): # Passed pawn bonus (increases closer to promotion) rank = pawn_sq // 8 if color == chess.WHITE: distance_to_promotion = 7 - rank eval += (7 - distance_to_promotion) * 20 else: distance_to_promotion = rank eval -= (7 - distance_to_promotion) * 20 return eval def _is_passed_pawn(self, board: chess.Board, pawn_sq: int, color: chess.Color) -> bool: """Check if pawn is passed (no opposing pawns ahead)""" rank, file = divmod(pawn_sq, 8) # Check files: current, left, right files_to_check = [file] if file > 0: files_to_check.append(file - 1) if file < 7: files_to_check.append(file + 1) # Check if any enemy pawns block path if color == chess.WHITE: ranks_ahead = range(rank + 1, 8) else: ranks_ahead = range(0, rank) for check_rank in ranks_ahead: for check_file in files_to_check: check_sq = check_rank * 8 + check_file piece = board.piece_at(check_sq) if piece and piece.piece_type == chess.PAWN and piece.color != color: return False return True