Chess_Engine / chess_engine /ai /evaluation.py
electro-sb's picture
first commit
100a6dd
# chess_engine/ai/evaluation.py
import chess
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import math
@dataclass
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