|
|
""" |
|
|
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) |
|
|
} |