| | """ |
| | 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 |