+# ChessBoard.py
+
+import json
+import os
+from copy import deepcopy, copy
+from functools import wraps
+from typing import Union, List, Dict, Generator
+
+from .ChessVector import ChessVector
+from .Pieces import *
+from .Moves import *
+from .configurations import ClassicConfig, FourPlayerConfig
+from .Utils import countAlpha, getResourcePath
+from .Exceptions import *
+
+
+def _defaultColors(func):
+ @wraps(func)
+ def wrapper(self, *colors):
+ if not colors:
+ colors = self.getColors()
+ returned = func(self, *colors)
+ if isinstance(returned, dict) and len(returned) == 1:
+ returned = returned.pop(*colors)
+ return returned
+ return wrapper
+
+
+class Board():
+ """Board object for storing and moving pieces
+
+ :param config: Board configuration (defaults to emtpy board)
+ """
+
+ def __init__(self, config={}):
+
+ self._board = []
+ with open(getResourcePath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "configurations/DefaultConfig.JSON")), "r") as default:
+ dConfig = json.load(default)
+
+ self._rows = config.get("rows") or dConfig.get("rows")
+ self._cols = config.get("cols") or dConfig.get("cols")
+ self._pieces = config.get("pieces") or dConfig.get("pieces")
+ self._moves = config.get("moves") or dConfig.get("moves")
+ self._promoteTo = config.get("promoteTo") or dConfig.get("promoteTo")
+ self._promoteFrom = config.get("promoteFrom") or dConfig.get("promoteFrom")
+ self._promoteAt = config.get("promoteAt") or dConfig.get("promteAt")
+ self._turnorder = config.get("turnorder") or dConfig.get("turnorder")
+
+ try:
+ self.currentTurn = self._turnorder[0]
+ except IndexError:
+ self.currentTurn = None
+ self._board = [[Empty(ChessVector((row, col))) for col in range(self._cols)] for row in range(self._rows)]
+
+ for color, pieceList in self._pieces.items():
+ for piece in pieceList:
+ self[piece.vector] = piece
+
+ for vec in config.get("disabled") or dConfig.get("disabled"):
+ self[vec] = Disabled(vec)
+
+ self._checks = {key: False for key in self._pieces.keys()}
+ self._checkmates = copy(self._checks)
+ self._kings = {key: [piece for piece in self._pieces[key] if isinstance(piece, King)] for key in self._pieces.keys()}
+ self._history = []
+
+ self.checkForCheck()
+
+ def __eq__(self, other):
+ for p1, p2 in zip(self, other):
+ if type(p1) == type(p2):
+ continue
+ else:
+ break
+
+ else:
+ return True
+ return False
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __iter__(self):
+ """Iterates through all positions in board
+
+ Use iterPieces() method to iterate through pieces of board.
+ """
+ for p in [p for row in self._board for p in row]:
+ yield p
+
+ def __str__(self):
+ string = "\n"
+ ending = "\t|\n\n\t\t__" + ("\t__" * self._cols) + "\n\n\t\t"
+ alpha = countAlpha()
+
+ for row in self._board:
+
+ num, char = next(alpha)
+
+ string += str(self._rows - num) + "\t|\t\t"
+
+ for piece in row:
+ string += str(piece) + "\t"
+
+ string += "\n\n"
+
+ ending += "\t" + char.upper()
+
+ return string + ending + "\n\n"
+
+ def __setitem__(self, index, item):
+
+ try:
+ iter(item)
+ except TypeError:
+ item = [item]
+
+ try:
+ iter(index)
+ except TypeError:
+ index = [index]
+
+ if len(index) != len(item):
+ raise ValueError("List index expected {0} values to unpack but {1} were given".format(
+ len(item), len(index)))
+
+ for i, vec in enumerate(index):
+
+ if isinstance(self._board[vec.row][vec.col], Disabled):
+ raise DisabledError(vec.getStr(self))
+
+ item1 = self._board[vec.row][vec.col]
+ item2 = item[i]
+
+ if not isinstance(item2, Disabled):
+
+ if not isinstance(item1, Empty):
+ self._removePiece(item1)
+
+ if not isinstance(item2, Empty):
+
+ if item2 in self.iterPieces(item2.color):
+ pass
+ else:
+ self._addPiece(item2, vec)
+
+ self._board[vec.row][vec.col] = item2
+
+ def __getitem__(self, index):
+ res = []
+
+ try:
+ iter(index)
+ except TypeError:
+ index = [index]
+
+ for vec in index:
+ if isinstance(self._board[vec.row][vec.col], Disabled):
+ raise DisabledError(vec.getStr(self))
+ res.append(self._board[vec.row][vec.col])
+
+ if len(res) == 1:
+ return res.pop()
+ else:
+ return res
+
+ def getRows(self) -> int:
+ """Get rows in board
+
+ :returns: Number of rows in board
+ :rtype: ``int``
+ """
+ return self._rows
+
+ def getCols(self) -> int:
+ """Get columns in board
+
+ :returns: Number of columns in board
+ :rtype: ``int``
+ """
+ return self._cols
+
+ def getHistory(self) -> list:
+ """Get history list of board
+
+ :returns: History of board
+ :rtype: ``list``
+ """
+ return self._history
+
+
+ def getTurnorder(self) -> list:
+ """Get turnorder list of board
+
+ :returns: Turnorder of board
+ :rtype: ``list``
+ """
+ return self._turnorder
+
+ @_defaultColors
+ def getChecks(self, *colors: str) -> Union[bool, Dict[str, bool]]:
+ """Get checks in board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``bool`` corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: If colors are in check or not
+ :rtype: ``bool`` or ``dict``
+ """
+ return {col: self._checks[col] for col in colors}
+
+ @_defaultColors
+ def getCheckmates(self, *colors: str) -> Union[bool, Dict[str, bool]]:
+ """Get checkmates in board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``bool`` corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: If colors are in checkmate or not
+ :rtype: ``bool`` or ``dict``
+ """
+ return {col: self._checkmates[col] for col in colors}
+
+ @_defaultColors
+ def getKings(self, *colors: str) -> Union[List[King], Dict[str, List[King]]]:
+ """Get kings in board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``list`` of kings corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: All kings of colors in board
+ :rtype: ``list`` or ``dict``
+ """
+ return {col: self._kings[col] for col in colors}
+
+ @_defaultColors
+ def getMoves(self, *colors: str) -> Union[List[Move], Dict[str, List[Move]]]:
+ """Get moves of board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``list`` of moves corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: All moves of colors in board
+ :rtype: ``list`` or ``dict``
+ """
+ return {col: self._moves[col] for col in colors}
+
+ @_defaultColors
+ def getPromoteAt(self, *colors: str) -> Union[int, Dict[str, int]]:
+ """Get promotion position of board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``int`` corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: The promotion position of colors in board
+ :rtype: ``list`` or ``dict``
+ """
+ return {col: self._promoteAt[col] for col in colors}
+
+ @_defaultColors
+ def getPromoteFrom(self, *colors: str) -> Union[List[Piece], Dict[str, List[Piece]]]:
+ """Get promotion starting pieces of board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``list`` corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: The promotion starting piece types of colors in board
+ :rtype: ``list`` or ``dict``
+ """
+ return {col: self._promoteFrom[col] for col in colors}
+
+ @_defaultColors
+ def getPromoteTo(self, *colors: str) -> Union[List[Piece], Dict[str, List[Piece]]]:
+ """Get promotion target pieces of board
+
+ If more than one color is given, this returns a ``dict``
+ with a ``list`` corresponding to each color.
+
+ :param *colors: Colors to return
+ :returns: The promotion target piece types of colors in board
+ :rtype: ``list`` or ``dict``
+ """
+ return {col: self._promoteTo[col] for col in colors}
+
+ def getColors(self) -> List[str]:
+ """Get all colors of board
+
+ :returns: List of colors in board
+ :rtype: ``list``
+ """
+ return self._pieces.keys()
+
+ @_defaultColors
+ def iterPieces(self, *colors: str) -> Generator[Piece, None, None]:
+ """Iterate through pieces of board
+
+ Use __iter__ to iterate through all positions of the board.
+
+ :param *colors: Colors of pieces to iterate through (default is all colors)
+ :yields: Every piece in board
+ :ytype: ``generator``
+ """
+ for col in colors:
+ for p in self._pieces[col]:
+ yield p
+
+ @_defaultColors
+ def eval(self, *colors: str) -> Dict[str, int]:
+ """Evaluate board
+
+ Returns the sum of all pieces' values of colors in board
+
+ :param *colors: Colors to evaluate (defaults to all colors of board)
+
+ :returns: Colors with corresponding sum of pieces
+ :rtype: ``dict``
+ """
+ return {col: sum(list(map(lambda p: p.value, list(self.iterPieces(col))))) for col in colors}
+
+ def removeColor(self, color: str) -> None:
+ """Remove color from board
+
+ :param color: Color to remove
+ """
+ vectors = list(map(lambda p: p.vector, list(self.iterPieces(color))))
+ self[vectors] = [Empty(vector) for vector in vectors]
+ self.checkForCheck()
+
+ def swapPositions(self, vec1: ChessVector, vec2: ChessVector) -> None:
+ """Swap position of two pieces
+
+ :param vec1: Starting position of first piece
+ :param vec2: Starting position of second piece
+ """
+ self._board[vec1.row][vec1.col].move(vec2)
+ self._board[vec2.row][vec2.col].move(vec1)
+ self._board[vec1.row][vec1.col], self._board[vec2.row][vec2.col] = self._board[vec2.row][vec2.col], self._board[vec1.row][vec1.col]
+
+ def isEmpty(self, vec: ChessVector) -> bool:
+ """Check if position is empty
+
+ :param vec: Position to check
+ :returns: True if position is empty, else False
+ :rtype: ``bool``
+ """
+ return isinstance(self[vec], Empty)
+
+ def isThreatened(self, vec: ChessVector, alliedColor: str) -> bool:
+ """Check if position is threatened by enemy pieces
+
+ :param vector: Position to check for threats
+ :param alliedColor: Color to exclude from enemy pieces
+ :returns: True if position is threatened, else False
+ :rtype: ``bool``
+ """
+ hostilePieces = [piece for col in self.getColors() if col != alliedColor for piece in self.iterPieces(col)]
+
+ for hp in hostilePieces:
+ hostile = hp.getMoves(self, ignoreCheck=True)
+ if vec in hostile:
+ return True
+ else:
+ return False
+
+ def checkForCheck(self, checkForMate=True) -> None:
+ """Check for any checks in board
+
+ If checkForMate is True and king is in check,
+ method checks if any allied pieces can move to
+ interfere with the threatened check.
+
+ :param checkForMate: Flag False to ignore checkmate (default is True)
+ :returns: None, stores result in attributes ``checks`` and ``checkmates``
+ """
+ for color in self.getColors():
+
+ for alliedKing in self._kings[color]:
+
+ if self.isThreatened(alliedKing.vector, color):
+ self._checks[color] = True
+ break
+ else:
+ self._checks[color] = False
+
+ if self._checks[color] and checkForMate:
+
+ alliedPiecesPos = map(lambda p: p.vector, list(self.iterPieces(color)))
+
+ for alliedPos in list(alliedPiecesPos):
+ for move in self[alliedPos].getMoves(self, ignoreCheck=True):
+ testBoard = deepcopy(self)
+ for pieceType in [None, *self._promoteTo[color]]:
+ try:
+ testBoard.movePiece(alliedPos, move, ignoreMate=True,
+ checkForMate=False, promote=pieceType,
+ printout=False, checkMove=False, ignoreOrder=True)
+ except PromotionError:
+ continue
+ else:
+ break
+
+ if testBoard._checks[color]:
+ continue
+ else:
+ self._checkmates[color] = False
+ break
+ else:
+ continue
+ break
+ else:
+ self._checkmates[color] = True
+
+ def advanceTurn(self) -> None:
+ """Advance the turn according to turnorder
+ """
+ newidx = self._turnorder.index(self.currentTurn) + 1
+ try:
+ self.currentTurn = self._turnorder[newidx]
+ except IndexError:
+ self.currentTurn = self._turnorder[0]
+
+ def movePiece(self, startVec: ChessVector, targetVec: ChessVector,
+ ignoreOrder=False, ignoreMate=False, ignoreCheck=False,
+ checkForCheck=True, checkForMate=True, checkMove=True,
+ printout=True, promote=None) -> str:
+ """Move piece on board
+
+ :param startVec: Position of moving piece
+ :param targetVec: Destination of moving piece
+ :param **Flags: Flags altering move rules, see below
+ :returns: Notation of move
+ :rtype: ``str``
+
+ :**Flags:
+ :ignoreOrder (False): Ignore the turnorder
+ :ignoreMate (False): Ignore if any pieces are in checkmate
+ :ignoreCheck (False): Ignore if any pieces are in check
+ :checkForCheck (True): Check for any checks after move
+ :checkForMate (True): Check for any checkmates after move
+ :checkMove (True): Check if piece is able to move to destination
+ :printout (True): Print the results of the move; checks, checkmates and move notation
+ :promote (None): Piece type to promote to
+ """
+
+ if self.isEmpty(startVec):
+ raise EmptyError(startVec.getStr(self))
+
+ startPiece = self[startVec]
+
+ if not ignoreOrder and self.currentTurn != startPiece.color:
+ raise TurnError
+
+ if self._checkmates[startPiece.color] and not ignoreMate:
+ raise CheckMate
+
+ if checkMove and not targetVec.matches(startPiece.getMoves(self, ignoreCheck=ignoreCheck, ignoreMate=ignoreMate)):
+ raise IllegalMove(startVec.getStr(self), targetVec.getStr(self))
+
+ for move in self._moves[startPiece.color]:
+ if move.pieceCondition(startPiece):
+ if targetVec in move.getDestinations(startPiece, self):
+ notation = move.action(startPiece, targetVec, self, promote)
+ if checkForCheck:
+ self.checkForCheck(checkForMate=checkForMate)
+ break
+
+ else:
+ raise IllegalMove(startVec.getStr(self), targetVec.getStr(self))
+
+ for color in self._checks.keys():
+ if self._checkmates[color]:
+ if printout:
+ print(f"{color} in Checkmate!")
+ if not "#" in notation:
+ notation += "#"
+
+ elif self._checks[color]:
+ if printout:
+ print(f"{color} in Check!")
+ if not "+" in notation:
+ notation += "+"
+
+ for piece in self.iterPieces():
+
+ if not piece is startPiece:
+ piece.postAction(self)
+
+ self._history.append(notation)
+ if printout:
+ print(notation)
+
+ self.advanceTurn()
+ return notation
+
+ def _addPiece(self, piece: Piece, vec: ChessVector) -> None:
+ if not piece.color in self.getColors():
+ self._pieces[piece.color] = []
+ self._kings[piece.color] = []
+ self._checks[piece.color] = False
+ self._checkmates[piece.color] = False
+
+ self._pieces[piece.color].append(piece)
+
+ if isinstance(piece, King):
+ self._kings[piece.color].append(piece)
+
+ piece.vector = vec
+
+ def _removePiece(self, piece: Piece) -> None:
+
+ self._pieces[piece.color].remove(piece)
+
+ if isinstance(piece, King) and piece in self._kings[piece.color]:
+ self._kings[piece.color].remove(piece)
+
+ if not self._pieces[piece.color]:
+ del self._pieces[piece.color]
+ del self._promoteTo[piece.color]
+ del self._promoteFrom[piece.color]
+ del self._promoteAt[piece.color]
+ del self._kings[piece.color]
+ del self._checks[piece.color]
+ del self._checkmates[piece.color]
+
+ self._turnorder.remove(piece.color)
+
+ piece.vector = None
+
+
+def initClassic() -> Board:
+ """Initialize a chessBoard setup for 2 players, classic setup
+
+ :returns: Classic chessboard
+ :rtype: ``Board``
+ """
+ board = Board(deepcopy(ClassicConfig.CONFIG))
+ return board
+
+
+def init4P() -> Board:
+ """Initialize a chessboard setup for four players
+
+ :returns 4 player chessboard
+ :rtype: ``Board``
+ """
+ board = Board(deepcopy(FourPlayerConfig.CONFIG))
+ return board