Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions moonfish/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ class Config:
syzygy_pieces: int
checkmate_score: int = CHECKMATE_SCORE
checkmate_threshold: int = CHECKMATE_THRESHOLD
nn_model_path: str | None = None
86 changes: 86 additions & 0 deletions moonfish/engines/nn_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Neural network chess engine.

Uses alpha-beta search with a pluggable neural network evaluator instead of
the classical PeSTO evaluation. Any evaluator implementing the Evaluator
protocol can be used.

Example:
from moonfish.evaluation.nn import NNEvaluator

# Load a custom ONNX model
evaluator = NNEvaluator.from_file("my_model.onnx")
engine = NNEngine(config, evaluator=evaluator)
best_move = engine.search_move(board)

# Or use an LLM for evaluation
def llm_eval(board):
return call_my_llm(board.fen())

evaluator = NNEvaluator(eval_fn=llm_eval)
engine = NNEngine(config, evaluator=evaluator)
"""

from chess import Board, Move

from moonfish.config import Config
from moonfish.engines.alpha_beta import AlphaBeta
from moonfish.evaluation.base import Evaluator
from moonfish.evaluation.classical import ClassicalEvaluator


class NNEngine(AlphaBeta):
"""
Chess engine that uses a pluggable evaluator for position assessment.

Inherits the full alpha-beta search from AlphaBeta but replaces the
evaluation function with a provided Evaluator instance. This allows
using neural networks, LLMs, or any custom evaluation function while
keeping the same search algorithm.
"""

def __init__(self, config: Config, evaluator: Evaluator | None = None):
"""
Initialize the NN engine.

Args:
config: Engine configuration.
evaluator: An Evaluator instance. If None, falls back to
ClassicalEvaluator (PeSTO tables).
"""
super().__init__(config)
self.evaluator = evaluator or ClassicalEvaluator()

def eval_board(self, board: Board) -> float:
"""
Evaluate the board using the plugged-in evaluator.

If Syzygy tablebases are available and the position qualifies,
tablebase probing takes priority over the evaluator.

Args:
board: The chess position to evaluate.

Returns:
Score from the side-to-move's perspective.
"""
# Syzygy probing still takes priority for endgame positions
if self.tablebase is not None:
from moonfish.psqt import count_pieces

pieces = sum(count_pieces(board))
if pieces <= self.config.syzygy_pieces:
try:
import chess.syzygy

dtz = self.tablebase.probe_dtz(board)
return dtz
except Exception:
pass
Comment on lines +78 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly broad exception handler
The base AlphaBeta.eval_board catches (chess.syzygy.MissingTableError, KeyError) specifically, but here a bare except Exception is used. This could silently swallow real errors (I/O failures, corrupted tablebase data, etc.) and fall through to the NN evaluator without any indication that something went wrong. Consider matching the parent class's specific exception types.

Suggested change
except Exception:
pass
except (chess.syzygy.MissingTableError, KeyError):
pass


return self.evaluator.evaluate(board)

def search_move(self, board: Board) -> Move:
"""Search for the best move, resetting evaluator state first."""
self.evaluator.reset()
return super().search_move(board)
5 changes: 5 additions & 0 deletions moonfish/evaluation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from moonfish.evaluation.base import Evaluator
from moonfish.evaluation.classical import ClassicalEvaluator
from moonfish.evaluation.nn import NNEvaluator

__all__ = ["Evaluator", "ClassicalEvaluator", "NNEvaluator"]
49 changes: 49 additions & 0 deletions moonfish/evaluation/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Base evaluator protocol.

Any evaluation function (classical, NNUE, transformer, LLM) should implement
this protocol to be usable with the alpha-beta search engine.
"""

from typing import Protocol, runtime_checkable

from chess import Board


@runtime_checkable
class Evaluator(Protocol):
"""
Protocol for board evaluation functions.

Implementations can range from simple piece-square tables to neural networks
or even LLM-based evaluators. The engine will call `evaluate()` at leaf nodes
of the search tree and in quiescence search.

The returned score should be from the perspective of the side to move:
- Positive = good for the side to move
- Negative = bad for the side to move
- 0 = roughly equal

Scores are in centipawns (100 = 1 pawn advantage).
"""

def evaluate(self, board: Board) -> float:
"""
Evaluate the given board position.

Args:
board: The chess position to evaluate.

Returns:
Score in centipawns from the side-to-move's perspective.
"""
...

def reset(self) -> None:
"""
Reset any internal state (e.g., caches, accumulators).

Called at the start of each new search. Implementations that
don't maintain state can make this a no-op.
"""
...
25 changes: 25 additions & 0 deletions moonfish/evaluation/classical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Classical evaluator wrapping the existing PeSTO evaluation.

This is the default evaluator used by the engine. It provides a baseline
for comparing against neural network evaluators.
"""

from chess import Board

from moonfish.psqt import BOARD_EVALUATION_CACHE, board_evaluation


class ClassicalEvaluator:
"""
Classical evaluation based on PeSTO piece-square tables with
tapered midgame/endgame scoring.
"""

def evaluate(self, board: Board) -> float:
"""Evaluate using PeSTO piece-square tables."""
return board_evaluation(board)

def reset(self) -> None:
"""Clear the evaluation cache between searches."""
BOARD_EVALUATION_CACHE.clear()
Loading
Loading