Spaces:
Sleeping
Sleeping
| # chess_engine/ai/evaluation.py | |
| import chess | |
| from typing import Dict, List, Tuple, Optional | |
| from dataclasses import dataclass | |
| import math | |
| class PositionEvaluation: | |
| """Complete position evaluation data""" | |
| total_score: float | |
| material_score: float | |
| positional_score: float | |
| safety_score: float | |
| mobility_score: float | |
| pawn_structure_score: float | |
| endgame_score: float | |
| white_advantage: float | |
| evaluation_breakdown: Dict[str, float] | |
| class ChessEvaluator: | |
| """ | |
| Chess position evaluator with various strategic factors | |
| """ | |
| # Piece values in centipawns | |
| PIECE_VALUES = { | |
| chess.PAWN: 100, | |
| chess.KNIGHT: 320, | |
| chess.BISHOP: 330, | |
| chess.ROOK: 500, | |
| chess.QUEEN: 900, | |
| chess.KING: 20000 | |
| } | |
| # Piece-square tables for positional evaluation | |
| PAWN_TABLE = [ | |
| [0, 0, 0, 0, 0, 0, 0, 0], | |
| [50, 50, 50, 50, 50, 50, 50, 50], | |
| [10, 10, 20, 30, 30, 20, 10, 10], | |
| [5, 5, 10, 25, 25, 10, 5, 5], | |
| [0, 0, 0, 20, 20, 0, 0, 0], | |
| [5, -5,-10, 0, 0,-10, -5, 5], | |
| [5, 10, 10,-20,-20, 10, 10, 5], | |
| [0, 0, 0, 0, 0, 0, 0, 0] | |
| ] | |
| KNIGHT_TABLE = [ | |
| [-50,-40,-30,-30,-30,-30,-40,-50], | |
| [-40,-20, 0, 0, 0, 0,-20,-40], | |
| [-30, 0, 10, 15, 15, 10, 0,-30], | |
| [-30, 5, 15, 20, 20, 15, 5,-30], | |
| [-30, 0, 15, 20, 20, 15, 0,-30], | |
| [-30, 5, 10, 15, 15, 10, 5,-30], | |
| [-40,-20, 0, 5, 5, 0,-20,-40], | |
| [-50,-40,-30,-30,-30,-30,-40,-50] | |
| ] | |
| BISHOP_TABLE = [ | |
| [-20,-10,-10,-10,-10,-10,-10,-20], | |
| [-10, 0, 0, 0, 0, 0, 0,-10], | |
| [-10, 0, 5, 10, 10, 5, 0,-10], | |
| [-10, 5, 5, 10, 10, 5, 5,-10], | |
| [-10, 0, 10, 10, 10, 10, 0,-10], | |
| [-10, 10, 10, 10, 10, 10, 10,-10], | |
| [-10, 5, 0, 0, 0, 0, 5,-10], | |
| [-20,-10,-10,-10,-10,-10,-10,-20] | |
| ] | |
| ROOK_TABLE = [ | |
| [0, 0, 0, 0, 0, 0, 0, 0], | |
| [5, 10, 10, 10, 10, 10, 10, 5], | |
| [-5, 0, 0, 0, 0, 0, 0, -5], | |
| [-5, 0, 0, 0, 0, 0, 0, -5], | |
| [-5, 0, 0, 0, 0, 0, 0, -5], | |
| [-5, 0, 0, 0, 0, 0, 0, -5], | |
| [-5, 0, 0, 0, 0, 0, 0, -5], | |
| [0, 0, 0, 5, 5, 0, 0, 0] | |
| ] | |
| QUEEN_TABLE = [ | |
| [-20,-10,-10, -5, -5,-10,-10,-20], | |
| [-10, 0, 0, 0, 0, 0, 0,-10], | |
| [-10, 0, 5, 5, 5, 5, 0,-10], | |
| [-5, 0, 5, 5, 5, 5, 0, -5], | |
| [0, 0, 5, 5, 5, 5, 0, -5], | |
| [-10, 5, 5, 5, 5, 5, 0,-10], | |
| [-10, 0, 5, 0, 0, 0, 0,-10], | |
| [-20,-10,-10, -5, -5,-10,-10,-20] | |
| ] | |
| KING_TABLE_MIDDLEGAME = [ | |
| [-30,-40,-40,-50,-50,-40,-40,-30], | |
| [-30,-40,-40,-50,-50,-40,-40,-30], | |
| [-30,-40,-40,-50,-50,-40,-40,-30], | |
| [-30,-40,-40,-50,-50,-40,-40,-30], | |
| [-20,-30,-30,-40,-40,-30,-30,-20], | |
| [-10,-20,-20,-20,-20,-20,-20,-10], | |
| [20, 20, 0, 0, 0, 0, 20, 20], | |
| [20, 30, 10, 0, 0, 10, 30, 20] | |
| ] | |
| KING_TABLE_ENDGAME = [ | |
| [-50,-40,-30,-20,-20,-30,-40,-50], | |
| [-30,-20,-10, 0, 0,-10,-20,-30], | |
| [-30,-10, 20, 30, 30, 20,-10,-30], | |
| [-30,-10, 30, 40, 40, 30,-10,-30], | |
| [-30,-10, 30, 40, 40, 30,-10,-30], | |
| [-30,-10, 20, 30, 30, 20,-10,-30], | |
| [-30,-30, 0, 0, 0, 0,-30,-30], | |
| [-50,-30,-30,-30,-30,-30,-30,-50] | |
| ] | |
| def __init__(self): | |
| """Initialize the evaluator""" | |
| self.piece_square_tables = { | |
| chess.PAWN: self.PAWN_TABLE, | |
| chess.KNIGHT: self.KNIGHT_TABLE, | |
| chess.BISHOP: self.BISHOP_TABLE, | |
| chess.ROOK: self.ROOK_TABLE, | |
| chess.QUEEN: self.QUEEN_TABLE, | |
| chess.KING: self.KING_TABLE_MIDDLEGAME | |
| } | |
| def evaluate_position(self, board: chess.Board) -> PositionEvaluation: | |
| """ | |
| Comprehensive position evaluation | |
| Args: | |
| board: Chess board to evaluate | |
| Returns: | |
| PositionEvaluation object with detailed analysis | |
| """ | |
| if board.is_checkmate(): | |
| score = -20000 if board.turn == chess.WHITE else 20000 | |
| return PositionEvaluation( | |
| total_score=score, | |
| material_score=score, | |
| positional_score=0, | |
| safety_score=0, | |
| mobility_score=0, | |
| pawn_structure_score=0, | |
| endgame_score=0, | |
| white_advantage=score, | |
| evaluation_breakdown={"checkmate": score} | |
| ) | |
| if board.is_stalemate() or board.is_insufficient_material(): | |
| return PositionEvaluation( | |
| total_score=0, | |
| material_score=0, | |
| positional_score=0, | |
| safety_score=0, | |
| mobility_score=0, | |
| pawn_structure_score=0, | |
| endgame_score=0, | |
| white_advantage=0, | |
| evaluation_breakdown={"draw": 0} | |
| ) | |
| # Calculate individual evaluation components | |
| material_score = self._evaluate_material(board) | |
| positional_score = self._evaluate_position_tables(board) | |
| safety_score = self._evaluate_king_safety(board) | |
| mobility_score = self._evaluate_mobility(board) | |
| pawn_structure_score = self._evaluate_pawn_structure(board) | |
| endgame_score = self._evaluate_endgame_factors(board) | |
| # Combine scores | |
| total_score = ( | |
| material_score + | |
| positional_score + | |
| safety_score + | |
| mobility_score + | |
| pawn_structure_score + | |
| endgame_score | |
| ) | |
| # Create breakdown | |
| breakdown = { | |
| "material": material_score, | |
| "positional": positional_score, | |
| "safety": safety_score, | |
| "mobility": mobility_score, | |
| "pawn_structure": pawn_structure_score, | |
| "endgame": endgame_score | |
| } | |
| return PositionEvaluation( | |
| total_score=total_score, | |
| material_score=material_score, | |
| positional_score=positional_score, | |
| safety_score=safety_score, | |
| mobility_score=mobility_score, | |
| pawn_structure_score=pawn_structure_score, | |
| endgame_score=endgame_score, | |
| white_advantage=total_score, | |
| evaluation_breakdown=breakdown | |
| ) | |
| def _evaluate_material(self, board: chess.Board) -> float: | |
| """Evaluate material balance""" | |
| score = 0 | |
| for square in chess.SQUARES: | |
| piece = board.piece_at(square) | |
| if piece: | |
| value = self.PIECE_VALUES[piece.piece_type] | |
| score += value if piece.color == chess.WHITE else -value | |
| return score | |
| def _evaluate_position_tables(self, board: chess.Board) -> float: | |
| """Evaluate piece positions using piece-square tables""" | |
| score = 0 | |
| is_endgame = self._is_endgame(board) | |
| for square in chess.SQUARES: | |
| piece = board.piece_at(square) | |
| if piece: | |
| rank = chess.square_rank(square) | |
| file = chess.square_file(square) | |
| # Choose appropriate table for king | |
| if piece.piece_type == chess.KING: | |
| table = self.KING_TABLE_ENDGAME if is_endgame else self.KING_TABLE_MIDDLEGAME | |
| else: | |
| table = self.piece_square_tables[piece.piece_type] | |
| # Flip table for black pieces | |
| if piece.color == chess.WHITE: | |
| value = table[rank][file] | |
| else: | |
| value = -table[7-rank][file] | |
| score += value | |
| return score | |
| def _evaluate_king_safety(self, board: chess.Board) -> float: | |
| """Evaluate king safety""" | |
| score = 0 | |
| # Check for king exposure | |
| for color in [chess.WHITE, chess.BLACK]: | |
| king_square = board.king(color) | |
| if king_square is None: | |
| continue | |
| # Count attackers around king | |
| attackers = 0 | |
| defenders = 0 | |
| for square in chess.SQUARES: | |
| if chess.square_distance(king_square, square) <= 2: | |
| if board.is_attacked_by(not color, square): | |
| attackers += 1 | |
| if board.is_attacked_by(color, square): | |
| defenders += 1 | |
| safety = (defenders - attackers) * 10 | |
| score += safety if color == chess.WHITE else -safety | |
| return score | |
| def _evaluate_mobility(self, board: chess.Board) -> float: | |
| """Evaluate piece mobility""" | |
| white_mobility = len(list(board.legal_moves)) if board.turn == chess.WHITE else 0 | |
| # Switch turn to count black mobility | |
| board.turn = not board.turn | |
| black_mobility = len(list(board.legal_moves)) if board.turn == chess.BLACK else 0 | |
| board.turn = not board.turn # Switch back | |
| return (white_mobility - black_mobility) * 1.5 | |
| def _evaluate_pawn_structure(self, board: chess.Board) -> float: | |
| """Evaluate pawn structure""" | |
| score = 0 | |
| # Get pawn positions | |
| white_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and | |
| board.piece_at(sq).piece_type == chess.PAWN and | |
| board.piece_at(sq).color == chess.WHITE] | |
| black_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and | |
| board.piece_at(sq).piece_type == chess.PAWN and | |
| board.piece_at(sq).color == chess.BLACK] | |
| # Evaluate doubled pawns | |
| score += self._evaluate_doubled_pawns(white_pawns, chess.WHITE) | |
| score += self._evaluate_doubled_pawns(black_pawns, chess.BLACK) | |
| # Evaluate isolated pawns | |
| score += self._evaluate_isolated_pawns(white_pawns, chess.WHITE) | |
| score += self._evaluate_isolated_pawns(black_pawns, chess.BLACK) | |
| # Evaluate passed pawns | |
| score += self._evaluate_passed_pawns(board, white_pawns, chess.WHITE) | |
| score += self._evaluate_passed_pawns(board, black_pawns, chess.BLACK) | |
| return score | |
| def _evaluate_doubled_pawns(self, pawns: List[int], color: chess.Color) -> float: | |
| """Evaluate doubled pawns penalty""" | |
| files = {} | |
| for pawn in pawns: | |
| file = chess.square_file(pawn) | |
| files[file] = files.get(file, 0) + 1 | |
| doubled_count = sum(max(0, count - 1) for count in files.values()) | |
| penalty = doubled_count * -20 | |
| return penalty if color == chess.WHITE else -penalty | |
| def _evaluate_isolated_pawns(self, pawns: List[int], color: chess.Color) -> float: | |
| """Evaluate isolated pawns penalty""" | |
| files = set(chess.square_file(pawn) for pawn in pawns) | |
| isolated_count = 0 | |
| for file in files: | |
| if (file - 1 not in files) and (file + 1 not in files): | |
| isolated_count += 1 | |
| penalty = isolated_count * -15 | |
| return penalty if color == chess.WHITE else -penalty | |
| def _evaluate_passed_pawns(self, board: chess.Board, pawns: List[int], color: chess.Color) -> float: | |
| """Evaluate passed pawns bonus""" | |
| bonus = 0 | |
| opponent_color = not color | |
| for pawn in pawns: | |
| file = chess.square_file(pawn) | |
| rank = chess.square_rank(pawn) | |
| # Check if pawn is passed | |
| is_passed = True | |
| direction = 1 if color == chess.WHITE else -1 | |
| # Check files that could block this pawn | |
| for check_file in [file - 1, file, file + 1]: | |
| if 0 <= check_file <= 7: | |
| for check_rank in range(rank + direction, 8 if color == chess.WHITE else -1, direction): | |
| if 0 <= check_rank <= 7: | |
| square = chess.square(check_file, check_rank) | |
| piece = board.piece_at(square) | |
| if piece and piece.piece_type == chess.PAWN and piece.color == opponent_color: | |
| is_passed = False | |
| break | |
| if not is_passed: | |
| break | |
| if is_passed: | |
| # Bonus increases with advancement | |
| advancement = rank if color == chess.WHITE else 7 - rank | |
| bonus += advancement * 10 | |
| return bonus if color == chess.WHITE else -bonus | |
| def _is_endgame(self, board: chess.Board) -> bool: | |
| """ | |
| Determine if the position is in the endgame | |
| Args: | |
| board: Chess board to evaluate | |
| Returns: | |
| True if position is in endgame, False otherwise | |
| """ | |
| # Count major pieces (queens and rooks) | |
| queens = 0 | |
| rooks = 0 | |
| total_material = 0 | |
| for square in chess.SQUARES: | |
| piece = board.piece_at(square) | |
| if piece: | |
| if piece.piece_type == chess.QUEEN: | |
| queens += 1 | |
| elif piece.piece_type == chess.ROOK: | |
| rooks += 1 | |
| total_material += self.PIECE_VALUES[piece.piece_type] | |
| # Endgame conditions: | |
| # 1. No queens | |
| # 2. Only one queen total and no other major pieces | |
| # 3. Less than 25% of starting material | |
| return (queens == 0) or (queens == 1 and rooks <= 1) or (total_material < 3200) | |
| def _evaluate_endgame_factors(self, board: chess.Board) -> float: | |
| """Evaluate endgame-specific factors""" | |
| if not self._is_endgame(board): | |
| return 0 | |
| score = 0 | |
| # King centralization in endgame | |
| score += self._evaluate_king_centralization(board) | |
| # Passed pawns become more valuable in endgame | |
| score += self._evaluate_endgame_passed_pawns(board) | |
| # Rook on open files | |
| score += self._evaluate_rooks_on_open_files(board) | |
| return score | |
| def _evaluate_king_centralization(self, board: chess.Board) -> float: | |
| """ | |
| Evaluate king centralization in endgame | |
| Kings should move to the center in endgames | |
| """ | |
| score = 0 | |
| for color in [chess.WHITE, chess.BLACK]: | |
| king_square = board.king(color) | |
| if king_square is None: | |
| continue | |
| # Calculate distance from center (d4, d5, e4, e5) | |
| file = chess.square_file(king_square) | |
| rank = chess.square_rank(king_square) | |
| # Distance from center files (0-3.5) | |
| file_distance = abs(3.5 - file) | |
| # Distance from center ranks (0-3.5) | |
| rank_distance = abs(3.5 - rank) | |
| # Manhattan distance from center | |
| center_distance = file_distance + rank_distance | |
| # Bonus for being close to center (max 15 points) | |
| centralization_bonus = (7 - center_distance) * 3 | |
| if color == chess.WHITE: | |
| score += centralization_bonus | |
| else: | |
| score -= centralization_bonus | |
| return score | |
| def _evaluate_endgame_passed_pawns(self, board: chess.Board) -> float: | |
| """ | |
| Evaluate passed pawns in endgame - they're more valuable | |
| """ | |
| score = 0 | |
| # Get pawn positions | |
| white_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and | |
| board.piece_at(sq).piece_type == chess.PAWN and | |
| board.piece_at(sq).color == chess.WHITE] | |
| black_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and | |
| board.piece_at(sq).piece_type == chess.PAWN and | |
| board.piece_at(sq).color == chess.BLACK] | |
| # Check for passed pawns | |
| for pawn in white_pawns: | |
| if self._is_passed_pawn(board, pawn, chess.WHITE): | |
| rank = chess.square_rank(pawn) | |
| # Bonus increases dramatically with advancement | |
| bonus = (rank * rank) * 5 | |
| score += bonus | |
| for pawn in black_pawns: | |
| if self._is_passed_pawn(board, pawn, chess.BLACK): | |
| rank = 7 - chess.square_rank(pawn) # Flip for black | |
| # Bonus increases dramatically with advancement | |
| bonus = (rank * rank) * 5 | |
| score -= bonus | |
| return score | |
| def _is_passed_pawn(self, board: chess.Board, square: int, color: chess.Color) -> bool: | |
| """Check if a pawn is passed""" | |
| file = chess.square_file(square) | |
| rank = chess.square_rank(square) | |
| # Direction of pawn movement | |
| direction = 1 if color == chess.WHITE else -1 | |
| # Check files that could block this pawn | |
| for check_file in [file - 1, file, file + 1]: | |
| if 0 <= check_file <= 7: | |
| for check_rank in range(rank + direction, 8 if color == chess.WHITE else -1, direction): | |
| if 0 <= check_rank <= 7: | |
| check_square = chess.square(check_file, check_rank) | |
| piece = board.piece_at(check_square) | |
| if piece and piece.piece_type == chess.PAWN and piece.color != color: | |
| return False | |
| return True | |
| def _evaluate_rooks_on_open_files(self, board: chess.Board) -> float: | |
| """Evaluate rooks on open or semi-open files""" | |
| score = 0 | |
| # Get all files with pawns | |
| files_with_white_pawns = set() | |
| files_with_black_pawns = set() | |
| for square in chess.SQUARES: | |
| piece = board.piece_at(square) | |
| if piece and piece.piece_type == chess.PAWN: | |
| file = chess.square_file(square) | |
| if piece.color == chess.WHITE: | |
| files_with_white_pawns.add(file) | |
| else: | |
| files_with_black_pawns.add(file) | |
| # Check rooks | |
| for square in chess.SQUARES: | |
| piece = board.piece_at(square) | |
| if piece and piece.piece_type == chess.ROOK: | |
| file = chess.square_file(square) | |
| # Open file (no pawns) | |
| if file not in files_with_white_pawns and file not in files_with_black_pawns: | |
| bonus = 25 | |
| # Semi-open file (no friendly pawns) | |
| elif (piece.color == chess.WHITE and file not in files_with_white_pawns) or \ | |
| (piece.color == chess.BLACK and file not in files_with_black_pawns): | |
| bonus = 15 | |
| else: | |
| bonus = 0 | |
| if piece.color == chess.WHITE: | |
| score += bonus | |
| else: | |
| score -= bonus | |
| return score | |