github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
3.01 kB
"""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"]
@dataclass(frozen=True)
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