Rafs-an09002's picture
Create engine/move_ordering.py
e3e5e3d verified
"""
Advanced Move Ordering Techniques
Critical for alpha-beta pruning efficiency
Research References:
- Stockfish move ordering (2023) - History heuristic + killer moves
- Fruit 2.1 - MVV-LVA and late move reductions
- Crafty - History tables and counter moves
Good move ordering can reduce search tree by 10-100x!
"""
import chess
from typing import List, Optional, Dict, Tuple
import numpy as np
class MoveOrderer:
"""
Advanced move ordering system
Combines multiple heuristics for optimal move ordering
"""
# Piece values for MVV-LVA (victim values)
PIECE_VALUES = {
chess.PAWN: 100,
chess.KNIGHT: 320,
chess.BISHOP: 330,
chess.ROOK: 500,
chess.QUEEN: 900,
chess.KING: 20000
}
def __init__(self):
"""Initialize move ordering data structures"""
# Killer moves (quiet moves that caused beta cutoff)
# killer_moves[depth] = [move1, move2]
self.killer_moves: Dict[int, List[Optional[chess.Move]]] = {}
self.max_killers = 2 # Store top 2 killer moves per depth
# History heuristic (move success rates)
# history[from_square][to_square] = score
self.history = np.zeros((64, 64), dtype=np.int32)
# Counter moves (refutation table)
# counter_moves[prev_move] = best_response
self.counter_moves: Dict[chess.Move, Optional[chess.Move]] = {}
# Statistics
self.history_hits = 0
self.killer_hits = 0
def order_moves(
self,
board: chess.Board,
moves: List[chess.Move],
depth: int,
tt_move: Optional[chess.Move] = None,
previous_move: Optional[chess.Move] = None
) -> List[chess.Move]:
"""
Order moves for optimal alpha-beta pruning
Priority order (research-based):
1. TT move (from transposition table)
2. Winning captures (MVV-LVA)
3. Killer moves
4. Counter moves
5. History heuristic
6. Losing captures
7. Quiet moves
Args:
board: Current position
moves: Legal moves to order
depth: Current search depth
tt_move: Best move from transposition table
previous_move: Opponent's last move
Returns:
Ordered list of moves
"""
scored_moves = []
for move in moves:
score = 0
# ========== PRIORITY 1: TT Move ==========
if tt_move and move == tt_move:
score += 1000000
# ========== PRIORITY 2: Captures (MVV-LVA) ==========
elif board.is_capture(move):
score += self._score_capture(board, move)
# ========== PRIORITY 3-7: Quiet Moves ==========
else:
# Check if it's a killer move
if self._is_killer_move(move, depth):
score += 9000
self.killer_hits += 1
# Counter move bonus
if previous_move and move == self.counter_moves.get(previous_move):
score += 8000
# History heuristic
history_score = self.history[move.from_square, move.to_square]
score += min(history_score, 7000)
# Promotions
if move.promotion == chess.QUEEN:
score += 10000
elif move.promotion in [chess.KNIGHT, chess.ROOK, chess.BISHOP]:
score += 5000
# Checks
board.push(move)
if board.is_check():
score += 6000
board.pop()
# Castling
if board.is_castling(move):
score += 3000
# Positional bonuses
score += self._score_positional(board, move)
scored_moves.append((score, move))
# Sort by score (descending)
scored_moves.sort(key=lambda x: x[0], reverse=True)
return [move for _, move in scored_moves]
def _score_capture(self, board: chess.Board, move: chess.Move) -> int:
"""
Score capture using MVV-LVA (Most Valuable Victim - Least Valuable Attacker)
Enhanced with SEE (Static Exchange Evaluation)
"""
captured_piece = board.piece_at(move.to_square)
moving_piece = board.piece_at(move.from_square)
if not captured_piece or not moving_piece:
return 0
victim_value = self.PIECE_VALUES.get(captured_piece.piece_type, 0)
attacker_value = self.PIECE_VALUES.get(moving_piece.piece_type, 1)
# MVV-LVA base score
mvv_lva_score = (victim_value * 10 - attacker_value) * 100
# Bonus for en passant
if board.is_en_passant(move):
mvv_lva_score += 10500
# Penalty for hanging pieces (simplified SEE)
# Check if capture square is defended
if board.is_attacked_by(not board.turn, move.to_square):
# Losing capture if victim < attacker
if victim_value < attacker_value:
mvv_lva_score -= 5000 # Deprioritize losing captures
return mvv_lva_score
def _score_positional(self, board: chess.Board, move: chess.Move) -> int:
"""
Positional scoring for quiet moves
Based on Stockfish evaluation terms
"""
score = 0
piece = board.piece_at(move.from_square)
if not piece:
return 0
# Center control (e4, d4, e5, d5)
center_squares = [chess.E4, chess.D4, chess.E5, chess.D5]
if move.to_square in center_squares:
score += 50
# Extended center
extended_center = [
chess.C3, chess.D3, chess.E3, chess.F3,
chess.C4, chess.F4,
chess.C5, chess.F5,
chess.C6, chess.D6, chess.E6, chess.F6
]
if move.to_square in extended_center:
score += 20
# Piece development (from back rank)
if piece.piece_type in [chess.KNIGHT, chess.BISHOP]:
from_rank = move.from_square // 8
if from_rank in [0, 7]: # Back rank
score += 30
# Knight outposts (protected squares in enemy territory)
if piece.piece_type == chess.KNIGHT:
to_rank = move.to_square // 8
if (board.turn == chess.WHITE and to_rank >= 4) or \
(board.turn == chess.BLACK and to_rank <= 3):
# Check if protected by pawn
board.push(move)
if board.is_attacked_by(board.turn, move.to_square):
score += 40
board.pop()
return score
def _is_killer_move(self, move: chess.Move, depth: int) -> bool:
"""Check if move is a killer move at this depth"""
killers = self.killer_moves.get(depth, [])
return move in killers
def update_killer_move(self, move: chess.Move, depth: int):
"""
Update killer moves for this depth
Killer moves are quiet moves that caused beta cutoff
"""
if depth not in self.killer_moves:
self.killer_moves[depth] = []
killers = self.killer_moves[depth]
# Add move if not already in list
if move not in killers:
killers.insert(0, move)
# Keep only top N killers
self.killer_moves[depth] = killers[:self.max_killers]
def update_history(self, move: chess.Move, depth: int, success: bool):
"""
Update history heuristic
Args:
move: Move that was tried
depth: Search depth
success: True if move caused beta cutoff
"""
if success:
# Increase score (depth squared bonus for deeper searches)
bonus = depth * depth
self.history[move.from_square, move.to_square] += bonus
self.history_hits += 1
else:
# Slight penalty for moves that failed low
self.history[move.from_square, move.to_square] -= 1
# Cap values to prevent overflow
self.history = np.clip(self.history, -10000, 10000)
def update_counter_move(self, previous_move: chess.Move, refutation: chess.Move):
"""
Update counter move table
Counter move = best response to opponent's last move
"""
self.counter_moves[previous_move] = refutation
def clear_history(self):
"""Clear history table (called at new search)"""
self.history.fill(0)
self.killer_moves.clear()
self.history_hits = 0
self.killer_hits = 0
def age_history(self, factor: float = 0.9):
"""
Age history table (reduce all values)
Prevents old data from dominating
"""
self.history = (self.history * factor).astype(np.int32)
def get_stats(self) -> Dict:
"""Get move ordering statistics"""
return {
'killer_hits': self.killer_hits,
'history_hits': self.history_hits,
'history_max': int(np.max(self.history)),
'killer_depths': len(self.killer_moves),
'counter_moves': len(self.counter_moves)
}