Rafs-an09002's picture
Create engine/endgame.py
b8f4b95 verified
"""
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