"""
Module for the SyzygyTable class.
"""
from typing import Protocol
import chess.syzygy
from chipiron.environments.chess_env.board import IBoard
from chipiron.environments.chess_env.move.imove import moveKey
from chipiron.players.boardevaluators.over_event import HowOver, OverTags, Winner
[docs]class SyzygyTable[T_Board: IBoard](Protocol):
"""
A class representing a Syzygy tablebase for chess endgame analysis.
Attributes:
table_base (chess.syzygy.Tablebase): The Syzygy tablebase object.
Methods:
fast_in_table(board: boards.BoardChi) -> bool:
Check if the given board is suitable for fast tablebase lookup.
in_table(board: boards.BoardChi) -> bool:
Check if the given board is in the tablebase.
get_over_event(board: boards.BoardChi) -> tuple[Winner, HowOver]:
Get the winner and how the game is over for the given board.
val(board: boards.BoardChi) -> int:
Get the value of the given board from the tablebase.
value_white(board: boards.BoardChi) -> int:
Get the value of the given board for the white player.
get_over_tag(board: boards.BoardChi) -> OverTags:
Get the over tag for the given board.
string_result(board: boards.BoardChi) -> str:
Get the string representation of the result for the given board.
dtz(board: boards.BoardChi) -> int:
Get the distance-to-zero (DTZ) value for the given board.
best_move(board: boards.BoardChi) -> chess.Move:
Get the best move according to the tablebase for the given board.
"""
[docs] def fast_in_table(self, board: T_Board) -> bool:
"""
Check if the given board is suitable for fast tablebase lookup.
Args:
board (boards.BoardChi): The board to check.
Returns:
bool: True if the board is suitable for fast lookup, False otherwise.
"""
return board.number_of_pieces_on_the_board() < 6
[docs] def in_table(self, board: T_Board) -> bool:
"""
Check if the given board is in the tablebase.
Args:
board (boards.BoardChi): The board to check.
Returns:
bool: True if the board is in the tablebase, False otherwise.
"""
try:
self.wdl(board=board)
except KeyError:
return False
return True
[docs] def wdl(self, board: T_Board) -> int: ...
[docs] def get_over_event(self, board: T_Board) -> tuple[Winner, HowOver]:
"""
Get the winner and how the game is over for the given board.
Args:
board (boards.BoardChi): The board to analyze.
Returns:
tuple[Winner, HowOver]: The winner and how the game is over.
"""
val: int = self.val(board)
who_is_winner_: Winner = Winner.NO_KNOWN_WINNER
how_over_: HowOver
if val != 0:
how_over_ = HowOver.WIN
if val > 0:
who_is_winner_ = (
Winner.WHITE if board.turn == chess.WHITE else Winner.BLACK
)
if val < 0:
who_is_winner_ = (
Winner.WHITE if board.turn == chess.BLACK else Winner.BLACK
)
else:
how_over_ = HowOver.DRAW
return who_is_winner_, how_over_
[docs] def val(self, board: T_Board) -> int:
"""
Get the value of the given board from the tablebase.
Args:
board (boards.BoardChi): The board to get the value for.
Returns:
int: The value of the board from the tablebase.
"""
# tablebase.probe_wdl Returns 2 if the side to move is winning, 0 if the position is a draw and -2 if the side to move is losing.
val: int = self.wdl(board)
return val
[docs] def value_white(self, board: T_Board) -> int:
"""
Get the value of the given board for the white player.
Args:
board (boards.BoardChi): The board to get the value for.
Returns:
int: The value of the board for the white player.
"""
# tablebase.probe_wdl Returns 2 if the side to move is winning, 0 if the position is a draw and -2 if the side to move is losing.
val: int = self.wdl(board)
if board.turn == chess.WHITE:
return val * 100000
else:
return val * -10000
[docs] def get_over_tag(self, board: T_Board) -> OverTags:
"""
Get the over tag for the given board.
Args:
board (boards.BoardChi): The board to get the over tag for.
Returns:
OverTags: The over tag for the board.
"""
val = self.wdl(board)
if val > 0:
if board.turn == chess.WHITE:
return OverTags.TAG_WIN_WHITE
else:
return OverTags.TAG_WIN_BLACK
elif val == 0:
return OverTags.TAG_DRAW
else:
if board.turn == chess.WHITE:
return OverTags.TAG_WIN_BLACK
else:
return OverTags.TAG_WIN_WHITE
[docs] def string_result(self, board: T_Board) -> str:
"""
Get the string representation of the result for the given board.
Args:
board (boards.BoardChi): The board to get the result for.
Returns:
str: The string representation of the result.
"""
val = self.wdl(board)
player_to_move = "white" if board.turn == chess.WHITE else "black"
if val > 0:
return "WIN for player " + player_to_move
elif val == 0:
return "DRAW"
else:
return "LOSS for player " + player_to_move
[docs] def dtz(self, board: T_Board) -> int:
"""
Get the distance-to-zero (DTZ) value for the given board.
Args:
board (boards.BoardChi): The board to get the DTZ value for.
Returns:
int: The DTZ value for the board.
"""
...
[docs] def best_move(self, board: T_Board) -> moveKey:
"""
Get the best move according to the tablebase for the given board.
Args:
board (boards.BoardChi): The board to find the best move for.
Returns:
chess.Move: The best move according to the tablebase.
"""
all_moves: list[moveKey] = board.legal_moves_.get_all()
# avoid draws by 50 move rules in winning position, # otherwise look
# for it to make it last and preserve pieces in case of mistake by opponent
best_value = -1000000000000000000000
assert all_moves
best_move: moveKey = all_moves[0]
for move in all_moves:
board_copy: T_Board = board.copy(stack=True)
board_copy.play_move_key(move=move)
val_player_next_board = self.val(board_copy)
val_player_node = -val_player_next_board
dtz_player_next_board = self.dtz(board_copy)
dtz_player_node = -dtz_player_next_board
if val_player_node > 0: # winning position
new_value = board.is_zeroing(move) * 100 - dtz_player_node + 1000
elif val_player_node == 0:
new_value = -board.is_zeroing(move) * 100 + dtz_player_node
elif val_player_node < 0:
new_value = -board.is_zeroing(move) * 100 + dtz_player_node - 1000
if new_value > best_value:
best_value = new_value
best_move = move
return best_move