Source code for en_pyssant._board

# -*- coding: utf-8 -*-
#
# Copyright (C) 2017-2018  Carmen Bianca Bakker <carmen@carmenbianca.eu>
# Copyright (C) 2017  Stefan Bakker <s.bakker777@gmail.com>
#
# This file is part of En Pyssant, available from its original location:
# <https://gitlab.com/carmenbianca/en-pyssant>.
#
# En Pyssant is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# En Pyssant is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with En Pyssant.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0+

"""A board representation and implementation."""

import operator
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from functools import lru_cache
from io import StringIO
from typing import Any, Iterator, List, Optional, Tuple, Union

from ._core import Piece, Side, Square, Type  # pylint: disable=unused-import
from ._util import ALL_SQUARES, validate_fen_board


[docs]class Board(metaclass=ABCMeta): """An abstract base class for a chess board implementation. A chess board is immutable and need implement only :meth:`get`, :meth:`put` and :meth:`__init__`. :meth:`__init__` must take no positional arguments and - without any arguments - create an initial chess board. All other methods are already implemented. """ def __init__(self): self._hash = None
[docs] @classmethod def from_fen(cls, fen: str) -> 'Board': """Generate a :class:`Board` from the board portion of a Forsyth-Edwards Notation string. :param fen: First part of Forsyth-Edwards Notation :return: A board. :raise ValueError: Input is invalid. """ board = cls() fen = fen.split()[0] chunks = fen.split('/') files = 'abcdefgh' if not validate_fen_board(fen): raise ValueError('{} is not a valid notation'.format(fen)) # From white to black for rank, chunk in zip(range(1, 9), reversed(chunks)): file_position = 0 for char in chunk: if char.isnumeric(): for _ in range(int(char)): board = board.put( '{}{}'.format(files[file_position], rank), None) file_position += 1 else: piece = Piece.from_str(char) board = board.put( '{}{}'.format(files[file_position], rank), piece) file_position += 1 return board
[docs] @abstractmethod def get(self, square: str) -> Optional[Piece]: """Return piece at *square*. Return :const:`None` if no piece exists at square. :param square: Square in algebraic notation. :return: Piece at square. """
[docs] @abstractmethod def put(self, square: str, piece: Optional[Piece]) -> 'Board': """Put *piece* on *square*. Override any existing pieces. Return a new board. :param square: Square in algebraic notation. :param piece: Piece to be placed on square. :return: A new board. """
[docs] def pretty(self) -> str: """ >>> print(DictBoard().pretty()) A B C D E F G H 8 r n b q k b n r 7 p p p p p p p p 6 . . . . . . . . 5 . . . . . . . . 4 . . . . . . . . 3 . . . . . . . . 2 P P P P P P P P 1 R N B Q K B N R :return: A pretty representation of the board. """ result = StringIO() result.write(' A B C D E F G H\n') for rank in reversed(list(zip(*[iter(ALL_SQUARES)] * 8))): row = [rank[0][1]] for square in rank: piece = self.get(square) if not piece: piece = '.' else: piece = str(piece) row.append(piece) result.write(' '.join(row)) result.write('\n') return result.getvalue().rstrip()
[docs] def all_pieces(self) -> Iterator[Tuple[Square, Optional[Piece]]]: """Yield all squares and their respective pieces, from a1 to h8. Increment files before ranks. Comparable to :func:`enumerate`, except for squares and pieces. :return: All squares and their respective pieces. """ return ((square, self.get(square)) for square in ALL_SQUARES)
def __getitem__(self, key: str) -> Optional[Piece]: return self.get(key) def __eq__(self, other: Any) -> bool: for square in ALL_SQUARES: try: if self.get(square) != other.get(square): return False except: # pylint: disable=bare-except return False return True def __ne__(self, other: Any) -> bool: return not self.__eq__(other) def __repr__(self) -> str: # First divide up in chunks of 8. chunks = [ [self.get('{}{}'.format(file_, rank)) for file_ in 'abcdefgh'] for rank in reversed(range(1, 9))] new_chunks = [] for chunk in chunks: new_chunk = [] counter = 0 # Just add each piece to *new_chunk*, unless it's an empty square. # Increment *counter* if the square is empty. Add the value of # counter to *new_chunk* when the loop hits a piece, or if it hits # the end. for piece in chunk: if piece: if counter: new_chunk.append(str(counter)) counter = 0 new_chunk.append( piece.type.value.upper() if piece.side is Side.WHITE else piece.type.value) else: counter += 1 if counter: new_chunk.append(str(counter)) # Join the chunks into a single string and append them to # *new_chunks*. new_chunks.append(''.join(new_chunk)) # Join the chunks with '/'. return '/'.join(new_chunks) def __hash__(self): if self._hash is not None: return self._hash result = 0 for i, (_, piece) in enumerate(self.all_pieces()): result += (i + 1) * hash(piece) self._hash = result return self._hash
def all_board_classes() -> List[Board]: """Return a list of all defined board classes, excluding the base class.""" result = [] for key, value in globals().items(): if key.endswith('Board') and key != 'Board': result.append(value) return sorted(result, key=operator.attrgetter('__name__')) @lru_cache(maxsize=64) def algebraic_to_index(square: Union[Square, str]) -> int: """Convert the square in algebraic notation to the corresponding index in our internal representation. :param square: Algebraic notation square. :return: Index on board. :raise ValueError: Input is invalid. >>> algebraic_to_index('a1') 0 >>> algebraic_to_index('h8') 63 >>> algebraic_to_index('9a') Traceback (most recent call last): ... ValueError: '9a' is not a valid square """ square = Square(square) rank = square.rank - 1 file_ = ord(square.file) - 97 return rank * 8 + (file_) ALGEBRAIC_TO_INDEX_MAP = dict() for _square in ALL_SQUARES: ALGEBRAIC_TO_INDEX_MAP[_square] = algebraic_to_index(_square) _BitBoardTup = namedtuple( '_BitBoardTup', [ 'kings', 'queens', 'rooks', 'bishops', 'knights', 'pawns', 'whites', 'blacks']) # Board looks reversed! This is normal. Most chess board implementations use # a1 as index 0. _INITIAL_STRINGBOARD = ( 'RNBQKBNR' # 0-7, a1-h1 'PPPPPPPP' # 8-15, a2-h2 ' ' # 16-23, a3-h3 ' ' # 24-31, a4-h4 ' ' # 32-39, a5-h5 ' ' # 40-47, a6-h6 'pppppppp' # 48-55, a7-h7 'rnbqkbnr' # 56-63, a8-h8 ) _INITIAL_BYTESBOARD = _INITIAL_STRINGBOARD.encode('ascii') _INITIAL_DICTBOARD = dict() _INITIAL_LISTBOARD = list() _INITIAL_KINGS = 0 _INITIAL_QUEENS = 0 _INITIAL_ROOKS = 0 _INITIAL_BISHOPS = 0 _INITIAL_KNIGHTS = 0 _INITIAL_PAWNS = 0 _INITIAL_WHITES = 0 _INITIAL_BLACKS = 0 for _char, _square in zip(_INITIAL_STRINGBOARD, ALL_SQUARES): if _char == ' ': _piece = None else: _piece = Piece.from_str(_char) _INITIAL_DICTBOARD[_square] = _piece _INITIAL_LISTBOARD.append(_piece) if not _piece: continue _bit = 1 << ALGEBRAIC_TO_INDEX_MAP[_square] if _piece.type is Type.KING: _INITIAL_KINGS = _INITIAL_KINGS | _bit elif _piece.type is Type.QUEEN: _INITIAL_QUEENS = _INITIAL_QUEENS | _bit elif _piece.type is Type.ROOK: _INITIAL_ROOKS = _INITIAL_ROOKS | _bit elif _piece.type is Type.BISHOP: _INITIAL_BISHOPS = _INITIAL_BISHOPS | _bit elif _piece.type is Type.KNIGHT: _INITIAL_KNIGHTS = _INITIAL_KNIGHTS | _bit elif _piece.type is Type.PAWN: _INITIAL_PAWNS = _INITIAL_PAWNS | _bit if _piece.side is Side.WHITE: _INITIAL_WHITES = _INITIAL_WHITES | _bit else: _INITIAL_BLACKS = _INITIAL_BLACKS | _bit _INITIAL_TUPLEBOARD = tuple(_INITIAL_LISTBOARD) _INITIAL_BITBOARD = _BitBoardTup( _INITIAL_KINGS, _INITIAL_QUEENS, _INITIAL_ROOKS, _INITIAL_BISHOPS, _INITIAL_KNIGHTS, _INITIAL_PAWNS, _INITIAL_WHITES, _INITIAL_BLACKS)
[docs]class BitBoard(Board): """A bitboard representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_BITBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: bit = 1 << ALGEBRAIC_TO_INDEX_MAP[square] side = None type_ = None if bit & self._board.whites: side = Side.WHITE elif bit & self._board.blacks: side = Side.BLACK else: return None if bit & self._board.kings: type_ = Type.KING elif bit & self._board.queens: type_ = Type.QUEEN elif bit & self._board.rooks: type_ = Type.ROOK elif bit & self._board.bishops: type_ = Type.BISHOP elif bit & self._board.knights: type_ = Type.KNIGHT elif bit & self._board.pawns: type_ = Type.PAWN return Piece(type_, side)
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'BitBoard': index = ALGEBRAIC_TO_INDEX_MAP[square] bit = 1 << index # Pop bit at index. mask = ~bit kings = self._board.kings & mask queens = self._board.queens & mask rooks = self._board.rooks & mask bishops = self._board.bishops & mask knights = self._board.knights & mask pawns = self._board.pawns & mask whites = self._board.whites & mask blacks = self._board.blacks & mask if piece is not None: if piece.type is Type.KING: kings = kings | bit elif piece.type is Type.QUEEN: queens = queens | bit elif piece.type is Type.ROOK: rooks = rooks | bit elif piece.type is Type.BISHOP: bishops = bishops | bit elif piece.type is Type.KNIGHT: knights = knights | bit elif piece.type is Type.PAWN: pawns = pawns | bit if piece.side is Side.WHITE: whites = whites | bit elif piece.side is Side.BLACK: blacks = blacks | bit return self.__class__(_board=_BitBoardTup( kings, queens, rooks, bishops, knights, pawns, whites, blacks))
[docs]class BytesBoard(Board): """A simple bytes-based board representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_BYTESBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: i = self._board[ALGEBRAIC_TO_INDEX_MAP[square]] letter = bytes((i,)) if letter == b' ': return None rtype = Type(letter.lower().decode('ascii')) rside = Side.WHITE if letter.isupper() else Side.BLACK return Piece(rtype, rside)
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'BytesBoard': if piece is None: letter = b' ' else: letter = str(piece).encode('ascii') index = ALGEBRAIC_TO_INDEX_MAP[square] return self.__class__(_board=b''.join([ self._board[:index], letter, self._board[index + 1:] ]))
[docs]class DictBoard(Board): """A simple dictionary-based board representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_DICTBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: return self._board[square]
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'DictBoard': # Shallow copy. This is acceptable, because all values in it are # immutable. new_dict = self._board.copy() new_dict[Square(square)] = piece return self.__class__(_board=new_dict)
[docs]class ListBoard(Board): """A simple list-based board representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_LISTBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: return self._board[ALGEBRAIC_TO_INDEX_MAP[square]]
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'DictBoard': # Shallow copy. This is acceptable, because all values in it are # immutable. new_list = self._board[:] new_list[ALGEBRAIC_TO_INDEX_MAP[square]] = piece return self.__class__(_board=new_list)
[docs]class StringBoard(Board): """A simple string-based board representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_STRINGBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: letter = self._board[ALGEBRAIC_TO_INDEX_MAP[square]] if letter == ' ': return None return Piece.from_str(letter)
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'StringBoard': if piece is None: letter = ' ' else: letter = str(piece) index = ALGEBRAIC_TO_INDEX_MAP[square] return self.__class__(_board='{}{}{}'.format( self._board[:index], letter, self._board[index + 1:] ))
[docs]class TupleBoard(Board): """A tuple-based board representation.""" def __init__(self, **kwargs): super().__init__() self._board = kwargs.get('_board', _INITIAL_TUPLEBOARD)
[docs] def get(self, square: str) -> Optional[Piece]: return self._board[ALGEBRAIC_TO_INDEX_MAP[square]]
[docs] def put(self, square: str, piece: Optional[Piece]) -> 'DictBoard': # Shallow copy. This is acceptable, because all values in it are # immutable. index = ALGEBRAIC_TO_INDEX_MAP[square] new_tuple = self._board[:index] + (piece,) + self._board[index + 1:] return self.__class__(_board=new_tuple)