| | """ |
| | Deep Search Engine for Synapse-Base |
| | Alpha-Beta pruning with advanced move ordering |
| | CPU-optimized for HF Spaces (2 vCPU, 16GB RAM) |
| | """ |
| |
|
| | import chess |
| | import time |
| | import logging |
| | from typing import Optional, Tuple, List |
| | from model_loader import SynapseModel |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | class SynapseEngine: |
| | """ |
| | Chess engine with neural network evaluation and alpha-beta search |
| | """ |
| | |
| | def __init__(self, model_path: str, num_threads: int = 2): |
| | """ |
| | Initialize engine |
| | |
| | Args: |
| | model_path: Path to ONNX model |
| | num_threads: CPU threads for inference |
| | """ |
| | self.model = SynapseModel(model_path, num_threads) |
| | self.nodes_evaluated = 0 |
| | self.cache = {} |
| | self.max_cache_size = 50000 |
| | |
| | |
| | self.MVV_LVA = { |
| | (chess.PAWN, chess.QUEEN): 50, |
| | (chess.PAWN, chess.ROOK): 40, |
| | (chess.KNIGHT, chess.QUEEN): 45, |
| | (chess.KNIGHT, chess.ROOK): 35, |
| | (chess.BISHOP, chess.QUEEN): 45, |
| | (chess.BISHOP, chess.ROOK): 35, |
| | (chess.ROOK, chess.QUEEN): 40, |
| | (chess.QUEEN, chess.PAWN): -10, |
| | } |
| | |
| | logger.info("🎯 Engine initialized") |
| | |
| | def evaluate_position(self, board: chess.Board) -> float: |
| | """ |
| | Evaluate position using neural network |
| | |
| | Args: |
| | board: chess.Board object |
| | |
| | Returns: |
| | float: evaluation score from white's perspective |
| | """ |
| | self.nodes_evaluated += 1 |
| | |
| | |
| | fen_key = board.fen().split(' ')[0] |
| | if fen_key in self.cache: |
| | return self.cache[fen_key] |
| | |
| | |
| | result = self.model.evaluate(board.fen()) |
| | evaluation = result['value'] |
| | |
| | |
| | if board.turn == chess.BLACK: |
| | evaluation = -evaluation |
| | |
| | |
| | if len(self.cache) < self.max_cache_size: |
| | self.cache[fen_key] = evaluation |
| | |
| | return evaluation |
| | |
| | def order_moves(self, board: chess.Board, moves: List[chess.Move]) -> List[chess.Move]: |
| | """ |
| | Order moves for better alpha-beta pruning |
| | |
| | Priority: |
| | 1. Captures (MVV-LVA) |
| | 2. Checks |
| | 3. Promotions |
| | 4. Castling |
| | 5. Other moves |
| | |
| | Args: |
| | board: Current position |
| | moves: List of legal moves |
| | |
| | Returns: |
| | Sorted list of moves |
| | """ |
| | scored_moves = [] |
| | |
| | for move in moves: |
| | score = 0 |
| | |
| | |
| | |
| | |
| | if board.is_capture(move): |
| | captured_piece = board.piece_at(move.to_square) |
| | moving_piece = board.piece_at(move.from_square) |
| | |
| | if captured_piece and moving_piece: |
| | |
| | piece_values = { |
| | chess.PAWN: 1, |
| | chess.KNIGHT: 3, |
| | chess.BISHOP: 3, |
| | chess.ROOK: 5, |
| | chess.QUEEN: 9, |
| | chess.KING: 0 |
| | } |
| | |
| | victim_value = piece_values.get(captured_piece.piece_type, 0) |
| | attacker_value = piece_values.get(moving_piece.piece_type, 1) |
| | |
| | |
| | score += (victim_value * 10 - attacker_value) * 1000 |
| | |
| | |
| | if move.promotion: |
| | if move.promotion == chess.QUEEN: |
| | score += 9000 |
| | elif move.promotion == chess.KNIGHT: |
| | score += 3000 |
| | |
| | |
| | board.push(move) |
| | if board.is_check(): |
| | score += 5000 |
| | board.pop() |
| | |
| | |
| | if board.is_castling(move): |
| | score += 2000 |
| | |
| | |
| | |
| | |
| | center_squares = [chess.D4, chess.D5, chess.E4, chess.E5] |
| | if move.to_square in center_squares: |
| | score += 100 |
| | |
| | |
| | piece = board.piece_at(move.from_square) |
| | if piece and piece.piece_type in [chess.KNIGHT, chess.BISHOP]: |
| | |
| | if move.from_square // 8 in [0, 7]: |
| | score += 50 |
| | |
| | scored_moves.append((score, move)) |
| | |
| | |
| | scored_moves.sort(key=lambda x: x[0], reverse=True) |
| | |
| | return [move for score, move in scored_moves] |
| | |
| | def quiescence_search( |
| | self, |
| | board: chess.Board, |
| | alpha: float, |
| | beta: float, |
| | depth: int = 6 |
| | ) -> float: |
| | """ |
| | Quiescence search to avoid horizon effect |
| | Only searches captures and checks |
| | |
| | Args: |
| | board: Current position |
| | alpha: Alpha value |
| | beta: Beta value |
| | depth: Remaining quiescence depth |
| | |
| | Returns: |
| | Evaluation score |
| | """ |
| | |
| | stand_pat = self.evaluate_position(board) |
| | |
| | if stand_pat >= beta: |
| | return beta |
| | if alpha < stand_pat: |
| | alpha = stand_pat |
| | |
| | if depth == 0: |
| | return stand_pat |
| | |
| | |
| | tactical_moves = [ |
| | move for move in board.legal_moves |
| | if board.is_capture(move) or board.gives_check(move) |
| | ] |
| | |
| | if not tactical_moves: |
| | return stand_pat |
| | |
| | tactical_moves = self.order_moves(board, tactical_moves) |
| | |
| | for move in tactical_moves: |
| | board.push(move) |
| | score = -self.quiescence_search(board, -beta, -alpha, depth - 1) |
| | board.pop() |
| | |
| | if score >= beta: |
| | return beta |
| | if score > alpha: |
| | alpha = score |
| | |
| | return alpha |
| | |
| | def alpha_beta( |
| | self, |
| | board: chess.Board, |
| | depth: int, |
| | alpha: float, |
| | beta: float, |
| | start_time: float, |
| | time_limit: float |
| | ) -> Tuple[float, Optional[chess.Move]]: |
| | """ |
| | Alpha-beta pruning search |
| | |
| | Args: |
| | board: Current position |
| | depth: Remaining search depth |
| | alpha: Alpha value |
| | beta: Beta value |
| | start_time: Search start time |
| | time_limit: Max time in seconds |
| | |
| | Returns: |
| | (evaluation, best_move) |
| | """ |
| | |
| | if time.time() - start_time > time_limit: |
| | return self.evaluate_position(board), None |
| | |
| | |
| | if board.is_game_over(): |
| | if board.is_checkmate(): |
| | return -10000, None |
| | return 0, None |
| | |
| | |
| | if depth == 0: |
| | return self.quiescence_search(board, alpha, beta, depth=2), None |
| | |
| | legal_moves = list(board.legal_moves) |
| | if not legal_moves: |
| | return 0, None |
| | |
| | |
| | ordered_moves = self.order_moves(board, legal_moves) |
| | |
| | best_move = ordered_moves[0] |
| | best_score = float('-inf') |
| | |
| | for move in ordered_moves: |
| | board.push(move) |
| | |
| | |
| | score, _ = self.alpha_beta( |
| | board, depth - 1, -beta, -alpha, start_time, time_limit |
| | ) |
| | score = -score |
| | |
| | board.pop() |
| | |
| | |
| | if score > best_score: |
| | best_score = score |
| | best_move = move |
| | |
| | |
| | alpha = max(alpha, score) |
| | if alpha >= beta: |
| | break |
| | |
| | return best_score, best_move |
| | |
| | def get_best_move( |
| | self, |
| | fen: str, |
| | depth: int = 6, |
| | time_limit: int = 5000 |
| | ) -> dict: |
| | """ |
| | Get best move for position |
| | |
| | Args: |
| | fen: FEN string |
| | depth: Search depth |
| | time_limit: Time limit in milliseconds |
| | |
| | Returns: |
| | dict with best_move, evaluation, nodes, etc. |
| | """ |
| | board = chess.Board(fen) |
| | |
| | |
| | self.nodes_evaluated = 0 |
| | |
| | |
| | time_limit_sec = time_limit / 1000.0 |
| | start_time = time.time() |
| | |
| | |
| | legal_moves = list(board.legal_moves) |
| | if len(legal_moves) == 1: |
| | return { |
| | 'best_move': legal_moves[0].uci(), |
| | 'evaluation': self.evaluate_position(board), |
| | 'depth_searched': 0, |
| | 'nodes_evaluated': 1, |
| | 'pv': [legal_moves[0].uci()] |
| | } |
| | |
| | |
| | best_move = None |
| | best_eval = None |
| | |
| | for current_depth in range(1, depth + 1): |
| | |
| | if time.time() - start_time > time_limit_sec * 0.9: |
| | break |
| | |
| | try: |
| | eval_score, move = self.alpha_beta( |
| | board, |
| | current_depth, |
| | float('-inf'), |
| | float('inf'), |
| | start_time, |
| | time_limit_sec |
| | ) |
| | |
| | if move: |
| | best_move = move |
| | best_eval = eval_score |
| | |
| | except Exception as e: |
| | logger.warning(f"Search error at depth {current_depth}: {e}") |
| | break |
| | |
| | |
| | if best_move is None: |
| | best_move = legal_moves[0] |
| | best_eval = self.evaluate_position(board) |
| | |
| | return { |
| | 'best_move': best_move.uci(), |
| | 'evaluation': round(best_eval, 4), |
| | 'depth_searched': current_depth, |
| | 'nodes_evaluated': self.nodes_evaluated, |
| | 'pv': [best_move.uci()] |
| | } |
| | |
| | def validate_fen(self, fen: str) -> bool: |
| | """Validate FEN string""" |
| | try: |
| | chess.Board(fen) |
| | return True |
| | except: |
| | return False |
| | |
| | def get_model_size(self) -> float: |
| | """Get model size in MB""" |
| | return self.model.get_size_mb() |