|
|
""" |
|
|
Move Ordering for Nexus-Core |
|
|
Simplified but effective heuristics |
|
|
|
|
|
Research: |
|
|
- Schaeffer (1989) - History heuristic |
|
|
- Akl & Newborn (1977) - Killer moves |
|
|
""" |
|
|
|
|
|
import chess |
|
|
from typing import List, Optional, Dict |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
class MoveOrderer: |
|
|
"""Move ordering with killer + history heuristics""" |
|
|
|
|
|
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 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.killer_hits = 0 |
|
|
self.history_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 pruning |
|
|
|
|
|
Priority: |
|
|
1. TT move |
|
|
2. Winning captures (MVV-LVA) |
|
|
3. Killer moves |
|
|
4. Counter moves |
|
|
5. History heuristic |
|
|
6. Quiet 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 |
|
|
|
|
|
|
|
|
board.push(move) |
|
|
if board.is_check(): |
|
|
score += 6000 |
|
|
board.pop() |
|
|
|
|
|
|
|
|
if board.is_castling(move): |
|
|
score += 3000 |
|
|
|
|
|
|
|
|
center = [chess.D4, chess.D5, chess.E4, chess.E5] |
|
|
if move.to_square in center: |
|
|
score += 50 |
|
|
|
|
|
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: |
|
|
"""MVV-LVA capture scoring""" |
|
|
|
|
|
captured = board.piece_at(move.to_square) |
|
|
attacker = board.piece_at(move.from_square) |
|
|
|
|
|
if not captured or not attacker: |
|
|
return 0 |
|
|
|
|
|
victim_value = self.PIECE_VALUES.get(captured.piece_type, 0) |
|
|
attacker_value = self.PIECE_VALUES.get(attacker.piece_type, 1) |
|
|
|
|
|
|
|
|
mvv_lva = (victim_value * 10 - attacker_value) * 100 |
|
|
|
|
|
|
|
|
if board.is_en_passant(move): |
|
|
mvv_lva += 10500 |
|
|
|
|
|
|
|
|
if board.is_attacked_by(not board.turn, move.to_square): |
|
|
if victim_value < attacker_value: |
|
|
mvv_lva -= 5000 |
|
|
|
|
|
return mvv_lva |
|
|
|
|
|
def _is_killer_move(self, move: chess.Move, depth: int) -> bool: |
|
|
"""Check if killer move""" |
|
|
killers = self.killer_moves.get(depth, []) |
|
|
return move in killers |
|
|
|
|
|
def update_killer_move(self, move: chess.Move, depth: int): |
|
|
"""Update killer moves""" |
|
|
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""" |
|
|
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""" |
|
|
self.counter_moves[previous_move] = refutation |
|
|
|
|
|
def clear_history(self): |
|
|
"""Clear history""" |
|
|
self.history.fill(0) |
|
|
self.killer_moves.clear() |
|
|
self.killer_hits = 0 |
|
|
self.history_hits = 0 |
|
|
|
|
|
def age_history(self, factor: float = 0.9): |
|
|
"""Age history table""" |
|
|
self.history = (self.history * factor).astype(np.int32) |
|
|
|
|
|
def get_stats(self) -> Dict: |
|
|
"""Get 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) |
|
|
} |