Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'

- name: Install dependencies
run: make install
Expand Down
29 changes: 11 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.12', '3.13']
env:
UV_SYSTEM_PYTHON: 1

Expand All @@ -31,25 +31,21 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: ${{ matrix.python-version }}

- name: Install dev requirements
run: |
make install
run: uv pip install -e .

- name: Test moonfish import
shell: bash -l {0}
run: |
python -c "import moonfish"
python -c "import moonfish; import chess; board = chess.Board(); move = moonfish.search_move(board)"

- name: Test CLI functionality
shell: bash -l {0}
run: |
moonfish --help

- name: Run unit tests
shell: bash -l {0}
run: |
python -m unittest tests/test.py

Expand All @@ -72,12 +68,11 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'

- name: Install dev requirements
run: |
make install

run: uv pip install -e .

- name: Run ufmt
run: ufmt check moonfish tests

Expand All @@ -100,12 +95,11 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'

- name: Install dev requirements
run: |
make install

run: uv pip install -e .

- name: Run Flake8
run: flake8 moonfish tests

Expand All @@ -128,11 +122,10 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'

- name: Install dev requirements
run: |
make install
run: uv pip install -e .

- name: Run mypy
run: mypy moonfish tests
2 changes: 1 addition & 1 deletion .github/workflows/nps-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'

- name: Install dependencies
run: make install
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
environment: pypi-publish
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
5 changes: 2 additions & 3 deletions moonfish/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from dataclasses import dataclass
from typing import Optional

# Score for checkmate.
CHECKMATE_SCORE = 10**8
# Threshold to differentiate checkmates from other moves.
CHECKMATE_THRESHOLD = 999 * (10**4)


