| """ |
| 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 |
| """ |
| |
| |
| ENDGAME_MATERIAL = { |
| 'pawn_endgame': 0, |
| 'minor_endgame': 660, |
| 'major_endgame': 1320, |
| } |
| |
| 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' |
| """ |
| |
| 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] |
| |
| |
| if board.fullmove_number < 10: |
| self.phase = 'opening' |
| elif total_material <= 16: |
| 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 |
| """ |
| |
| if board.is_insufficient_material(): |
| return True |
| |
| |
| if board.halfmove_clock >= 100: |
| return True |
| |
| |
| 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() |
| |
| |
| non_king_pieces = sum(1 for p in pieces.values() if p.piece_type != chess.KING) |
| |
| |
| if non_king_pieces == 0: |
| return True |
| |
| |
| 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) |
| |
| |
| if self.is_known_draw(board): |
| return 0.0 |
| |
| |
| if phase == 'endgame': |
| |
| king_activity_bonus = self._king_activity_bonus(board) |
| eval_score += king_activity_bonus |
| |
| |
| 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 |
| |
| |
| rank, file = divmod(king_sq, 8) |
| center_distance = abs(rank - 3.5) + abs(file - 3.5) |
| |
| |
| 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 |
| |
| |
| 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): |
| |
| 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) |
| |
| |
| files_to_check = [file] |
| if file > 0: |
| files_to_check.append(file - 1) |
| if file < 7: |
| files_to_check.append(file + 1) |
| |
| |
| 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 |