Chess-Vision-Backend / app /predict.py
heigon77's picture
Chess Vision backend (digitization + move prediction)
3f9559b
Raw
History Blame Contribute Delete
4.38 kB
"""Human-like move prediction: CNN (piece + square classifiers) + Stockfish.
The CNN models predict the move a *human* would likely play (trained for white to
move; black positions are mirrored). Stockfish provides engine-strength moves and
a hybrid that keeps human-like moves which don't blunder.
"""
from __future__ import annotations
from pathlib import Path
import chess
import numpy as np
import onnxruntime as ort
PIECE_ORDER = [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN, chess.KING]
SQUARE_NAMES = ["pawn", "knight", "bishop", "rook", "queen", "king"]
def _softmax(x):
e = np.exp(x - x.max())
return e / e.sum()
class MovePredictor:
def __init__(self, models_dir: str, stockfish_path: str | None = None):
d = Path(models_dir)
prov = ["CPUExecutionProvider"]
self.piece = ort.InferenceSession(str(d / "piece.int8.onnx"), providers=prov)
self.squares = [ort.InferenceSession(str(d / f"square_{s}.int8.onnx"), providers=prov)
for s in SQUARE_NAMES]
self.stockfish_path = (stockfish_path if stockfish_path and Path(stockfish_path).exists()
else None)
# ── encoding: 12x8x8, ch 0-5 white P,N,B,R,Q,K; 6-11 black; row0 = rank8 ──
def _encode(self, board: chess.Board):
enc = np.zeros((12, 8, 8), np.float32)
for sq in chess.SQUARES:
p = board.piece_at(sq)
if p:
c = PIECE_ORDER.index(p.piece_type) + (0 if p.color == chess.WHITE else 6)
enc[c, 7 - (sq >> 3), sq & 7] = 1.0
return enc[None]
def _cnn_white(self, board: chess.Board) -> dict:
enc = self._encode(board)
pieces = _softmax(self.piece.run(None, {"input": enc})[0].flatten())
legal = set(board.legal_moves)
mp: dict[str, float] = {}
for i, ptype in enumerate(PIECE_ORDER):
sq = (_softmax(self.squares[i].run(None, {"input": enc})[0].flatten()) + pieces[i]) / 2
for fsq in board.pieces(ptype, chess.WHITE):
fs = chess.square_name(fsq)
for j in range(64):
try:
mv = chess.Move.from_uci(fs + chess.square_name(j))
except ValueError:
continue
if mv in legal:
mp[mv.uci()] = float(sq[j])
return mp
def cnn_scores(self, board: chess.Board) -> dict:
"""Move->score; handles black by mirroring the board and the moves."""
if board.turn == chess.WHITE:
return self._cnn_white(board)
mirrored = self._cnn_white(board.mirror())
out = {}
for uci, s in mirrored.items():
mv = chess.Move.from_uci(uci)
real = chess.Move(chess.square_mirror(mv.from_square),
chess.square_mirror(mv.to_square), mv.promotion)
out[real.uci()] = s
return out
def predict(self, fen: str, top_n: int = 3) -> dict:
board = chess.Board(fen)
cnn = [m for m, _ in sorted(self.cnn_scores(board).items(), key=lambda x: -x[1])]
out = {"fen": fen, "turn": "white" if board.turn else "black",
"cnn": cnn[:top_n], "stockfish": [], "hybrid": cnn[:top_n]}
if self.stockfish_path:
from stockfish import Stockfish
sf = Stockfish(path=self.stockfish_path, parameters={"Threads": 1})
sf.set_fen_position(fen)
sf.update_engine_parameters({"MultiPV": top_n})
out["stockfish"] = [t["Move"] for t in sf.get_top_moves(top_n) if t.get("Move")]
# hybrid: among the top human-like CNN moves, prefer the one Stockfish
# rates best (lowest eval for the opponent after the move).
scored = []
for mv in cnn[:max(6, top_n * 2)]:
board.push_uci(mv)
sf.set_fen_position(board.fen())
ev = sf.get_evaluation()
val = ev.get("value", 0) if ev.get("type") == "cp" else (10000 if ev.get("value", 0) > 0 else -10000)
board.pop()
scored.append((mv, val)) # val = opponent's eval after our move (lower = better for us)
out["hybrid"] = [m for m, _ in sorted(scored, key=lambda x: x[1])][:top_n]
return out