Spaces:
Running
Running
| """Engine abstraction for optional exemplar move analysis.""" | |
| from __future__ import annotations | |
| import shutil | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Literal, Optional, Protocol, Tuple | |
| import chess | |
| from chess_tutor.lc0_uci import resolve_lc0_executable | |
| EngineBackend = Literal["off", "stockfish", "lc0"] | |
| EngineAnalysisMode = Literal["off", "auto", "stockfish", "lc0"] | |
| class EngineAdvice: | |
| bestmove_uci: str | |
| pv_uci: Tuple[str, ...] | |
| eval_cp_white: Optional[int] | |
| played_eval_cp_white: Optional[int] | |
| class MoveAdvisor(Protocol): | |
| def analyze_position( | |
| self, | |
| fen: str, | |
| *, | |
| movetime_ms: int = 500, | |
| ) -> EngineAdvice: ... | |
| def close(self) -> None: ... | |
| def uci_to_san(fen: str, uci: str) -> str: | |
| board = chess.Board(fen) | |
| move = chess.Move.from_uci(uci) | |
| return board.san(move) | |
| def eval_after_move_cp_white(fen: str, uci: str, advisor: MoveAdvisor, *, movetime_ms: int) -> Optional[int]: | |
| board = chess.Board(fen) | |
| move = chess.Move.from_uci(uci) | |
| if move not in board.legal_moves: | |
| return None | |
| board.push(move) | |
| advice = advisor.analyze_position(board.fen(), movetime_ms=movetime_ms) | |
| return advice.eval_cp_white | |
| def resolve_engine_backend( | |
| mode: EngineAnalysisMode | str, | |
| *, | |
| stockfish_path: str | None = None, | |
| lc0_exec: str | None = None, | |
| ) -> EngineBackend: | |
| """Map CLI/catalog mode to a concrete backend; ``auto`` tries Stockfish then LC0.""" | |
| name = str(mode or "off").strip().lower() | |
| if name == "off": | |
| return "off" | |
| if name == "stockfish": | |
| return "stockfish" | |
| if name == "lc0": | |
| return "lc0" | |
| if name != "auto": | |
| raise ValueError(f"Unknown engine mode: {mode!r}") | |
| if stockfish_path: | |
| if Path(stockfish_path).is_file(): | |
| return "stockfish" | |
| elif shutil.which("stockfish"): | |
| return "stockfish" | |
| try: | |
| resolve_lc0_executable(lc0_exec) | |
| from chess_tutor.onnx_probe_config import load_onnx_probe_config | |
| cfg = load_onnx_probe_config() | |
| if Path(cfg.weights_path).is_file(): | |
| return "lc0" | |
| except (FileNotFoundError, ValueError): | |
| pass | |
| return "off" | |
| def create_move_advisor( | |
| backend: EngineAnalysisMode | EngineBackend | str, | |
| *, | |
| stockfish_path: str | None = None, | |
| lc0_exec: str | None = None, | |
| lc0_weights: str | None = None, | |
| ) -> MoveAdvisor | None: | |
| resolved = resolve_engine_backend( | |
| backend, | |
| stockfish_path=stockfish_path, | |
| lc0_exec=lc0_exec, | |
| ) | |
| if resolved == "off": | |
| return None | |
| if resolved == "stockfish": | |
| from chess_tutor.engine.stockfish_uci import StockfishMoveAdvisor | |
| return StockfishMoveAdvisor(executable=stockfish_path) | |
| if resolved == "lc0": | |
| from chess_tutor.engine.lc0_adapter import Lc0MoveAdvisor | |
| return Lc0MoveAdvisor(executable=lc0_exec, weights=lc0_weights) | |
| return None | |