@dataclass
@dataclass(slots=True)
class Config:
"""
Configuration for the engine.
Expand All @@ -19,7 +18,7 @@ class Config:
null_move: bool
null_move_r: int
quiescence_search_depth: int
syzygy_path: Optional[str]
syzygy_path: str | None
syzygy_pieces: int
checkmate_score: int = CHECKMATE_SCORE
checkmate_threshold: int = CHECKMATE_THRESHOLD
86 changes: 44 additions & 42 deletions moonfish/engines/alpha_beta.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from copy import copy
from multiprocessing.managers import DictProxy
from typing import Dict, Optional, Tuple

import chess.syzygy
from chess import Board, Move
Expand All @@ -9,10 +7,14 @@
from moonfish.move_ordering import organize_moves, organize_moves_quiescence
from moonfish.psqt import board_evaluation, count_pieces

CACHE_KEY = Dict[
Tuple[str, int, bool, float, float], Tuple[float | int, Optional[Move]]
CACHE_KEY = dict[
tuple[object, int, bool, float, float], tuple[float | int, Move | None]
]

INF = float("inf")
NEG_INF = float("-inf")
NULL_MOVE = Move.null()


class AlphaBeta:
"""
Expand Down Expand Up @@ -53,16 +55,16 @@ def eval_board(self, board: Board) -> float:
Returns:
- score: the score for the current board
"""
pieces = sum(count_pieces(board))

# Use pre-opened tablebase for endgame positions
if pieces <= self.config.syzygy_pieces and self.tablebase is not None:
try:
dtz = self.tablebase.probe_dtz(board)
return dtz
except (chess.syzygy.MissingTableError, KeyError):
# Position not in tablebase, fall through to normal evaluation
pass
# Short-circuit: only count pieces if a tablebase is loaded
if self.tablebase is not None:
pieces = sum(count_pieces(board))
if pieces <= self.config.syzygy_pieces:
try:
dtz = self.tablebase.probe_dtz(board)
return dtz
except (chess.syzygy.MissingTableError, KeyError):
# Position not in tablebase, fall through to normal evaluation
pass

return board_evaluation(board)

Expand Down Expand Up @@ -118,7 +120,7 @@ def quiescence_search(
# (not ideal but prevents infinite recursion)
return stand_pat

best_score = float("-inf")
best_score = NEG_INF
moves = list(board.legal_moves) # All evasions
else:
# Not in check: normal quiescence behavior
Expand Down Expand Up @@ -146,10 +148,10 @@ def quiescence_search(
score = 0.0 # Draw score
else:
score = -self.quiescence_search(
board=board,
depth=depth - 1,
alpha=-beta,
beta=-alpha,
board,
depth - 1,
-beta,
-alpha,
)

board.pop()
Expand All @@ -172,9 +174,9 @@ def negamax(
depth: int,
null_move: bool,
cache: DictProxy | CACHE_KEY,
alpha: float = float("-inf"),
beta: float = float("inf"),
) -> Tuple[float | int, Optional[Move]]:
alpha: float = NEG_INF,
beta: float = INF,
) -> tuple[float | int, Move | None]:
"""
This functions receives a board, depth and a player; and it returns
the best move for the current board based on how many depths we're looking ahead
Expand Down Expand Up @@ -204,7 +206,7 @@ def negamax(
Returns:
- best_score, best_move: returns best move that it found and its value.
"""
cache_key = (board.fen(), depth, null_move, alpha, beta)
cache_key = (board._transposition_key(), depth, null_move, alpha, beta)

self.nodes += 1

Expand All @@ -224,10 +226,10 @@ def negamax(
if depth <= 0:
# evaluate current board
board_score = self.quiescence_search(
board=board,
depth=copy(self.config.quiescence_search_depth),
alpha=alpha,
beta=beta,
board,
self.config.quiescence_search_depth,
alpha,
beta,
)
cache[cache_key] = (board_score, None)
return board_score, None
Expand All @@ -240,14 +242,14 @@ def negamax(
):
board_score = self.eval_board(board)
if board_score >= beta:
board.push(Move.null())
board.push(NULL_MOVE)
board_score = -self.negamax(
board=board,
depth=depth - 1 - self.config.null_move_r,
null_move=False,
cache=cache,
alpha=-beta,
beta=-beta + 1,
board,
depth - 1 - self.config.null_move_r,
False,
cache,
-beta,
-beta + 1,
)[0]
board.pop()
if board_score >= beta:
Expand All @@ -257,20 +259,20 @@ def negamax(
best_move = None

# initializing best_score
best_score = float("-inf")
best_score = NEG_INF
moves = organize_moves(board)

for move in moves:
# make the move
board.push(move)

board_score = -self.negamax(
board=board,
depth=depth - 1,
null_move=null_move,
cache=cache,
alpha=-beta,
beta=-alpha,
board,
depth - 1,
null_move,
cache,
-beta,
-alpha,
)[0]
if board_score > self.config.checkmate_threshold:
board_score -= 1
Expand Down Expand Up @@ -313,7 +315,7 @@ def search_move(self, board: Board) -> Move:
cache: CACHE_KEY = {}

best_move = self.negamax(
board, copy(self.config.negamax_depth), self.config.null_move, cache
board, self.config.negamax_depth, self.config.null_move, cache
)[1]
assert best_move is not None, "Best move from root should not be None"
return best_move
5 changes: 2 additions & 3 deletions moonfish/engines/l1p_alpha_beta.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from copy import copy
from multiprocessing import cpu_count, Manager, Pool

from chess import Board, Move
Expand All @@ -25,8 +24,8 @@ def search_move(self, board: Board) -> Move:
board.push(move)
arguments.append(
(
copy(board),
copy(self.config.negamax_depth) - 1,
board.copy(),
self.config.negamax_depth - 1,
self.config.null_move,
shared_cache,
)
Expand Down
10 changes: 4 additions & 6 deletions moonfish/engines/l2p_alpha_beta.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from collections import defaultdict
from copy import copy
from functools import partial
from multiprocessing import cpu_count, Manager, Pool
from multiprocessing.managers import DictProxy
from typing import List, Tuple

from chess import Board, Move
from moonfish.config import Config
Expand Down Expand Up @@ -32,7 +30,7 @@ def __init__(self, config: Config):

def generate_board_and_moves(
self, og_board: Board, board_to_move_that_generates_it: DictProxy, layer: int
) -> List[Tuple[Board, Board, int]]:
) -> list[tuple[Board, Board, int]]:
"""
Generate all possible boards with their layer depth for each board.

Expand All @@ -48,7 +46,7 @@ def generate_board_and_moves(
- layer depth
"""
boards_and_moves = []
board = copy(og_board)
board = og_board.copy()

# if board has no legal moves, we leave it as is
# we need to run this board through negamax to get its value
Expand All @@ -69,7 +67,7 @@ def generate_board_and_moves(
else:
board_to_move_that_generates_it[board.fen()] = move
# add new board, original board, and current layer to our output
boards_and_moves.append((copy(board), og_board, layer + 1))
boards_and_moves.append((board.copy(), og_board, layer + 1))

board.pop()

Expand Down Expand Up @@ -101,7 +99,7 @@ def search_move(self, board: Board) -> Move:
negamax_arguments = [
(
board,
copy(self.config.negamax_depth) - START_LAYER,
self.config.negamax_depth - START_LAYER,
self.config.null_move,
shared_cache,
)
Expand Down
11 changes: 5 additions & 6 deletions moonfish/engines/lazy_smp.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from copy import copy
from multiprocessing import cpu_count, Manager, Pool

from chess import Board, Move
from moonfish.engines.alpha_beta import AlphaBeta
from moonfish.engines.alpha_beta import AlphaBeta, INF, NEG_INF


class LazySMP(AlphaBeta):
Expand All @@ -20,7 +19,7 @@ def search_move(self, board: Board) -> Move:
[
(
board,
copy(self.config.negamax_depth),
self.config.negamax_depth,
self.config.null_move,
shared_cache,
)
Expand All @@ -31,10 +30,10 @@ def search_move(self, board: Board) -> Move:
# return best move for our original board
return shared_cache[
(
board.fen(),
board._transposition_key(),
self.config.negamax_depth,
self.config.null_move,
float("-inf"),
float("inf"),
NEG_INF,
INF,
)
][1]
Loading