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