from __future__ import annotations
import types
from typing import Any
import numpy as np
from draughts.boards.base import BaseBoard
from draughts.models import Color, Figure
from draughts.move import Move
from draughts.utils import (
get_diagonal_moves,
get_short_diagonal_moves,
logger,
)
# fmt: off
SQUARES= [ B10, D10, F10, H10, J10,
A9, C9, E9, G9, I9,
B8, D8, F8, H8, J8,
A7, C7, E7, G7, I7,
B6, D6, F6, H6, J6,
A5, C5, E5, G5, I5,
B4, D4, F4, H4, J4,
A3, C3, E3, G3, I3,
B2, D2, F2, H2, J2,
A1, C1, E1, G1, I1] = range(50)
# fmt: on
[docs]
class Board(BaseBoard):
"""
**Board for Standard (international) checkers.**
Game rules:
- Board size: 10x10
- Any piece can capture backwards and forwards
- Capture is mandatory
- King can move along the diagonal any number of squares
**Winning and drawing**
- A player wins the game when the opponent no longer has any valid moves.
This can be either because all of the player's pieces have been captured,
or because they are all blocked and thus have no more squares available.
- If the same position appears on the board for the third time,
with the same side to move, the game is considered drawn by threefold repetition.
- The game is drawn when both players make 25 consecutive king moves without capturing.
When one player has only a king left, and the other player three pieces including at least
one king (three kings, two kings and a man, or one king and two men),
the game is drawn after both players made 16 moves.
- When one player has only a king left, and the other player two pieces
or less including at least one king (one king, two kings, or one king and a man),
the game is drawn after both players made 5 moves.
"""
GAME_TYPE = 20
STARTING_POSITION = np.array([1] * 20 + [0] * 10 + [-1] * 20, dtype=np.int8)
STARTING_COLOR = Color.WHITE
VARIANT_NAME = "Standard (international) checkers"
ROW_IDX = {val: val // 5 for val in range(len(STARTING_POSITION))}
COL_IDX = {val: val % 10 for val in range(len(STARTING_POSITION))}
# def __init__(
# self, starting_position=STARTING_POSITION, turn=STARTING_COLOR, *args, **kwargs
# ) -> None:
# super().__init__(starting_position, turn, *args, **kwargs)
@property
def is_draw(self) -> bool:
return (
self.is_threefold_repetition
or self.is_5_moves_rule
or self.is_16_moves_rule
or self.is_25_moves_rule
)
@property
def is_25_moves_rule(self) -> bool:
if self.halfmove_clock < 50:
return False
logger.debug("25 moves rule")
return True
# return self.halfmove_clock >= 50
@property
def is_16_moves_rule(self) -> bool:
if self.halfmove_clock < 32:
return False
if len(self._pos[self._pos != Figure.EMPTY]) > 4:
return False
if np.abs(self._pos).sum() < Figure.KING.value * 2 + Figure.MAN.value * 2:
return False
logger.debug("16 moves rule")
return True
@property
def is_5_moves_rule(self) -> bool:
# if count of pieces is not 3 or 4
if len(self._pos[self._pos != Figure.EMPTY]) > 3:
return False
if np.abs(self._pos).sum() < Figure.KING.value * 2 + Figure.MAN.value:
return False
if self.halfmove_clock < 10:
return False
logger.debug("5 moves rule")
return True
@property
def legal_moves(self) -> list[Move]:
all_moves = []
is_capture_mandatory = False
squares_list = np.transpose(np.nonzero(self._pos * self.turn.value > 0))
for square in squares_list.flatten():
moves = self._legal_moves_from(square, is_capture_mandatory)
# if not is_capture_mandatory and any( # TODO this should optimize the search
# [len(move.square_list) > 0 for move in moves]
# ):
# is_capture_mandatory = True
all_moves.extend(moves)
return [mv for mv in all_moves if len(mv) == max(len(m) for m in all_moves)]
def _get_man_legal_moves_from(
self, square: int, is_capture_mandatory: bool
) -> list:
moves = []
# white can move only on even directions
for idx, direction in enumerate(self.DIAGONAL_LONG_MOVES[square]):
if (
len(direction) > 0
and (self.turn.value + idx)
in [-1, 0, 3, 4] # TERRIBLE HACK get only directions for given piece
and self._pos[direction[0]] == Figure.EMPTY
and not is_capture_mandatory
):
moves.append(Move([square, direction[0]]))
elif (
len(direction) > 1
and self._pos[direction[0]] * self.turn.value < 0
and self._pos[direction[1]] == Figure.EMPTY
):
move = Move(
[square, direction[1]], [direction[0]], [self._pos[direction[0]]]
)
# moves.append(move)
self.push(move, False)
new_moves = [
move + m for m in self._get_man_legal_moves_from(direction[1], True)
]
moves += [move] if len(new_moves) == 0 else new_moves
self.pop(False)
return moves
def _get_king_legal_moves_from(
self, square: int, is_capture_mandatory: bool
) -> list[Move]:
moves = []
for direction in self.DIAGONAL_SHORT_MOVES[square]:
for idx, target in enumerate(direction):
if (
len(direction) > idx + 1
and self._pos[target] * self.turn.value < 0
and self._pos[direction[idx + 1]] == Figure.EMPTY
):
i = idx + 1
while (
i < len(direction) and self._pos[direction[i]] == Figure.EMPTY
):
move = Move(
[square, direction[i]], [target], [self._pos[target]]
)
moves.append(move)
self.push(move, False)
moves += [
move + m
for m in self._get_king_legal_moves_from(direction[i], True)
]
# if one move is longer then others return only this one
self.pop(False)
max_len = max([len(m) for m in moves])
moves = [m for m in moves if len(m) == max_len]
i += 1
break
if (
self._pos[target] == Figure.EMPTY.value and not is_capture_mandatory
): # casual move
moves.append(Move([square, target]))
else:
break
return moves
def _legal_moves_from(self, square: int, is_capture_mandatory=False) -> list[Move]:
entity = Figure(self._pos[square])
if abs(entity) == Figure.MAN:
moves = self._get_man_legal_moves_from(square, is_capture_mandatory)
else:
moves = self._get_king_legal_moves_from(square, is_capture_mandatory)
if is_capture_mandatory:
moves = [move for move in moves if len(move.captured_list) > 0]
return moves
if __name__ == "__main__":
board = Board()
for i in range(10):
# random move
move = np.random.choice(list(board.legal_moves))
board.push(move)
print(board.pdn)