| """ |
| 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 = { |
| 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""" |
| |
| |
| |
| self.killer_moves: Dict[int, List[Optional[chess.Move]]] = {} |
| self.max_killers = 2 |
| |
| |
| |
| self.history = np.zeros((64, 64), dtype=np.int32) |
| |
| |
| |
| self.counter_moves: Dict[chess.Move, Optional[chess.Move]] = {} |
| |
| |
| 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 |
| |
| |
| if tt_move and move == tt_move: |
| score += 1000000 |
| |
| |
| elif board.is_capture(move): |
| score += self._score_capture(board, move) |
| |
| |
| else: |
| |
| if self._is_killer_move(move, depth): |
| score += 9000 |
| self.killer_hits += 1 |
| |
| |
| if previous_move and move == self.counter_moves.get(previous_move): |
| score += 8000 |
| |
| |
| history_score = self.history[move.from_square, move.to_square] |
| score += min(history_score, 7000) |
| |
| |
| if move.promotion == chess.QUEEN: |
| score += 10000 |
| elif move.promotion in [chess.KNIGHT, chess.ROOK, chess.BISHOP]: |
| score += 5000 |
| |
| |
| board.push(move) |
| if board.is_check(): |
| score += 6000 |
| board.pop() |
| |
| |
| if board.is_castling(move): |
| score += 3000 |
| |
| |
| score += self._score_positional(board, move) |
| |
| scored_moves.append((score, move)) |
| |
| |
| 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_score = (victim_value * 10 - attacker_value) * 100 |
| |
| |
| if board.is_en_passant(move): |
| mvv_lva_score += 10500 |
| |
| |
| |
| if board.is_attacked_by(not board.turn, move.to_square): |
| |
| if victim_value < attacker_value: |
| mvv_lva_score -= 5000 |
| |
| 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_squares = [chess.E4, chess.D4, chess.E5, chess.D5] |
| if move.to_square in center_squares: |
| score += 50 |
| |
| |
| 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 |
| |
| |
| if piece.piece_type in [chess.KNIGHT, chess.BISHOP]: |
| from_rank = move.from_square // 8 |
| if from_rank in [0, 7]: |
| score += 30 |
| |
| |
| 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): |
| |
| 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] |
| |
| |
| if move not in killers: |
| killers.insert(0, move) |
| |
| 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: |
| |
| bonus = depth * depth |
| self.history[move.from_square, move.to_square] += bonus |
| self.history_hits += 1 |
| else: |
| |
| self.history[move.from_square, move.to_square] -= 1 |
| |
| |
| 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) |
| } |