Source code for board

""" This module has the Board class.
"""
from typing import Any, Dict
from functools import singledispatchmethod
from enums import Color, Numbers, Positions, PieceType, Rank, Status
from pieces import Pawn, factory_piece, factory_set


[docs]class Board(): """Board class """ def __init__(self) -> None: """Constructor """ self._board: Dict[Any, Any] = {positions: None for positions in Positions.POSITIONS.value}
[docs] def add_piece(self, piece: PieceType, *, color: Color=Color.WHITE, position: tuple) -> None: """Add piece to the board. :param piece: Bishop, King, Knight, Pawn, Queen or Rook :type piece: PieceType :param color: ``Color.WHITE`` or ``Color.BLACK`` (default is ``Color.WHITE``) :type color: Color :param position: ``(file, rank)`` for position :type position: tuple """ self._board[position] = factory_piece(piece, color=color, position=position)
[docs] def add_piece_set(self) -> None: """Adds piece set with default positions by color to the board. """ for color in Color: self._board.update(factory_set(color=color))
@property def board(self) -> dict: """Returns the board dictionary with 64 key-value pairs. Keys are two-item tuples, ``(file, rank)``, and values are ``None`` (empty positions) or ``Piece`` (Bishop, King, Knight, Pawn, Queen or Rook). A read-only property. :getter: board dictionary :type: dict :raises AttributeError: setting read-only property """ return self._board
[docs] def check_end_position(self, start: tuple, end: tuple) -> tuple: """Check for capture or empty at end position and returns corresponding ``Status``. :param start: ``(file, rank)`` for ``start`` position :type end: tuple :param end: ``(file, rank)`` for ``end`` position :type start: tuple :return: ``Status.CAPTURE``, ``Status.INVALID`` or ``Status.VALID`` :rtype: ``Status`` """ if (target := self._board[end]): if self._board[start].color is not target.color: return (Status.CAPTURE, end) if self._board[end] is None: return (Status.VALID, tuple()) return (Status.INVALID, tuple())
[docs] @singledispatchmethod @staticmethod def convert_position(position): """Converts an algebriac notation string, ``'a1'``, to a tuple, ``(97, 1)``, or vice versa. :param value: two-character string or ``(file, rank)`` :type value: str | tuple :raises NotImplementedError: Types other than string or tuple. :raises ValueError: Not a two-character string or tuple. :return: ``(file, rank)`` or two-character string :rtype: tuple | string """ raise NotImplementedError(f"Non-supported value type: {position}")
@convert_position.register(str) @staticmethod def _(position: str) -> tuple: if len(position) != Numbers.TWO: raise ValueError(f"Not a two-character string: {position}") try: return Positions.ALGEBRIAC.value[position] except KeyError as exc: raise ValueError(f"Invalid string value: {position}") from exc @convert_position.register(tuple) @staticmethod def _(position: tuple) -> str: if len(position) != Numbers.TWO: raise ValueError(f"Not (file, rank) tuple: {position}") try: return Positions.FILE_RANK.value[position] except KeyError as exc: raise ValueError(f"Invalid tuple value: {position}") from exc
[docs] def has_empty_positions(self, start: tuple, end: tuple) -> bool: """Determines empty positions between ``start`` and ``end`` positions and returns Boolean. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``True`` or ``False`` :rtype: bool """ def next_diagonal(start, end, order): return start + 1 if order else end - 1 def next_straight(start, end): return start + 1 if start < end else end + 1 (file_start, rank_start), (file_end, rank_end) = start, end # check side-by-side postions if abs(file_start - file_end) == 1 or abs(rank_start - rank_end) == 1: return True # check vertical positions if file_start == file_end: rank_next = next_straight(rank_start, rank_end) while Numbers.ONE < rank_next <= Numbers.SEVEN: if self._board[file_start, rank_next]: return False rank_next += 1 # check horizontal positions elif rank_start == rank_end: file_next = next_straight(file_start, file_end) # TODO - double check added file conditional for castling while Numbers.A < file_next < Numbers.H and \ file_start < file_next < file_end: if self._board[file_next, rank_start]: return False file_next += 1 # check diagonal positions else: file_order = file_start < file_end rank_order = rank_start < rank_end if self._board[start].color: file_next = next_diagonal(file_start, file_end, file_order) rank_next = next_diagonal(rank_start, rank_end, rank_order) else: file_next = next_diagonal(file_end, file_start, file_order) rank_next = next_diagonal(rank_start, rank_end, rank_order) while Numbers.A < file_next < Numbers.H and \ Numbers.ONE < rank_next <= Numbers.SEVEN: if self._board[file_next, rank_next]: return False file_next = next_diagonal(file_next, file_next, file_order) rank_next = next_diagonal(rank_next, rank_next, rank_order) return True
[docs] @staticmethod def is_diagonal_move(start: tuple, end: tuple) -> bool: """Determines diagonal move and returns Boolean. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``True`` or ``False`` :rtype: bool """ (file_start, rank_start), (file_end, rank_end) = start, end return abs(file_end - file_start) == abs(rank_end - rank_start)
[docs] @staticmethod def is_short_move(start: int, end: int, *, limit: int=1) -> bool: """Determines short move (one or two positions) and returns Boolean. :param start: integer for ``end`` position :type start: int :param end: integer for ``start`` position :type end: int :param limit: integer, 1 (default value) or 2 :type limit: int :raises ValueError: invalid limit values :return: ``True`` or ``False`` :rtype: bool """ if not Numbers.ONE <= limit <= Numbers.TWO: msg = f"Invalid limit value: {limit}. Should be 1 or 2." raise ValueError(msg) return abs(end - start) == limit
def _move_piece(self, start: tuple, end: tuple) -> bool: """Move piece on the board and returns Boolean. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``True`` or ``False`` :rtype: bool """ status = dict(Bishop=self.validate_move_bishop, # type: ignore King=self.validate_move_king, Knight=self.validate_move_knight, Pawn=self.validate_move_pawn, Queen=self.validate_move_queen, Rook=self.validate_move_rook, ).get(type(self._board[start]), lambda *args: (Status.INVALID, tuple()))(start, end) match status[0]: case Status.INVALID: return False case Status.CAPTURE: # TODO use second value self._board[end], self._board[start] = self._board[start], None self._board[end].position = end case Status.CASTLING: #TODO in-check check pass case Status.CHECK: pass case Status.CHECKMATE: pass case Status.EN_PASSANT: self.swap_positions(start, end) self._board[end].position = end self._board[status[1]] = None case Status.VALID: self.swap_positions(start, end) self._board[end].position = end case _: return False return False
[docs] def swap_positions(self, start: tuple, end: tuple) -> None: """Swap two positions on the board. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple """ board = self._board board[end], board[start] = board[start], board[end]
[docs] def validate_move_bishop(self, start: tuple, end: tuple) -> tuple: """Validate move for bishop. One to seven positions, diagonally. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, or ``Status.CAPTURE`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ if self.is_diagonal_move(start, end): if self.has_empty_positions(start, end): return self.check_end_position(start, end) return (Status.INVALID, tuple())
[docs] def validate_move_king(self, start: tuple, end: tuple) -> tuple: """Validate move for the king. One position, any direction, or two positions horizontally when castling with a rook. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, ``Status.CASTLING`` with ``(file, rank)`` of rook, or ``Status.CAPTURE`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ (file_start, rank_start), (file_end, rank_end) = start, end # horizontal move if rank_start == rank_end: # one position if self.is_short_move(file_start, file_end): return self.check_end_position(start, end) # castling (all conditions must be true) if all([self._board[start].first_move, self._board[start].check is False, self.is_short_move(file_start, file_end, limit=2), self._board[(file_end - 1 if file_start < file_end else file_end + 1, rank_end)] is None, self._board[end] is None]): rook_end, rook_start = Positions.CASTLING.value[end] if (target := self._board[(rook_start)]) and target.first_move: if self.has_empty_positions(rook_start, start): return (Status.CASTLING, (rook_start, rook_end)) # vertical move if file_start == file_end: if self.is_short_move(rank_start, rank_end): return self.check_end_position(start, end) # diagonal move if self.is_diagonal_move(start, end): return self.check_end_position(start, end) return (Status.INVALID, tuple())
[docs] def validate_move_knight(self, start: tuple, end: tuple) -> tuple: """Validate move for knight. Move two positions, horizontally or vertically, and one position over. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, or ``Status.CAPTURE`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ def end_value(number): return file_end - number if file_start < file_end else \ file_end + number def start_value(number): return file_start + number if file_start < file_end else \ file_start - number (file_start, _), (file_end, _) = start, end # horizontal if file_start == end_value(2): if file_end == start_value(2): return self.check_end_position(start, end) # vertical if file_start == end_value(1): if file_end == start_value(1): return self.check_end_position(start, end) return (Status.INVALID, tuple())
[docs] def validate_move_pawn(self, start: tuple, end: tuple) -> tuple: """Validate move for pawn. Forward one position, two positions (first move only), or diagonally one position for en passant or capture. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, or ``Status.CAPTURE`` or ``Status.EN_PASSANT`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ (file_start, rank_start), (file_end, rank_end) = start, end middle = rank_end + 1 if file_start < file_end else rank_end - 1 # vertical if file_start == file_end: # one position if self.is_short_move(rank_start, rank_end): return self.check_end_position(start, end) # two positions if self._board[start].first_move and rank_start in Rank.PAWN: if self.is_short_move(rank_start, rank_end, limit=2): if self._board[(file_end, middle)] is None: if self._board[end] is None: return (Status.VALID, tuple()) # diagonal if self.is_diagonal_move(start, end): if self.is_short_move(file_start, file_end): # en passant (all conditions must be true) if all([self._board[end] is None, (target := self._board[(file_end, middle)]), isinstance(target, Pawn), self._board[start].color is not target.color]): return (Status.EN_PASSANT, (file_end, middle)) # capture if (target := self._board[end]): if self._board[start].color is not target.color: return (Status.CAPTURE, end) return (Status.INVALID, tuple())
[docs] def validate_move_queen(self, start: tuple, end: tuple) -> tuple: """Validate move for the queen. One to seven positions, any direction. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, or ``Status.CAPTURE`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ (file_start, rank_start), (file_end, rank_end) = start, end # horizontal if rank_start == rank_end: if self.has_empty_positions(start, end): return self.check_end_position(start, end) # vertical if file_start == file_end: if self.has_empty_positions(start, end): return self.check_end_position(start, end) # diagonal if self.is_diagonal_move(start, end): if self.has_empty_positions(start, end): return self.check_end_position(start, end) return (Status.INVALID, tuple())
[docs] def validate_move_rook(self, start: tuple, end: tuple) -> tuple: """Validate move for rook. One to seven positions, horizontal or vertical. Returns ``Status.INVALID`` or ``Status.VALID`` with an empty tuple, or ``Status.CAPTURE`` with ``(file, rank)`` of captured piece. :param start: ``(file, rank)`` for ``start`` position :type start: tuple :param end: ``(file, rank)`` for ``end`` position :type end: tuple :return: ``(Status, ())`` or ``(Status, (file, rank))`` :rtype: tuple """ (file_start, rank_start), (file_end, rank_end) = start, end # horizontal if rank_start == rank_end: if self.has_empty_positions(start, end): return self.check_end_position(start, end) # vertical if file_start == file_end: if self.has_empty_positions(start, end): return self.check_end_position(start, end) return (Status.INVALID, tuple())