Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More efficient move calculation for Gaviota and Syzygy #1069

Merged
merged 2 commits into from
Jan 9, 2025
Merged
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
44 changes: 24 additions & 20 deletions lib/engine_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
COMMANDS_TYPE, MOVE, InfoStrDict, InfoDictKeys, InfoDictValue, GO_COMMANDS_TYPE, EGTPATH_TYPE,
ENGINE_INPUT_ARGS_TYPE, ENGINE_INPUT_KWARGS_TYPE)
from extra_game_handlers import game_specific_options
from operator import itemgetter
from typing import Any, Optional, Union, Literal, cast
from types import TracebackType
LICHESS_TYPE = Union[lichess.Lichess, test_bot.lichess.Lichess]
Expand Down Expand Up @@ -1149,8 +1150,10 @@ def get_syzygy(board: chess.Board, game: model.Game,
or chess.popcount(board.occupied) > syzygy_cfg.max_pieces
or board.uci_variant not in ["chess", "antichess", "atomic"]):
return None, -3

move: Union[chess.Move, list[chess.Move]]
move_quality = syzygy_cfg.move_quality

with chess.syzygy.open_tablebase(syzygy_cfg.paths[0]) as tablebase:
for path in syzygy_cfg.paths[1:]:
tablebase.add_directory(path)
Expand All @@ -1164,13 +1167,12 @@ def get_syzygy(board: chess.Board, game: model.Game,
move = [chess_move for chess_move, dtz in good_moves]
logger.info(f"Suggesting moves from syzygy (wdl: {best_wdl}) for game {game.id}")
return move, best_wdl
else:
# There can be multiple moves with the same dtz.
best_dtz = min([dtz for chess_move, dtz in good_moves])
best_moves = [chess_move for chess_move, dtz in good_moves if dtz == best_dtz]
move = random.choice(best_moves)
logger.info(f"Got move {move.uci()} from syzygy (wdl: {best_wdl}, dtz: {best_dtz}) for game {game.id}")
return move, best_wdl
# There can be multiple moves with the same dtz.
best_dtz = min(good_moves, key=itemgetter(1))[1]
best_moves = [chess_move for chess_move, dtz in good_moves if dtz == best_dtz]
move = random.choice(best_moves)
logger.info(f"Got move {move.uci()} from syzygy (wdl: {best_wdl}, dtz: {best_dtz}) for game {game.id}")
return move, best_wdl
except KeyError:
# Attempt to only get the WDL score. It returns moves of quality="suggest", even if quality is set to "best".
try:
Expand Down Expand Up @@ -1222,15 +1224,18 @@ def get_gaviota(board: chess.Board, game: model.Game,
or chess.popcount(board.occupied) > gaviota_cfg.max_pieces
or board.uci_variant != "chess"):
return None, -3

move: Union[chess.Move, list[chess.Move]]
move_quality = gaviota_cfg.move_quality

# Since gaviota TBs use dtm and not dtz, we have to put a limit where after it the position are considered to have
# a syzygy wdl=1/-1, so the positions are draws under the 50 move rule. We use min_dtm_to_consider_as_wdl_1 as a
# second limit, because if a position has 5 pieces and dtm=110 it may take 98 half-moves, to go down to 4 pieces and
# another 12 to mate, so this position has a syzygy wdl=2/-2. To be safe, the first limit is 100 moves, which
# guarantees that all moves have a syzygy wdl=2/-2. Setting min_dtm_to_consider_as_wdl_1 to 100 will disable it
# because dtm >= dtz, so if abs(dtm) < 100 => abs(dtz) < 100, so wdl=2/-2.
min_dtm_to_consider_as_wdl_1 = gaviota_cfg.min_dtm_to_consider_as_wdl_1

with chess.gaviota.open_tablebase(gaviota_cfg.paths[0]) as tablebase:
for path in gaviota_cfg.paths[1:]:
tablebase.add_directory(path)
Expand All @@ -1240,7 +1245,7 @@ def get_gaviota(board: chess.Board, game: model.Game,

best_wdl = max(map(dtm_to_gaviota_wdl, moves.values()))
good_moves = [(move, dtm) for move, dtm in moves.items() if dtm_to_gaviota_wdl(dtm) == best_wdl]
best_dtm = min([dtm for move, dtm in good_moves])
best_dtm = min(good_moves, key=itemgetter(1))[1]

pseudo_wdl = dtm_to_wdl(best_dtm, min_dtm_to_consider_as_wdl_1)
if move_quality == "suggest":
Expand All @@ -1249,7 +1254,7 @@ def get_gaviota(board: chess.Board, game: model.Game,
move = [chess_move for chess_move, dtm in best_moves]
logger.info(f"Suggesting moves from gaviota (pseudo wdl: {pseudo_wdl}) for game {game.id}")
else:
move, dtm = random.choice(best_moves)
move, dtm = best_moves[0]
logger.info(f"Got move {move.uci()} from gaviota (pseudo wdl: {pseudo_wdl}, dtm: {dtm})"
f" for game {game.id}")
else:
Expand Down Expand Up @@ -1295,21 +1300,20 @@ def good_enough_gaviota_moves(good_moves: list[tuple[chess.Move, int]], best_dtm
# want to avoid these positions, if there is a move where even when we add the halfmove_clock the
# dtz is still <100.
return [(move, dtm) for move, dtm in good_moves if dtm < 100]
elif best_dtm < min_dtm_to_consider_as_wdl_1:
if best_dtm < min_dtm_to_consider_as_wdl_1:
# If a move had wdl=2 and dtz=98, but halfmove_clock is 4 then the real wdl=1 and dtz=102, so we
# want to avoid these positions, if there is a move where even when we add the halfmove_clock the
# dtz is still <100.
return [(move, dtm) for move, dtm in good_moves if dtm < min_dtm_to_consider_as_wdl_1]
elif best_dtm <= -min_dtm_to_consider_as_wdl_1:
if best_dtm <= -min_dtm_to_consider_as_wdl_1:
# If a move had wdl=-2 and dtz=-98, but halfmove_clock is 4 then the real wdl=-1 and dtz=-102, so we
# want to only choose between the moves where the real wdl=-1.
return [(move, dtm) for move, dtm in good_moves if dtm <= -min_dtm_to_consider_as_wdl_1]
elif best_dtm <= -100:
if best_dtm <= -100:
# If a move had wdl=-2 and dtz=-98, but halfmove_clock is 4 then the real wdl=-1 and dtz=-102, so we
# want to only choose between the moves where the real wdl=-1.
return [(move, dtm) for move, dtm in good_moves if dtm <= -100]
else:
return good_moves
return good_moves


def piecewise_function(range_definitions: list[tuple[float, Literal["e", "i"], int]], last_value: int,
Expand Down Expand Up @@ -1366,9 +1370,9 @@ def score_syzygy_moves(board: chess.Board,
"""Score all the moves using syzygy egtbs."""
moves = {}
for move in board.legal_moves:
board_copy = board.copy()
board_copy.push(move)
moves[move] = scorer(tablebase, board_copy)
board.push(move)
moves[move] = scorer(tablebase, board)
board.pop()
return moves


Expand All @@ -1380,7 +1384,7 @@ def score_gaviota_moves(board: chess.Board,
"""Score all the moves using gaviota egtbs."""
moves = {}
for move in board.legal_moves:
board_copy = board.copy()
board_copy.push(move)
moves[move] = scorer(tablebase, board_copy)
board.push(move)
moves[move] = scorer(tablebase, board)
board.pop()
return moves
Loading