From: Nils Forssén Date: Thu, 18 Sep 2025 00:00:02 +0000 (+0200) Subject: initial X-Git-Url: https://gitweb.forssennils.se/?a=commitdiff_plain;ds=inline;p=pawnshop.git initial --- e68ab3ea5ed91459a48c302371d7b319ada6f3f9 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..623c1ae Binary files /dev/null and b/LICENSE differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..08042e3 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Pawnshop +A simple chess package for python written solely as hobby project. I've also written a GUI with networking socket capabilities (add link here) and might create some AI furter down the line. + +The package also includes a 4-player mode, although the ruleset is unchanged from the classic game, Checkmate -> Game Over! +## Background +This is my first real project to scale (> 1000 lines of code) and the first to be published to [PyPi](https://pypi.org/project/pawnshop/). + +The project has certainly been a product of time with other hobby projects and school often taking priority. This along with multiple mid-project large scale refractorizations, reconsiderations and laziness should explain the unorganized codebase. + +Needless to say, I've attempted to document the code well and create a proper package that I can "proudly" publish, Although I still don't recommend anyone to continue developing or using this outside of simple projects like this. There are definitely better alternatives out there. + +Some central documentation, more extensive testing and usage examples would have been beneficial, but as of now I just want to continue with other projects and leave this as finished for the time being. diff --git a/build/lib/pawnshop/ChessBoard.py b/build/lib/pawnshop/ChessBoard.py new file mode 100644 index 0000000..b945643 --- /dev/null +++ b/build/lib/pawnshop/ChessBoard.py @@ -0,0 +1,554 @@ +# 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 diff --git a/build/lib/pawnshop/ChessVector.py b/build/lib/pawnshop/ChessVector.py new file mode 100644 index 0000000..8aee5bc --- /dev/null +++ b/build/lib/pawnshop/ChessVector.py @@ -0,0 +1,200 @@ +# ChessVector.py + +from typing import Union, Tuple, List, TYPE_CHECKING +from .Utils import toAlpha, inverseIdx, countAlpha +# import pawnshop.ChessBoard + +if TYPE_CHECKING: + from pawnshop.ChessBoard import Board + + +class ChessVector(object): + """ChessVector object + + Object to store position on chessboard + Initialize object with position in (row, col) or string notation format + If a string notation format is given, the board must also be given + The vector supports common operations such as addition, multiplication with other vectors + + :param position: Tuple or string notation position on chessboard + :param board: Board to use when determining position given by string notation (default is None) + """ + + def __init__(self, position: Union[Tuple[int, int], str], board=None): + self._row = 0 + self._col = 0 + + if isinstance(position, tuple): + row, col = position + self.row = int(row) + self.col = int(col) + elif isinstance(position, str) and not board is None: + position = position.lower() + for char in position: + if char.isdigit(): + i = position.find(char) + if i == 0: + raise ValueError("Position does not include column!") + alpha = position[:i] + num = position[i::] + row = board.getRows() - int(num) + for n, a in countAlpha(): + if a == alpha: + col = n + break + else: + continue + break + else: + raise ValueError("position does not include row!") + self.row = row + self.col = col + else: + raise ValueError("Position is not a string or a tuple!") + + @property + def col(self): + return self._col + + @col.setter + def col(self, newCol): + self._col = newCol + + @property + def row(self): + return self._row + + @row.setter + def row(self, newRow): + self._row = newRow + + def __sub__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row - other.row, self.col - other.col)) + else: + return ChessVector((self.row - other, self.col - other)) + + def __rsub__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row - self.row, other.col - self.col)) + else: + raise ValueError(f"Cannot subtract {type(self)} from non-{type(self)}!") + + def __add__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row + other.row, self.col + other.col)) + else: + return ChessVector((self.row + other, self.col + other)) + + def __radd__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row + self.row, other.col + self.col)) + else: + raise ValueError(f"Cannot add {type(self)} to non-{type(self)}!") + + def __mul__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row * other.row, self.col * other.col)) + else: + return ChessVector((self.row * other, self.col * other)) + + def __rmul__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row * self.row, other.col * self.col)) + else: + raise ValueError(f"Cannot multiply non-{type(self)} by {type(self)}!") + + def __div__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row / other.row, self.col / other.col)) + else: + return ChessVector((self.row / other, self.col / other)) + + def __rdiv__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row / self.row, other.col / self.col)) + else: + raise ValueError(f"Cannot divide non-{type(self)} by {type(self)}!") + + def __neg__(self): + return ChessVector((-self.row, -self.col)) + + def __pos__(self): + return ChessVector((+self.row, +self.col)) + + def __eq__(self, other): + if isinstance(other, ChessVector): + return self.row == other.row and self.col == other.col + else: + raise ValueError(f"Cannot compare {type(self)} with {type(other)}!") + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if isinstance(other, ChessVector): + return self.row < other.row and self.col < other.col + else: + raise ValueError(f"Cannot compare {type(self)} with {type(other)}!") + + def __gt__(self, other): + return not self < other + + def __le__(self, other): + return self < other or self == other + + def __ge__(self, other): + return self > other or self == other + + def __repr__(self): + return str((self.row, self.col)) + + def __str__(self): + return str((self.row, self.col)) + + def tuple(self) -> Tuple[int, int]: + """Return tuple format of vector + + :returns: (row, col) tuple + :rType: ´´tuple´´ + """ + return (self._row, self._col) + + def getStr(self, board: "Board") -> str: + """Return string notation format of vector + + :param board: Board to determine string position from + :returns: string notation of vector position + :rType: ´´str´´ + """ + notation = "" + notation += toAlpha(self.col) + notation += inverseIdx(self.row, board) + return notation + + def matches(self, otherVecs: List["ChessVector"]) -> bool: + """Check if vector matches any of other vectors + + :param otherVecs: List of other vectors + :returns: If match is found or not + :rType: ´´bool´´ + """ + for vec in otherVecs: + if self.row == vec.row and self.col == vec.col: + return True + else: + return False + + def copy(self) -> "ChessVector": + """Create a new copy of this vector + + :returns: Copy of this vector + :rType: ´´ChessVector´´ + """ + return ChessVector((self.row, self.col)) + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/build/lib/pawnshop/Exceptions.py b/build/lib/pawnshop/Exceptions.py new file mode 100644 index 0000000..5b1144d --- /dev/null +++ b/build/lib/pawnshop/Exceptions.py @@ -0,0 +1,42 @@ +# Exceptions.py + +class Illegal(Exception): + """Move is ilegal""" + pass + + +class IllegalMove(Illegal): + def __init__(self, startPos, targetPos, + msg="Piece at {0} cannot move to {1}!"): + super().__init__(msg.format(startPos, targetPos)) + + +class CheckMate(Illegal): + def __init__(self, msg="Your king is in checkmate!"): + super().__init__(msg) + + +class EmptyError(IndexError): + def __init__(self, position, msg="Position {0} is empty!"): + super().__init__(msg.format(position)) + + +class DisabledError(IndexError): + def __init__(self, position, msg="Position {0} is out of bounce!"): + super().__init__(msg.format(position)) + + +class PromotionError(Exception): + def __init__(self, msg="Moved piece needs to be promoted!"): + super().__init__(msg) + + +class TurnError(Exception): + def __init__(self, msg="Wrong player!"): + super().__init__(msg) + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/build/lib/pawnshop/GameNotations.py b/build/lib/pawnshop/GameNotations.py new file mode 100644 index 0000000..4c011e5 --- /dev/null +++ b/build/lib/pawnshop/GameNotations.py @@ -0,0 +1,194 @@ +# GameNotations.py + +import re +from copy import deepcopy + +from .ChessBoard import ( + initClassic, + Board +) +from .configurations import ClassicConfig +from .ChessVector import ChessVector +from .Pieces import * +from .Utils import toAlpha +from .Moves import ( + CastleK, + CastleQ +) + + +STANDARDTAGS = [ + "Event", + "Site", + "Date", + "Round", + "White", + "Black", + "Result" +] +OPTIONALTAGS = [ + "Annotator", + "PlyCount", + "TimeControl", + "Time", + "Termination", + "Mode", + "FEN" +] +ALLTAGS = [*STANDARDTAGS, *OPTIONALTAGS] + + +def board2PGN(board: Board, **tags) -> str: + """Get Portable Game Notation from board + + :param board: Board to get notation from + :param **tags: Tags added to the notation + :returns: PGN string + :rtype: ``str`` + + :**tags: Tags found in STANDARDTAGS and OPTIONALTAGS + """ + PGNString = "" + tags = {t.lower(): v for t, v in tags.items()} + + for TAG in ALLTAGS: + if TAG.lower() in tags: + PGNString += f"[{TAG} \"{str(tags[TAG.lower()])}\"]\n" + i = 0 + while i * 2 < len(board.history): + i += 1 + PGNString += str(i) + ". " + " ".join(board.history[(i - 1) * 2:i * 2:1]) + "\n" + + return PGNString + + +def PGN2Board(PGNString: str) -> Board: + """Get Board object from Portable Game Notation + + :param PGNString: PGN string + :returns: Board object from PGN + :rtype: ``Board`` + """ + notations = re.finditer(r"\s*(?PO-O-O)|(?PO-O)|(?P[A-Z]*)(?P[a-h]?)(?P[x]?)(?P[a-h]+)(?P\d+)=?(?P[A-Z]?)\+*\#?", PGNString) + + board = initClassic() + for i, notation in enumerate(notations): + color = ["white", "black"][i % 2 == 1] + if (not notation.group("castleK") is None) or (not notation.group("castleQ") is None): + for king in board.kings[color]: + for move in board.moves[color]: + if ((not notation.group("castleK") is None) and move is CastleK) or ((not notation.group("castleQ") is None) and move is CastleQ): + board.movePiece(king.vector, move.getDestinations(king, board).pop(), checkMove=False, ignoreMate=True, checkForCheck=False, printOut=False, ignoreOrder=True) + break + else: + continue + break + else: + for piece in board.pieces[color]: + vector = ChessVector(notation.group("col") + notation.group("rank"), board) + if vector.matches(piece.getMoves(board)): + pType = pieceNotations[notation.group("piece")] + if isinstance(piece, pType): + if notation.group("pcol") == "" or notation.group("pcol") == toAlpha(piece.vector.col): + board.movePiece(piece.vector, vector, checkMove=False, promote=pieceNotations[notation.group("promote")], ignoreMate=True, checkForCheck=False, printOut=False, ignoreOrder=True) + break + else: + continue + else: + continue + + board.checkForCheck() + return board + + +def FEN2Board(FENString: str) -> Board: + """Get Board object from Forsyth-Edwards-Notation + + :param FENString: Forsyth-Edwards-Notation + :returns: Board object from FEN + :rtype: ``Board`` + """ + board = Board() + config = deepcopy(ClassicConfig.CONFIG) + del config["pieces"] + board.setup(config) + + fieldFinder = re.finditer(r"[^ ]+", FENString) + rowFinder = re.finditer(r"([^/]+)", next(fieldFinder).group()) + + for rowi, row in enumerate(rowFinder): + coli = 0 + for chari, char in enumerate(row.group(0)): + if char.isnumeric(): + for coli in range(coli, coli + int(char)): + vector = ChessVector((rowi, coli), board) + board[vector] = Empty(vector) + coli += 1 + else: + vector = ChessVector((rowi, coli), board) + + if char.isupper(): + board[vector] = pieceNotations[char]("white", direction="up") + elif char.islower(): + board[vector] = pieceNotations[char.upper()]("black", direction="down") + coli += 1 + + # No other fields are critical, might implement more later + return board + + +def board2FEN(board: Board) -> str: + """Get Forsyth-Edward-Notation from board + + The notation does not account for: + current turn, castling potential, en-passant or move count + - only the position is notated (I am lazy) + + :param board: Board to get FEN from + :returns: FEN string + :rtype: ``str`` + """ + FENString = "" + for rowi, row in enumerate(board._board): + empty = 0 + for coli, piece in enumerate(row): + if isinstance(piece, Empty) or isinstance(piece, Disabled): + empty += 1 + else: + if empty: + FENString += str(empty) + if piece.color == "white": + ps = piece.symbol.upper() + elif piece.color == "black": + ps = piece.symbol.lower() + FENString += ps + empty = 0 + + if empty: + FENString += str(empty) + if not rowi == board.getRows() - 1: + FENString += "/" + + return FENString + + +def readable(historyList: List[str], players=2) -> str: + """Get printable format of history + + :param historyList: History to be read + :param players: How many players the history includes + :returns: Readable string of history + :rtype: ``str`` + """ + finalString = "" + i = 0 + while i * players < len(historyList): + i += 1 + finalString += str(i) + ". " + " - ".join(historyList[(i - 1) * players:i * players:1]) + "\n" + return finalString.strip() + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/build/lib/pawnshop/Moves.py b/build/lib/pawnshop/Moves.py new file mode 100644 index 0000000..0784822 --- /dev/null +++ b/build/lib/pawnshop/Moves.py @@ -0,0 +1,397 @@ +# Moves.py + +from typing import List, Union, Tuple, TYPE_CHECKING +from abc import ABC, abstractclassmethod +from .Pieces import * +from .Utils import createNotation +from .Exceptions import PromotionError +from .ChessVector import ChessVector + +if TYPE_CHECKING: + from .ChessBoard import Board + + +class Move(ABC): + """Abstract class for moves in chess + """ + + @abstractclassmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Test if piece satisfies move requirement + """ + raise NotImplementedError + + @abstractclassmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> list: + """Return list of possible destinations + """ + raise NotImplementedError + + @abstractclassmethod + def action(thisMove, startPiece, targetPos, board, *args, **kwargs) -> str: + """Move the piece + """ + raise NotImplementedError + + +class Standard(Move): + """Standard move in chess + + Moves piece according to .getStandardMoves() method + """ + + @classmethod + def pieceCondition(thisMove, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Since there is no condition to standard moves, this will always return True. + + :returns: True + :rtype: ``bool`` + """ + return True + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Calls piece.getStandardMoves() method to get all standard moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + return piece.getStandardMoves(board) + + @classmethod + def action(thisMove, startPiece: Piece, targetVec: ChessVector, board: "Board", promote=None, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to standard move rules. + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :param promote: Promotion type of piece (default is None) + :returns: Notation of move + :rtype: ``str`` + """ + promo = False + + for pieceType in board.getPromoteFrom(startPiece.color): + if isinstance(startPiece, pieceType): + + if startPiece.rank + abs((startPiece.vector - targetVec).tuple()[startPiece.forwardVec.col]) == board.getPromoteAt(startPiece.color): + if promote is None: + raise PromotionError + + if promote not in board.getPromoteTo(startPiece.color): + raise PromotionError( + f"{startPiece.color} cannot promote to {promote}!") + + promo = True + + break + + targetPiece = board[targetVec] + + notation = createNotation( + board, startPiece, targetVec, + isPawn=isinstance(startPiece, Pawn), capture=not isinstance(targetPiece, Empty)) + + if not isinstance(targetPiece, Empty): + board[targetVec] = Empty(targetVec) + board.swapPositions(startPiece.vector, targetVec) + else: + board.swapPositions(startPiece.vector, targetVec) + if promo: + newPiece = promote(startPiece.color) + newPiece.move(startPiece.vector) + board[startPiece.vector] = newPiece + notation += "=" + newPiece.symbol + + return notation + + +class _Castling(Move): + """Parent class to King-side and Queen-side castling + """ + + @classmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Must be pieces first move and piece must be instance of ``King``. + + :param piece: Piece to check + :returns: If piece satisfies requirements + :rtype: ``bool`` + """ + return piece.firstMove and isinstance(piece, King) + + @classmethod + def action(thisMove, startPiece: Piece, targetVec: ChessVector, board: "Board", *args, **kwargs) -> None: + """Performs the action of move + + Moves piece according to move rules. + Returns None as Queen-side and King-side castling are noted differently. + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + """ + for rook in thisMove.findRooks(startPiece, board): + between = thisMove.findBetween(startPiece.vector, rook.vector) + if targetVec in between: + kingTarget, rookTarget = thisMove.getTargets(between) + board.swapPositions(startPiece.vector, kingTarget) + board.swapPositions(rook.vector, rookTarget) + break + else: + raise ValueError(f"Piece cannot move to {targetVec}") + + def findBetween(vec1: ChessVector, vec2: ChessVector) -> List[ChessVector]: + """Helper function to find all positions between two positions + + Helper function. + If there are not positions between or given positions + are not in a row, the returned list is empty. + + :param vec1: First position + :param vec2: Second position + :returns: List of possitions betweeen + :rtype: ``list`` + """ + rowStep = vec1.row - vec2.row and (1, -1)[vec1.row - vec2.row < 0] + colStep = vec1.col - vec2.col and (1, -1)[vec1.col - vec2.col < 0] + + if not rowStep: + colRange = range(vec2.col + colStep, vec1.col, colStep) + rowRange = [vec1.row] * len(colRange) + elif not colStep: + rowRange = range(vec2.row + rowStep, vec1.row, rowStep) + colRange = [vec1.col] * len(rowRange) + else: + rowRange = range(0, 0) + colRange = range(0, 0) + + return [ChessVector(idx) for idx in zip(rowRange, colRange)] + + def emptyBetween(board: "Board", between: List[ChessVector]) -> bool: + """Check if all positions are emtpy + + Helper funciton. + Check if all positions between two pieces are empty + + :param board: Board to check positions in + :param between: List of positions to check + :returns: If all positions are empty or not + :rtype: ``bool`` + """ + for vector in between: + if not isinstance(board[vector], Empty): + return False + else: + return True + + def findRooks(piece: Piece, board: "Board") -> List[Piece]: + """Find all rooks in board that are on same lane as piece + + Helper function. + Iterates through all pieces on board looking for + rooks on same lane as piece. + + :param piece: Piece to check for same lane + :param board: Board to check for rooks in + :returns: List of rooks on same lane as piece + :rtype: ``list`` + """ + def vecCondition(vec1, vec2): + return bool(vec2.row - vec1.row) != bool(vec2.col - vec1.col) and (not vec2.row - vec1.row or not vec2.col - vec2.col) + + rookList = [] + for p in board.iterPieces(piece.color): + if isinstance(p, Rook) and p.firstMove and vecCondition(piece.vector, p.vector): + rookList.append(p) + return rookList + + def getTargets(between: list) -> Union[Tuple[ChessVector], None]: + """Get castling targets + + Helper function + Get the two middle squares of list of positions between. + If list is of length 1, this returns None. + Biased towards the start of list. + + + :param between: List of positions between + :returns: Tuple of target positions + :rtype: ``tuple`` or None + """ + if not len(between) > 1: + return None + if not len(between) % 2: + target1 = between[int((len(between) / 2) - 1)] + target2 = between[int((len(between) / 2))] + else: + target1 = between[int((len(between) / 2) - 0.5)] + target2 = between[int((len(between) / 2) + 0.5)] + return (target1, target2) + + +class CastleK(_Castling): + """Castle King-side move + """ + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible castling moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + if not board.getChecks(piece.color): + for rook in thisMove.findRooks(piece, board): + between = thisMove.findBetween(piece.vector, rook.vector) + if thisMove.emptyBetween(board, between) and not len(between) % 2: + kingTarget, _ = thisMove.getTargets(between) + walked = thisMove.findBetween(piece.vector, kingTarget) + for vec in walked: + if board.isThreatened(vec, piece.color): + break + else: + destList.append(kingTarget) + return destList + + @classmethod + def action(thisMove, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the castling move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + super().action(*args, **kwargs) + return "O-O" + + +class CastleQ(_Castling): + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible castling moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + if not board.getChecks(piece.color): + for rook in thisMove.findRooks(piece, board): + between = thisMove.findBetween(piece.vector, rook.vector) + if thisMove.emptyBetween(board, between) and len(between) % 2: + kingTarget, _ = thisMove.getTargets(between) + walked = thisMove.findBetween(piece.vector, kingTarget) + for vec in walked: + if board.isThreatened(vec, piece.color): + break + else: + destList.append(kingTarget) + return destList + + @classmethod + def action(thisMove, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the castling move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + super().action(*args, **kwargs) + return "O-O-O" + + +class EnPassant(Move): + """Special move en-passant + """ + + @classmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Piece must be of instance ``Pawn`` + + :param piece: Piece to check + :returns: If piece satisfies requirements + :rtype: ``bool`` + """ + return isinstance(piece, Pawn) + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible en-passant moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + for diagVec in (piece.lDiagVec, piece.rDiagVec): + checkVec = (piece.vector - piece.forwardVec) + diagVec + try: + if isinstance(board[checkVec], Pawn) and board[checkVec].passed and board[checkVec].forwardVec == -piece.forwardVec: + destList.append(piece.vector + diagVec) + except IndexError: + pass + return destList + + @classmethod + def action(thisMove, piece: Piece, targetVec: ChessVector, board: "Board", *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the en-passant move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + notation = createNotation(board, piece, targetVec, + isPawn=True, capture=True) + + board[targetVec - piece.forwardVec] = Empty(targetVec - piece.forwardVec) + board.swapPositions(piece.vector, targetVec) + return notation diff --git a/build/lib/pawnshop/Pieces.py b/build/lib/pawnshop/Pieces.py new file mode 100644 index 0000000..fe9b84c --- /dev/null +++ b/build/lib/pawnshop/Pieces.py @@ -0,0 +1,423 @@ +# Pieces.py + +from copy import deepcopy +from abc import ABC, abstractmethod +from typing import List, Tuple, TYPE_CHECKING +from .Utils import _positivePos, _catchOutofBounce, removeDupes +from .ChessVector import ChessVector + +if TYPE_CHECKING: + from .ChessBoard import Board + +_directions = { + "up": ((-1, 0), (-1, -1), (-1, 1)), + "down": ((1, 0), (1, 1), (1, -1)), + "right": ((0, 1), (-1, 1), (1, 1)), + "left": ((0, -1), (1, -1), (-1, -1)) +} +_directions = {key: [ChessVector(offset) for offset in _directions[key]] for key in _directions} + + +class Piece(ABC): + """Abstract base class for pieces + + :param color: Color of piece + :param value: Numerical value of piece + :param symbol: Char symbol of piece + """ + + def __init__(self, color: str, value: int, symbol: str, *args, **kwargs): + self.vector = None + self.color = color + self.value = value + self.symbol = symbol + self.firstMove = True + + def __str__(self): + return self.color[0] + self.symbol + + @abstractmethod + def getStandardMoves(self, board: "Board"): + """Returns standard destinations of piece in board + """ + raise NotImplementedError + + def getMoves(self, board: "Board", ignoreCheck=False, ignoreMate=False) -> List[ChessVector]: + """Returns all moves of piece in board + + Uses board.getMoves() method to check what moves piece is allowed to. + + :param board: Board to move in + :param **Flags: Flags to pass into move + :returns: List of possible moves + :rtype: ``list`` + + :**Flags: + :ignoreCheck (False): Ignore checks when getting moves + :ignoreMate (False): Ignore checkmate when getting moves + """ + destList = [] + for move in board.getMoves(self.color): + if move.pieceCondition(self): + destList.extend(move.getDestinations(self, board)) + + if not ignoreCheck: + remove = [] + + for dest in destList: + testBoard = deepcopy(board) + testBoard.movePiece(self.vector, dest, ignoreMate=ignoreMate, checkForMate=False, printout=False, checkMove=False, promote=Queen, ignoreOrder=True) + if testBoard.getChecks(self.color): + remove.append(dest) + + for dest in remove: + destList.remove(dest) + + return destList + + def move(self, destVector: ChessVector) -> None: + """Move piece to destination + + :param destVector: Destination + """ + self.vector = destVector + self.firstMove = False + + def postAction(self, board: "Board") -> None: + """Do action after piece is moved in board + + Call this after a piece is moved in board + """ + pass + + @_positivePos + @_catchOutofBounce + def canWalk(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can walk to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can move + :rtype: ``bool`` + """ + return board.isEmpty(vector) + + @_positivePos + @_catchOutofBounce + def canCapture(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can capture to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can capture + :rtype: ``bool`` + """ + destPiece = board[vector] + try: + return destPiece.color != self.color + except AttributeError: + return False + + @_positivePos + @_catchOutofBounce + def canMove(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can capture to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can move (capture or walk) + :rtype: ``bool`` + """ + destPiece = board[vector] + try: + return destPiece.color != self.color + except AttributeError: + return board.isEmpty(vector) + + def _getMovesInLine(self, iterVector: ChessVector, board: "Board") -> List[ChessVector]: + """Get moves in one line + + Return all positions piece is can move to iterating with iterVector. + Stops if piece can capture as piece cannot continue moving after capturing. + + :param iterVector: Vector to iterate moves with + :param board: Board to check in + :returns: List of possible destinations + :rtype: ``list`` + """ + moveList = [] + newV = self.vector + while True: + newV += iterVector + if self.canWalk(newV, board): + moveList.append(newV) + elif self.canCapture(newV, board): + moveList.append(newV) + break + else: + break + return moveList + + +class Pawn(Piece): + """Pawn object + + :param color: Color of piece + :param direction: Movement direction of Pawn (default is "up") + :param rank: Starting rank of pawn, used to calc promote + """ + + def __init__(self, color: str, direction="up", rank=2, *args, **kwargs): + super().__init__(color, 1, "P") + + self.passed = False + self.direction = direction.lower() + self.rank = rank + + if direction in _directions.keys(): + self.forwardVec, self.lDiagVec, self.rDiagVec = _directions[direction] + else: + raise ValueError(f"Direction is not any of {_directions.keys()}") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + destVec = self.vector + self.forwardVec + + if self.canWalk(destVec, board): + destList.append(destVec) + if self.firstMove: + destVec += self.forwardVec + if self.canWalk(destVec, board): + destList.append(destVec) + + for destVec in self.getAttacking(board): + if self.canCapture(destVec, board): + destList.append(destVec) + + return destList + + def move(self, newV: ChessVector) -> None: + """Move piece to destination + + If Pawn moves 2 places, it can be captured by en-passant. + + :param newV: Destination + """ + if self.firstMove: + if abs(self.vector.row - newV.row) == 2 or abs(self.vector.col - newV.col) == 2: + self.passed = True + self.rank += 1 + self.rank += 1 + super().move(newV) + + def postAction(self, *args, **kwargs): + """Do action after piece is moved in board + + Call this after a piece is moved in board + """ + self.passed = False + + def getAttacking(self, *args, **kwargs) -> Tuple[ChessVector]: + """Get the threatened positions of piece + + :returns: Tuple of threatened positions + :rType: ´´tuple´´ + """ + return [self.vector + self.lDiagVec, self.vector + self.rDiagVec] + + +class Rook(Piece): + """Rook object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 5, "R") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + forwardVec = vecTuple[0] + destList.extend(self._getMovesInLine(forwardVec, board)) + return destList + + +class Knight(Piece): + """Knight object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 3, "N") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + offsetList = [ + (1, 2), + (1, -2), + (-1, 2), + (-1, -2), + (2, 1), + (2, -1), + (-2, 1), + (-2, -1) + ] + vecList = [ChessVector(offset) for offset in offsetList] + + for offsetVec in vecList: + destVec = self.vector + offsetVec + if self.canMove(destVec, board): + destList.append(destVec) + return destList + + +class Bishop(Piece): + """Bishop object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 3, "B") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + destList.extend(self._getMovesInLine(vecTuple[1], board)) + return destList + + +class King(Piece): + """King object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, int(1e10), "K") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for offsetVec in removeDupes([vec for vecList in _directions.values() for vec in vecList]): + destVec = self.vector + offsetVec + if self.canMove(destVec, board): + destList.append(destVec) + return destList + + +class Queen(Piece): + """Queen object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 9, "Q") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + destList.extend(self._getMovesInLine(vecTuple[0], board)) + destList.extend(self._getMovesInLine(vecTuple[1], board)) + return destList + + +class Disabled(): + """Disabled object + + Object for representing disabled positions in chessboard + + :param vector: Position of disabled square + """ + + def __init__(self, vector: ChessVector, *args, **kwargs): + self.vector = vector + + def __str__(self): + return " " + + def move(self, vec: ChessVector): + """Move disabled object + + Move the disabled square + + :param vec: New position + """ + self.vector = vec + + +class Empty(): + """Empty object + + Object for representing empty positions in chessboard + + :param vector: Position of empty square + """ + + def __init__(self, vector: ChessVector, *args, **kwargs): + self.vector = vector + + def __str__(self): + return "__" + + def move(self, vec: ChessVector): + """Move empty object + + Move the empty square + + :param vec: New position + """ + self.vector = vec + + +pieceNotations = { + "P": Pawn, + "N": Knight, + "B": Bishop, + "R": Rook, + "Q": Queen, + "K": King +} + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/build/lib/pawnshop/Utils.py b/build/lib/pawnshop/Utils.py new file mode 100644 index 0000000..0eb9da3 --- /dev/null +++ b/build/lib/pawnshop/Utils.py @@ -0,0 +1,159 @@ +# Utils.py + +from typing import List, Generator, TYPE_CHECKING +from string import ascii_lowercase +import sys, os + +if TYPE_CHECKING: + from .ChessBoard import Board + from .ChessVector import ChessVector + from .Pieces import Piece + +def _catchOutofBounce(func): + """Decorator for catching out of bounce ´´IndexError´´""" + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except IndexError: + return False + return wrapper + + +def _positivePos(func): + """Decorator for ensuring a position is not negative""" + def wrapper(pInstance, vector, bInstance, *args, **kwargs): + if not vector.row < 0 and not vector.col < 0: + return func(pInstance, vector, bInstance, *args, **kwargs) + else: + return False + return wrapper + + +def removeDupes(vectorList: List["ChessVector"]) -> List["ChessVector"]: + """Remove duplicate positions + + :param vectorList: List to remove duplicates from + :returns: List without duplicates + :rtype: ``list`` + """ + for i, superVec in enumerate(vectorList): + if superVec.matches(vectorList[i + 1::]): + vectorList.remove(superVec) + return removeDupes(vectorList) + else: + return vectorList + + +def createNotation(board: "Board", startPiece: "Piece", targetVec: "ChessVector", isPawn=False, capture=False) -> str: + """Create a notation for a move + + Creates notation of move according to standard chess notation. + + :param startPiece: Piece to be moved + :param targetVec: Destination of move + :param **Flags: Flags to create notation + :returns: Notation of move + :rtype: ``str`` + + :**Flags: + :isPawn (True): + :capture (True): + """ + notation = "" + targetNot = targetVec.getStr(board) + + if not isPawn: + notation = startPiece.symbol + for piece in board.iterPieces(startPiece.color): + if piece is not startPiece and isinstance(piece, type(startPiece)): + if targetVec.matches(piece.getMoves(board, ignoreCheck=True)): + if piece.vector.col == startPiece.vector.col: + notation += inverseIdx(startPiece.vector.row, board) + else: + notation += toAlpha(startPiece.vector.col) + break + elif capture: + notation = toAlpha(startPiece.vector.col) + + if capture: + notation += "x" + + notation += targetNot + return notation + + +def countAlpha() -> Generator[str, None, None]: + """Generator to count in alphabetical order + + Counts in alphabetical order. + a->b->c->...->aa->ab->...->ba->... + + :yields: Character + :ytype: ``generator`` + """ + stringList = [0] + num = 0 + while True: + yield (num, "".join([ascii_lowercase[num] for num in stringList])) + i = 1 + num += 1 + + while True: + if i > len(stringList): + stringList.insert(0, 0) + break + else: + changeTo = stringList[-i] + 1 + if changeTo >= len(ascii_lowercase): + stringList[-i::] = [0] * (i) + i += 1 + continue + else: + stringList[-i] = changeTo + break + + +def inverseIdx(idx: int, board: "Board") -> str: + """Inverse index + + Inverses idx given board rows and returns string + + :param idx: Index to reverse + :param board: Board to reverse according to rows + :returns: Reversed index + :rtype: ``str`` + """ + return str(board.getRows() - idx) + + +def toAlpha(num: int) -> str: + """Convert number to alphabetical + + Counts through all alpha until reaching number. + (I tried to make it not have to count through all alphas, + however, since my alpha system doesn't match any regular + base number system I was not able to.) + + :param num: Number to convert + :returns: Alphabetical string from num + :rtype: str + """ + for n, notation in countAlpha(): + if num == n: + return notation + +def getResourcePath(relative_path): + """ + Get pyinstaller resource + """ + + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + + return os.path.join(os.path.abspath("."), relative_path) + + +if __name__ == "__main__": + # Do some testing + + pass diff --git a/build/lib/pawnshop/__init__.py b/build/lib/pawnshop/__init__.py new file mode 100644 index 0000000..f27784b --- /dev/null +++ b/build/lib/pawnshop/__init__.py @@ -0,0 +1,6 @@ +# import pawnshop.ChessBoard +# import pawnshop.ChessVector +# import pawnshop.Utils +# import pawnshop.GameNotations +__all__ = ["ChessVector", "ChessBoard", "GameNotations", "Utils", "Moves", "Utils", "Pieces", "Exceptions"] +from . import * diff --git a/build/lib/pawnshop/configurations/ClassicConfig.py b/build/lib/pawnshop/configurations/ClassicConfig.py new file mode 100644 index 0000000..039cf47 --- /dev/null +++ b/build/lib/pawnshop/configurations/ClassicConfig.py @@ -0,0 +1,66 @@ +# ClassicConfig.py + +from pawnshop.Pieces import * +from pawnshop.ChessVector import ChessVector +from pawnshop.Moves import * + +_colors = ("black", "white") +_black, _white = _colors +_classicPieces = { + Rook(_black): ChessVector((0, 0)), + Knight(_black): ChessVector((0, 1)), + Bishop(_black): ChessVector((0, 2)), + Queen(_black): ChessVector((0, 3)), + King(_black): ChessVector((0, 4)), + Bishop(_black): ChessVector((0, 5)), + Knight(_black): ChessVector((0, 6)), + Rook(_black): ChessVector((0, 7)), + Pawn(_black, "down"): ChessVector((1, 0)), + Pawn(_black, "down"): ChessVector((1, 1)), + Pawn(_black, "down"): ChessVector((1, 2)), + Pawn(_black, "down"): ChessVector((1, 3)), + Pawn(_black, "down"): ChessVector((1, 4)), + Pawn(_black, "down"): ChessVector((1, 5)), + Pawn(_black, "down"): ChessVector((1, 6)), + Pawn(_black, "down"): ChessVector((1, 7)), + + Rook(_white): ChessVector((7, 0)), + Knight(_white): ChessVector((7, 1)), + Bishop(_white): ChessVector((7, 2)), + Queen(_white): ChessVector((7, 3)), + King(_white): ChessVector((7, 4)), + Bishop(_white): ChessVector((7, 5)), + Knight(_white): ChessVector((7, 6)), + Rook(_white): ChessVector((7, 7)), + Pawn(_white, "up"): ChessVector((6, 0)), + Pawn(_white, "up"): ChessVector((6, 1)), + Pawn(_white, "up"): ChessVector((6, 2)), + Pawn(_white, "up"): ChessVector((6, 3)), + Pawn(_white, "up"): ChessVector((6, 4)), + Pawn(_white, "up"): ChessVector((6, 5)), + Pawn(_white, "up"): ChessVector((6, 6)), + Pawn(_white, "up"): ChessVector((6, 7)) +} + +for piece, vector in _classicPieces.items(): + piece.vector = vector + +_pieceDict = {color: [piece for piece in _classicPieces.keys() if piece.color == color] for color in _colors} +_moveDict = {color: [Standard, CastleK, CastleQ, EnPassant] for color in _colors} +_promoteToDict = {color: [Queen, Rook, Knight, Bishop] for color in _colors} +_promoteFromDict = {color: [Pawn] for color in _colors} +_promoteAtDict = {color: 8 for color in _colors} + +CONFIG = { + "rows": 8, + "cols": 8, + "pieces": _pieceDict, + "moves": _moveDict, + "promoteTo": _promoteToDict, + "promoteFrom": _promoteFromDict, + "promoteAt": _promoteAtDict, + "turnorder": ["white", "black"] +} + +if __name__ == "__main__": + print(CONFIG) diff --git a/build/lib/pawnshop/configurations/DefaultConfig.JSON b/build/lib/pawnshop/configurations/DefaultConfig.JSON new file mode 100644 index 0000000..4de45ae --- /dev/null +++ b/build/lib/pawnshop/configurations/DefaultConfig.JSON @@ -0,0 +1,12 @@ +{ + "rows": 8, + "cols": 8, + "colors": [], + "disabled": [], + "pieces": {}, + "moves": {}, + "promoteTo": {}, + "promoteFrom": {}, + "promoteAt": {}, + "turnorder": [] +} diff --git a/build/lib/pawnshop/configurations/FourPlayerConfig.py b/build/lib/pawnshop/configurations/FourPlayerConfig.py new file mode 100644 index 0000000..56ab996 --- /dev/null +++ b/build/lib/pawnshop/configurations/FourPlayerConfig.py @@ -0,0 +1,139 @@ +# FourPlayerConfig.py + +from pawnshop.Pieces import * +from pawnshop.Moves import * +from pawnshop.ChessVector import ChessVector + +_colors = ("yellow", "green", "red", "blue") +_yellow, _green, _red, _blue = _colors + +_fourPlayerPieces = { + Rook(_yellow): ChessVector((0, 3)), + Knight(_yellow): ChessVector((0, 4)), + Bishop(_yellow): ChessVector((0, 5)), + Queen(_yellow): ChessVector((0, 6)), + King(_yellow): ChessVector((0, 7)), + Bishop(_yellow): ChessVector((0, 8)), + Knight(_yellow): ChessVector((0, 9)), + Rook(_yellow): ChessVector((0, 10)), + Pawn(_yellow, "down"): ChessVector((1, 3)), + Pawn(_yellow, "down"): ChessVector((1, 4)), + Pawn(_yellow, "down"): ChessVector((1, 5)), + Pawn(_yellow, "down"): ChessVector((1, 6)), + Pawn(_yellow, "down"): ChessVector((1, 7)), + Pawn(_yellow, "down"): ChessVector((1, 8)), + Pawn(_yellow, "down"): ChessVector((1, 9)), + Pawn(_yellow, "down"): ChessVector((1, 10)), + + Rook(_green): ChessVector((3, 13)), + Knight(_green): ChessVector((4, 13)), + Bishop(_green): ChessVector((5, 13)), + Queen(_green): ChessVector((6, 13)), + King(_green): ChessVector((7, 13)), + Bishop(_green): ChessVector((8, 13)), + Knight(_green): ChessVector((9, 13)), + Rook(_green): ChessVector((10, 13)), + Pawn(_green, "left"): ChessVector((3, 12)), + Pawn(_green, "left"): ChessVector((4, 12)), + Pawn(_green, "left"): ChessVector((5, 12)), + Pawn(_green, "left"): ChessVector((6, 12)), + Pawn(_green, "left"): ChessVector((7, 12)), + Pawn(_green, "left"): ChessVector((8, 12)), + Pawn(_green, "left"): ChessVector((9, 12)), + Pawn(_green, "left"): ChessVector((10, 12)), + + Rook(_red): ChessVector((13, 3)), + Knight(_red): ChessVector((13, 4)), + Bishop(_red): ChessVector((13, 5)), + Queen(_red): ChessVector((13, 6)), + King(_red): ChessVector((13, 7)), + Bishop(_red): ChessVector((13, 8)), + Knight(_red): ChessVector((13, 9)), + Rook(_red): ChessVector((13, 10)), + Pawn(_red, "up"): ChessVector((12, 3)), + Pawn(_red, "up"): ChessVector((12, 4)), + Pawn(_red, "up"): ChessVector((12, 5)), + Pawn(_red, "up"): ChessVector((12, 6)), + Pawn(_red, "up"): ChessVector((12, 7)), + Pawn(_red, "up"): ChessVector((12, 8)), + Pawn(_red, "up"): ChessVector((12, 9)), + Pawn(_red, "up"): ChessVector((12, 10)), + + Rook(_blue): ChessVector((3, 0)), + Knight(_blue): ChessVector((4, 0)), + Bishop(_blue): ChessVector((5, 0)), + Queen(_blue): ChessVector((6, 0)), + King(_blue): ChessVector((7, 0)), + Bishop(_blue): ChessVector((8, 0)), + Knight(_blue): ChessVector((9, 0)), + Rook(_blue): ChessVector((10, 0)), + Pawn(_blue, "right"): ChessVector((3, 1)), + Pawn(_blue, "right"): ChessVector((4, 1)), + Pawn(_blue, "right"): ChessVector((5, 1)), + Pawn(_blue, "right"): ChessVector((6, 1)), + Pawn(_blue, "right"): ChessVector((7, 1)), + Pawn(_blue, "right"): ChessVector((8, 1)), + Pawn(_blue, "right"): ChessVector((9, 1)), + Pawn(_blue, "right"): ChessVector((10, 1)) +} +_disabled = [ + ChessVector((0, 0)), + ChessVector((0, 1)), + ChessVector((0, 2)), + ChessVector((1, 0)), + ChessVector((1, 1)), + ChessVector((1, 2)), + ChessVector((2, 0)), + ChessVector((2, 1)), + ChessVector((2, 2)), + ChessVector((0, 11)), + ChessVector((0, 12)), + ChessVector((0, 13)), + ChessVector((1, 11)), + ChessVector((1, 12)), + ChessVector((1, 13)), + ChessVector((2, 11)), + ChessVector((2, 12)), + ChessVector((2, 13)), + ChessVector((11, 0)), + ChessVector((11, 1)), + ChessVector((11, 2)), + ChessVector((12, 0)), + ChessVector((12, 1)), + ChessVector((12, 2)), + ChessVector((13, 0)), + ChessVector((13, 1)), + ChessVector((13, 2)), + ChessVector((11, 11)), + ChessVector((11, 12)), + ChessVector((11, 13)), + ChessVector((12, 11)), + ChessVector((12, 12)), + ChessVector((12, 13)), + ChessVector((13, 11)), + ChessVector((13, 12)), + ChessVector((13, 13)) +] +for piece, vector in _fourPlayerPieces.items(): + piece.vector = vector + +_pieceDict = {color: [piece for piece in _fourPlayerPieces.keys() if piece.color == color] for color in _colors} +_moveDict = {color: [Standard, CastleK, CastleQ] for color in _colors} +_promoteToDict = {color: [Queen, Rook, Knight, Bishop] for color in _colors} +_promoteFromDict = {color: [Pawn] for color in _colors} +_promoteAtDict = {color: 8 for color in _colors} + +CONFIG = { + "rows": 14, + "cols": 14, + "pieces": _pieceDict, + "moves": _moveDict, + "promoteTo": _promoteToDict, + "promoteFrom": _promoteFromDict, + "promoteAt": _promoteAtDict, + "disabled": _disabled, + "turnorder": ["red", "blue", "yellow", "green"] +} + +if __name__ == "__main__": + print(CONFIG) diff --git a/build/lib/pawnshop/configurations/__init__.py b/build/lib/pawnshop/configurations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config.pypirc b/config.pypirc new file mode 100644 index 0000000..787305b --- /dev/null +++ b/config.pypirc @@ -0,0 +1,7 @@ +[testpypi] +username = __token__ +password = pypi-AgENdGVzdC5weXBpLm9yZwIkZGVmMzQ2NmItYTRiZS00NDQyLWI0NGMtMjE1YjA2YTdhYTA2AAIleyJwZXJtaXNzaW9ucyI6ICJ1c2VyIiwgInZlcnNpb24iOiAxfQAABiBeZ1fDv4uC4Jd4PqJ6DyIgOIzHaihlRwrxALZuKY5fvA + +[pypi] +username = __token__ +password = pypi-AgEIcHlwaS5vcmcCJDA4OGFhYzUxLWU0ZGQtNDk5YS1hNzlmLTA0OTJmNDVkOTEzNwACJXsicGVybWlzc2lvbnMiOiAidXNlciIsICJ2ZXJzaW9uIjogMX0AAAYgYA0Y_MBPziPX1LkFMOTOyk1Az8oj6qVAhVFdz2OUW34 diff --git a/dist/pawnshop-1.0.3-py3-none-any.whl b/dist/pawnshop-1.0.3-py3-none-any.whl new file mode 100644 index 0000000..b252aa4 Binary files /dev/null and b/dist/pawnshop-1.0.3-py3-none-any.whl differ diff --git a/dist/pawnshop-1.0.3.tar.gz b/dist/pawnshop-1.0.3.tar.gz new file mode 100644 index 0000000..673a362 Binary files /dev/null and b/dist/pawnshop-1.0.3.tar.gz differ diff --git a/pawnshop.egg-info/PKG-INFO b/pawnshop.egg-info/PKG-INFO new file mode 100644 index 0000000..cc6a754 --- /dev/null +++ b/pawnshop.egg-info/PKG-INFO @@ -0,0 +1,27 @@ +Metadata-Version: 2.1 +Name: pawnshop +Version: 1.0.3 +Summary: A simple chess library as hobby project. +Home-page: https://github.com/NilsForssen/pawnshop +Author: Nils Forssén +Author-email: forssennils@gmail.com +License: UNKNOWN +Description: # Pawnshop + A simple chess package for python written solely as hobby project. I've also written a GUI with networking socket capabilities (add link here) and might create some AI furter down the line. + + The package also includes a 4-player mode, although the ruleset is unchanged from the classic game, Checkmate -> Game Over! + ## Background + This is my first real project to scale (> 1000 lines of code) and the first to be published to [PyPi](https://pypi.org/project/pawnshop/). + + The project has certainly been a product of time with other hobby projects and school often taking priority. This along with multiple mid-project large scale refractorizations, reconsiderations and laziness should explain the unorganized codebase. + + Needless to say, I've attempted to document the code well and create a proper package that I can "proudly" publish, Although I still don't recommend anyone to continue developing or using this outside of simple projects like this. There are definitely better alternatives out there. + + Some central documentation, more extensive testing and usage examples would have been beneficial, but as of now I just want to continue with other projects and leave this as finished for the time being. + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown diff --git a/pawnshop.egg-info/SOURCES.txt b/pawnshop.egg-info/SOURCES.txt new file mode 100644 index 0000000..2e512d2 --- /dev/null +++ b/pawnshop.egg-info/SOURCES.txt @@ -0,0 +1,25 @@ +.gitattributes +.gitignore +LICENSE +README.md +setup.py +dist/pawnshop-1.0.3-py3-none-any.whl +dist/pawnshop-1.0.3.tar.gz +pawnshop/ChessBoard.py +pawnshop/ChessVector.py +pawnshop/Exceptions.py +pawnshop/GameNotations.py +pawnshop/Moves.py +pawnshop/Pieces.py +pawnshop/Utils.py +pawnshop/__init__.py +pawnshop.egg-info/PKG-INFO +pawnshop.egg-info/SOURCES.txt +pawnshop.egg-info/dependency_links.txt +pawnshop.egg-info/top_level.txt +pawnshop/configurations/ClassicConfig.py +pawnshop/configurations/DefaultConfig.JSON +pawnshop/configurations/FourPlayerConfig.py +pawnshop/configurations/__init__.py +tests/test_1.py +tests/test_2.py \ No newline at end of file diff --git a/pawnshop.egg-info/dependency_links.txt b/pawnshop.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pawnshop.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pawnshop.egg-info/top_level.txt b/pawnshop.egg-info/top_level.txt new file mode 100644 index 0000000..de2b74e --- /dev/null +++ b/pawnshop.egg-info/top_level.txt @@ -0,0 +1 @@ +pawnshop diff --git a/pawnshop/ChessBoard.py b/pawnshop/ChessBoard.py new file mode 100644 index 0000000..b945643 --- /dev/null +++ b/pawnshop/ChessBoard.py @@ -0,0 +1,554 @@ +# 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 diff --git a/pawnshop/ChessVector.py b/pawnshop/ChessVector.py new file mode 100644 index 0000000..8aee5bc --- /dev/null +++ b/pawnshop/ChessVector.py @@ -0,0 +1,200 @@ +# ChessVector.py + +from typing import Union, Tuple, List, TYPE_CHECKING +from .Utils import toAlpha, inverseIdx, countAlpha +# import pawnshop.ChessBoard + +if TYPE_CHECKING: + from pawnshop.ChessBoard import Board + + +class ChessVector(object): + """ChessVector object + + Object to store position on chessboard + Initialize object with position in (row, col) or string notation format + If a string notation format is given, the board must also be given + The vector supports common operations such as addition, multiplication with other vectors + + :param position: Tuple or string notation position on chessboard + :param board: Board to use when determining position given by string notation (default is None) + """ + + def __init__(self, position: Union[Tuple[int, int], str], board=None): + self._row = 0 + self._col = 0 + + if isinstance(position, tuple): + row, col = position + self.row = int(row) + self.col = int(col) + elif isinstance(position, str) and not board is None: + position = position.lower() + for char in position: + if char.isdigit(): + i = position.find(char) + if i == 0: + raise ValueError("Position does not include column!") + alpha = position[:i] + num = position[i::] + row = board.getRows() - int(num) + for n, a in countAlpha(): + if a == alpha: + col = n + break + else: + continue + break + else: + raise ValueError("position does not include row!") + self.row = row + self.col = col + else: + raise ValueError("Position is not a string or a tuple!") + + @property + def col(self): + return self._col + + @col.setter + def col(self, newCol): + self._col = newCol + + @property + def row(self): + return self._row + + @row.setter + def row(self, newRow): + self._row = newRow + + def __sub__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row - other.row, self.col - other.col)) + else: + return ChessVector((self.row - other, self.col - other)) + + def __rsub__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row - self.row, other.col - self.col)) + else: + raise ValueError(f"Cannot subtract {type(self)} from non-{type(self)}!") + + def __add__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row + other.row, self.col + other.col)) + else: + return ChessVector((self.row + other, self.col + other)) + + def __radd__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row + self.row, other.col + self.col)) + else: + raise ValueError(f"Cannot add {type(self)} to non-{type(self)}!") + + def __mul__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row * other.row, self.col * other.col)) + else: + return ChessVector((self.row * other, self.col * other)) + + def __rmul__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row * self.row, other.col * self.col)) + else: + raise ValueError(f"Cannot multiply non-{type(self)} by {type(self)}!") + + def __div__(self, other): + if isinstance(other, ChessVector): + return ChessVector((self.row / other.row, self.col / other.col)) + else: + return ChessVector((self.row / other, self.col / other)) + + def __rdiv__(self, other): + if isinstance(other, ChessVector): + return ChessVector((other.row / self.row, other.col / self.col)) + else: + raise ValueError(f"Cannot divide non-{type(self)} by {type(self)}!") + + def __neg__(self): + return ChessVector((-self.row, -self.col)) + + def __pos__(self): + return ChessVector((+self.row, +self.col)) + + def __eq__(self, other): + if isinstance(other, ChessVector): + return self.row == other.row and self.col == other.col + else: + raise ValueError(f"Cannot compare {type(self)} with {type(other)}!") + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if isinstance(other, ChessVector): + return self.row < other.row and self.col < other.col + else: + raise ValueError(f"Cannot compare {type(self)} with {type(other)}!") + + def __gt__(self, other): + return not self < other + + def __le__(self, other): + return self < other or self == other + + def __ge__(self, other): + return self > other or self == other + + def __repr__(self): + return str((self.row, self.col)) + + def __str__(self): + return str((self.row, self.col)) + + def tuple(self) -> Tuple[int, int]: + """Return tuple format of vector + + :returns: (row, col) tuple + :rType: ´´tuple´´ + """ + return (self._row, self._col) + + def getStr(self, board: "Board") -> str: + """Return string notation format of vector + + :param board: Board to determine string position from + :returns: string notation of vector position + :rType: ´´str´´ + """ + notation = "" + notation += toAlpha(self.col) + notation += inverseIdx(self.row, board) + return notation + + def matches(self, otherVecs: List["ChessVector"]) -> bool: + """Check if vector matches any of other vectors + + :param otherVecs: List of other vectors + :returns: If match is found or not + :rType: ´´bool´´ + """ + for vec in otherVecs: + if self.row == vec.row and self.col == vec.col: + return True + else: + return False + + def copy(self) -> "ChessVector": + """Create a new copy of this vector + + :returns: Copy of this vector + :rType: ´´ChessVector´´ + """ + return ChessVector((self.row, self.col)) + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/pawnshop/Exceptions.py b/pawnshop/Exceptions.py new file mode 100644 index 0000000..5b1144d --- /dev/null +++ b/pawnshop/Exceptions.py @@ -0,0 +1,42 @@ +# Exceptions.py + +class Illegal(Exception): + """Move is ilegal""" + pass + + +class IllegalMove(Illegal): + def __init__(self, startPos, targetPos, + msg="Piece at {0} cannot move to {1}!"): + super().__init__(msg.format(startPos, targetPos)) + + +class CheckMate(Illegal): + def __init__(self, msg="Your king is in checkmate!"): + super().__init__(msg) + + +class EmptyError(IndexError): + def __init__(self, position, msg="Position {0} is empty!"): + super().__init__(msg.format(position)) + + +class DisabledError(IndexError): + def __init__(self, position, msg="Position {0} is out of bounce!"): + super().__init__(msg.format(position)) + + +class PromotionError(Exception): + def __init__(self, msg="Moved piece needs to be promoted!"): + super().__init__(msg) + + +class TurnError(Exception): + def __init__(self, msg="Wrong player!"): + super().__init__(msg) + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/pawnshop/GameNotations.py b/pawnshop/GameNotations.py new file mode 100644 index 0000000..4c011e5 --- /dev/null +++ b/pawnshop/GameNotations.py @@ -0,0 +1,194 @@ +# GameNotations.py + +import re +from copy import deepcopy + +from .ChessBoard import ( + initClassic, + Board +) +from .configurations import ClassicConfig +from .ChessVector import ChessVector +from .Pieces import * +from .Utils import toAlpha +from .Moves import ( + CastleK, + CastleQ +) + + +STANDARDTAGS = [ + "Event", + "Site", + "Date", + "Round", + "White", + "Black", + "Result" +] +OPTIONALTAGS = [ + "Annotator", + "PlyCount", + "TimeControl", + "Time", + "Termination", + "Mode", + "FEN" +] +ALLTAGS = [*STANDARDTAGS, *OPTIONALTAGS] + + +def board2PGN(board: Board, **tags) -> str: + """Get Portable Game Notation from board + + :param board: Board to get notation from + :param **tags: Tags added to the notation + :returns: PGN string + :rtype: ``str`` + + :**tags: Tags found in STANDARDTAGS and OPTIONALTAGS + """ + PGNString = "" + tags = {t.lower(): v for t, v in tags.items()} + + for TAG in ALLTAGS: + if TAG.lower() in tags: + PGNString += f"[{TAG} \"{str(tags[TAG.lower()])}\"]\n" + i = 0 + while i * 2 < len(board.history): + i += 1 + PGNString += str(i) + ". " + " ".join(board.history[(i - 1) * 2:i * 2:1]) + "\n" + + return PGNString + + +def PGN2Board(PGNString: str) -> Board: + """Get Board object from Portable Game Notation + + :param PGNString: PGN string + :returns: Board object from PGN + :rtype: ``Board`` + """ + notations = re.finditer(r"\s*(?PO-O-O)|(?PO-O)|(?P[A-Z]*)(?P[a-h]?)(?P[x]?)(?P[a-h]+)(?P\d+)=?(?P[A-Z]?)\+*\#?", PGNString) + + board = initClassic() + for i, notation in enumerate(notations): + color = ["white", "black"][i % 2 == 1] + if (not notation.group("castleK") is None) or (not notation.group("castleQ") is None): + for king in board.kings[color]: + for move in board.moves[color]: + if ((not notation.group("castleK") is None) and move is CastleK) or ((not notation.group("castleQ") is None) and move is CastleQ): + board.movePiece(king.vector, move.getDestinations(king, board).pop(), checkMove=False, ignoreMate=True, checkForCheck=False, printOut=False, ignoreOrder=True) + break + else: + continue + break + else: + for piece in board.pieces[color]: + vector = ChessVector(notation.group("col") + notation.group("rank"), board) + if vector.matches(piece.getMoves(board)): + pType = pieceNotations[notation.group("piece")] + if isinstance(piece, pType): + if notation.group("pcol") == "" or notation.group("pcol") == toAlpha(piece.vector.col): + board.movePiece(piece.vector, vector, checkMove=False, promote=pieceNotations[notation.group("promote")], ignoreMate=True, checkForCheck=False, printOut=False, ignoreOrder=True) + break + else: + continue + else: + continue + + board.checkForCheck() + return board + + +def FEN2Board(FENString: str) -> Board: + """Get Board object from Forsyth-Edwards-Notation + + :param FENString: Forsyth-Edwards-Notation + :returns: Board object from FEN + :rtype: ``Board`` + """ + board = Board() + config = deepcopy(ClassicConfig.CONFIG) + del config["pieces"] + board.setup(config) + + fieldFinder = re.finditer(r"[^ ]+", FENString) + rowFinder = re.finditer(r"([^/]+)", next(fieldFinder).group()) + + for rowi, row in enumerate(rowFinder): + coli = 0 + for chari, char in enumerate(row.group(0)): + if char.isnumeric(): + for coli in range(coli, coli + int(char)): + vector = ChessVector((rowi, coli), board) + board[vector] = Empty(vector) + coli += 1 + else: + vector = ChessVector((rowi, coli), board) + + if char.isupper(): + board[vector] = pieceNotations[char]("white", direction="up") + elif char.islower(): + board[vector] = pieceNotations[char.upper()]("black", direction="down") + coli += 1 + + # No other fields are critical, might implement more later + return board + + +def board2FEN(board: Board) -> str: + """Get Forsyth-Edward-Notation from board + + The notation does not account for: + current turn, castling potential, en-passant or move count + - only the position is notated (I am lazy) + + :param board: Board to get FEN from + :returns: FEN string + :rtype: ``str`` + """ + FENString = "" + for rowi, row in enumerate(board._board): + empty = 0 + for coli, piece in enumerate(row): + if isinstance(piece, Empty) or isinstance(piece, Disabled): + empty += 1 + else: + if empty: + FENString += str(empty) + if piece.color == "white": + ps = piece.symbol.upper() + elif piece.color == "black": + ps = piece.symbol.lower() + FENString += ps + empty = 0 + + if empty: + FENString += str(empty) + if not rowi == board.getRows() - 1: + FENString += "/" + + return FENString + + +def readable(historyList: List[str], players=2) -> str: + """Get printable format of history + + :param historyList: History to be read + :param players: How many players the history includes + :returns: Readable string of history + :rtype: ``str`` + """ + finalString = "" + i = 0 + while i * players < len(historyList): + i += 1 + finalString += str(i) + ". " + " - ".join(historyList[(i - 1) * players:i * players:1]) + "\n" + return finalString.strip() + + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/pawnshop/Moves.py b/pawnshop/Moves.py new file mode 100644 index 0000000..0784822 --- /dev/null +++ b/pawnshop/Moves.py @@ -0,0 +1,397 @@ +# Moves.py + +from typing import List, Union, Tuple, TYPE_CHECKING +from abc import ABC, abstractclassmethod +from .Pieces import * +from .Utils import createNotation +from .Exceptions import PromotionError +from .ChessVector import ChessVector + +if TYPE_CHECKING: + from .ChessBoard import Board + + +class Move(ABC): + """Abstract class for moves in chess + """ + + @abstractclassmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Test if piece satisfies move requirement + """ + raise NotImplementedError + + @abstractclassmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> list: + """Return list of possible destinations + """ + raise NotImplementedError + + @abstractclassmethod + def action(thisMove, startPiece, targetPos, board, *args, **kwargs) -> str: + """Move the piece + """ + raise NotImplementedError + + +class Standard(Move): + """Standard move in chess + + Moves piece according to .getStandardMoves() method + """ + + @classmethod + def pieceCondition(thisMove, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Since there is no condition to standard moves, this will always return True. + + :returns: True + :rtype: ``bool`` + """ + return True + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Calls piece.getStandardMoves() method to get all standard moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + return piece.getStandardMoves(board) + + @classmethod + def action(thisMove, startPiece: Piece, targetVec: ChessVector, board: "Board", promote=None, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to standard move rules. + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :param promote: Promotion type of piece (default is None) + :returns: Notation of move + :rtype: ``str`` + """ + promo = False + + for pieceType in board.getPromoteFrom(startPiece.color): + if isinstance(startPiece, pieceType): + + if startPiece.rank + abs((startPiece.vector - targetVec).tuple()[startPiece.forwardVec.col]) == board.getPromoteAt(startPiece.color): + if promote is None: + raise PromotionError + + if promote not in board.getPromoteTo(startPiece.color): + raise PromotionError( + f"{startPiece.color} cannot promote to {promote}!") + + promo = True + + break + + targetPiece = board[targetVec] + + notation = createNotation( + board, startPiece, targetVec, + isPawn=isinstance(startPiece, Pawn), capture=not isinstance(targetPiece, Empty)) + + if not isinstance(targetPiece, Empty): + board[targetVec] = Empty(targetVec) + board.swapPositions(startPiece.vector, targetVec) + else: + board.swapPositions(startPiece.vector, targetVec) + if promo: + newPiece = promote(startPiece.color) + newPiece.move(startPiece.vector) + board[startPiece.vector] = newPiece + notation += "=" + newPiece.symbol + + return notation + + +class _Castling(Move): + """Parent class to King-side and Queen-side castling + """ + + @classmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Must be pieces first move and piece must be instance of ``King``. + + :param piece: Piece to check + :returns: If piece satisfies requirements + :rtype: ``bool`` + """ + return piece.firstMove and isinstance(piece, King) + + @classmethod + def action(thisMove, startPiece: Piece, targetVec: ChessVector, board: "Board", *args, **kwargs) -> None: + """Performs the action of move + + Moves piece according to move rules. + Returns None as Queen-side and King-side castling are noted differently. + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + """ + for rook in thisMove.findRooks(startPiece, board): + between = thisMove.findBetween(startPiece.vector, rook.vector) + if targetVec in between: + kingTarget, rookTarget = thisMove.getTargets(between) + board.swapPositions(startPiece.vector, kingTarget) + board.swapPositions(rook.vector, rookTarget) + break + else: + raise ValueError(f"Piece cannot move to {targetVec}") + + def findBetween(vec1: ChessVector, vec2: ChessVector) -> List[ChessVector]: + """Helper function to find all positions between two positions + + Helper function. + If there are not positions between or given positions + are not in a row, the returned list is empty. + + :param vec1: First position + :param vec2: Second position + :returns: List of possitions betweeen + :rtype: ``list`` + """ + rowStep = vec1.row - vec2.row and (1, -1)[vec1.row - vec2.row < 0] + colStep = vec1.col - vec2.col and (1, -1)[vec1.col - vec2.col < 0] + + if not rowStep: + colRange = range(vec2.col + colStep, vec1.col, colStep) + rowRange = [vec1.row] * len(colRange) + elif not colStep: + rowRange = range(vec2.row + rowStep, vec1.row, rowStep) + colRange = [vec1.col] * len(rowRange) + else: + rowRange = range(0, 0) + colRange = range(0, 0) + + return [ChessVector(idx) for idx in zip(rowRange, colRange)] + + def emptyBetween(board: "Board", between: List[ChessVector]) -> bool: + """Check if all positions are emtpy + + Helper funciton. + Check if all positions between two pieces are empty + + :param board: Board to check positions in + :param between: List of positions to check + :returns: If all positions are empty or not + :rtype: ``bool`` + """ + for vector in between: + if not isinstance(board[vector], Empty): + return False + else: + return True + + def findRooks(piece: Piece, board: "Board") -> List[Piece]: + """Find all rooks in board that are on same lane as piece + + Helper function. + Iterates through all pieces on board looking for + rooks on same lane as piece. + + :param piece: Piece to check for same lane + :param board: Board to check for rooks in + :returns: List of rooks on same lane as piece + :rtype: ``list`` + """ + def vecCondition(vec1, vec2): + return bool(vec2.row - vec1.row) != bool(vec2.col - vec1.col) and (not vec2.row - vec1.row or not vec2.col - vec2.col) + + rookList = [] + for p in board.iterPieces(piece.color): + if isinstance(p, Rook) and p.firstMove and vecCondition(piece.vector, p.vector): + rookList.append(p) + return rookList + + def getTargets(between: list) -> Union[Tuple[ChessVector], None]: + """Get castling targets + + Helper function + Get the two middle squares of list of positions between. + If list is of length 1, this returns None. + Biased towards the start of list. + + + :param between: List of positions between + :returns: Tuple of target positions + :rtype: ``tuple`` or None + """ + if not len(between) > 1: + return None + if not len(between) % 2: + target1 = between[int((len(between) / 2) - 1)] + target2 = between[int((len(between) / 2))] + else: + target1 = between[int((len(between) / 2) - 0.5)] + target2 = between[int((len(between) / 2) + 0.5)] + return (target1, target2) + + +class CastleK(_Castling): + """Castle King-side move + """ + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible castling moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + if not board.getChecks(piece.color): + for rook in thisMove.findRooks(piece, board): + between = thisMove.findBetween(piece.vector, rook.vector) + if thisMove.emptyBetween(board, between) and not len(between) % 2: + kingTarget, _ = thisMove.getTargets(between) + walked = thisMove.findBetween(piece.vector, kingTarget) + for vec in walked: + if board.isThreatened(vec, piece.color): + break + else: + destList.append(kingTarget) + return destList + + @classmethod + def action(thisMove, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the castling move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + super().action(*args, **kwargs) + return "O-O" + + +class CastleQ(_Castling): + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible castling moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + if not board.getChecks(piece.color): + for rook in thisMove.findRooks(piece, board): + between = thisMove.findBetween(piece.vector, rook.vector) + if thisMove.emptyBetween(board, between) and len(between) % 2: + kingTarget, _ = thisMove.getTargets(between) + walked = thisMove.findBetween(piece.vector, kingTarget) + for vec in walked: + if board.isThreatened(vec, piece.color): + break + else: + destList.append(kingTarget) + return destList + + @classmethod + def action(thisMove, *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the castling move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + super().action(*args, **kwargs) + return "O-O-O" + + +class EnPassant(Move): + """Special move en-passant + """ + + @classmethod + def pieceCondition(thisMove, piece: Piece, *args, **kwargs) -> bool: + """Moving piece must satisfy this condition + + Piece must be of instance ``Pawn`` + + :param piece: Piece to check + :returns: If piece satisfies requirements + :rtype: ``bool`` + """ + return isinstance(piece, Pawn) + + @classmethod + def getDestinations(thisMove, piece: Piece, board: "Board", *args, **kwargs) -> List[ChessVector]: + """Get possible destinations of piece + + Returns all possible en-passant moves of piece. + Call pieceCondition() classmethod prior. + + :param piece: Piece to get moves from + :param board: Board which piece is stored in + :returns: list of possible destinations + :rtype: list + """ + destList = [] + for diagVec in (piece.lDiagVec, piece.rDiagVec): + checkVec = (piece.vector - piece.forwardVec) + diagVec + try: + if isinstance(board[checkVec], Pawn) and board[checkVec].passed and board[checkVec].forwardVec == -piece.forwardVec: + destList.append(piece.vector + diagVec) + except IndexError: + pass + return destList + + @classmethod + def action(thisMove, piece: Piece, targetVec: ChessVector, board: "Board", *args, **kwargs) -> str: + """Performs the action of move + + Moves piece according to move rules. + Returns the notation of the en-passant move + Call pieceCondition() and getDestinations() classmethods prior. + + :param startPiece: Piece to be moved + :param targetVec: Destination of piece move + :param board: Board to perform move in + :returns: Notation of move + :rtype: str + """ + notation = createNotation(board, piece, targetVec, + isPawn=True, capture=True) + + board[targetVec - piece.forwardVec] = Empty(targetVec - piece.forwardVec) + board.swapPositions(piece.vector, targetVec) + return notation diff --git a/pawnshop/Pieces.py b/pawnshop/Pieces.py new file mode 100644 index 0000000..fe9b84c --- /dev/null +++ b/pawnshop/Pieces.py @@ -0,0 +1,423 @@ +# Pieces.py + +from copy import deepcopy +from abc import ABC, abstractmethod +from typing import List, Tuple, TYPE_CHECKING +from .Utils import _positivePos, _catchOutofBounce, removeDupes +from .ChessVector import ChessVector + +if TYPE_CHECKING: + from .ChessBoard import Board + +_directions = { + "up": ((-1, 0), (-1, -1), (-1, 1)), + "down": ((1, 0), (1, 1), (1, -1)), + "right": ((0, 1), (-1, 1), (1, 1)), + "left": ((0, -1), (1, -1), (-1, -1)) +} +_directions = {key: [ChessVector(offset) for offset in _directions[key]] for key in _directions} + + +class Piece(ABC): + """Abstract base class for pieces + + :param color: Color of piece + :param value: Numerical value of piece + :param symbol: Char symbol of piece + """ + + def __init__(self, color: str, value: int, symbol: str, *args, **kwargs): + self.vector = None + self.color = color + self.value = value + self.symbol = symbol + self.firstMove = True + + def __str__(self): + return self.color[0] + self.symbol + + @abstractmethod + def getStandardMoves(self, board: "Board"): + """Returns standard destinations of piece in board + """ + raise NotImplementedError + + def getMoves(self, board: "Board", ignoreCheck=False, ignoreMate=False) -> List[ChessVector]: + """Returns all moves of piece in board + + Uses board.getMoves() method to check what moves piece is allowed to. + + :param board: Board to move in + :param **Flags: Flags to pass into move + :returns: List of possible moves + :rtype: ``list`` + + :**Flags: + :ignoreCheck (False): Ignore checks when getting moves + :ignoreMate (False): Ignore checkmate when getting moves + """ + destList = [] + for move in board.getMoves(self.color): + if move.pieceCondition(self): + destList.extend(move.getDestinations(self, board)) + + if not ignoreCheck: + remove = [] + + for dest in destList: + testBoard = deepcopy(board) + testBoard.movePiece(self.vector, dest, ignoreMate=ignoreMate, checkForMate=False, printout=False, checkMove=False, promote=Queen, ignoreOrder=True) + if testBoard.getChecks(self.color): + remove.append(dest) + + for dest in remove: + destList.remove(dest) + + return destList + + def move(self, destVector: ChessVector) -> None: + """Move piece to destination + + :param destVector: Destination + """ + self.vector = destVector + self.firstMove = False + + def postAction(self, board: "Board") -> None: + """Do action after piece is moved in board + + Call this after a piece is moved in board + """ + pass + + @_positivePos + @_catchOutofBounce + def canWalk(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can walk to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can move + :rtype: ``bool`` + """ + return board.isEmpty(vector) + + @_positivePos + @_catchOutofBounce + def canCapture(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can capture to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can capture + :rtype: ``bool`` + """ + destPiece = board[vector] + try: + return destPiece.color != self.color + except AttributeError: + return False + + @_positivePos + @_catchOutofBounce + def canMove(self, vector: ChessVector, board: "Board") -> bool: + """Check if piece can capture to destination in board + + :param vector: Destination + :param board: Board to check in + :returns: If piece can move (capture or walk) + :rtype: ``bool`` + """ + destPiece = board[vector] + try: + return destPiece.color != self.color + except AttributeError: + return board.isEmpty(vector) + + def _getMovesInLine(self, iterVector: ChessVector, board: "Board") -> List[ChessVector]: + """Get moves in one line + + Return all positions piece is can move to iterating with iterVector. + Stops if piece can capture as piece cannot continue moving after capturing. + + :param iterVector: Vector to iterate moves with + :param board: Board to check in + :returns: List of possible destinations + :rtype: ``list`` + """ + moveList = [] + newV = self.vector + while True: + newV += iterVector + if self.canWalk(newV, board): + moveList.append(newV) + elif self.canCapture(newV, board): + moveList.append(newV) + break + else: + break + return moveList + + +class Pawn(Piece): + """Pawn object + + :param color: Color of piece + :param direction: Movement direction of Pawn (default is "up") + :param rank: Starting rank of pawn, used to calc promote + """ + + def __init__(self, color: str, direction="up", rank=2, *args, **kwargs): + super().__init__(color, 1, "P") + + self.passed = False + self.direction = direction.lower() + self.rank = rank + + if direction in _directions.keys(): + self.forwardVec, self.lDiagVec, self.rDiagVec = _directions[direction] + else: + raise ValueError(f"Direction is not any of {_directions.keys()}") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + destVec = self.vector + self.forwardVec + + if self.canWalk(destVec, board): + destList.append(destVec) + if self.firstMove: + destVec += self.forwardVec + if self.canWalk(destVec, board): + destList.append(destVec) + + for destVec in self.getAttacking(board): + if self.canCapture(destVec, board): + destList.append(destVec) + + return destList + + def move(self, newV: ChessVector) -> None: + """Move piece to destination + + If Pawn moves 2 places, it can be captured by en-passant. + + :param newV: Destination + """ + if self.firstMove: + if abs(self.vector.row - newV.row) == 2 or abs(self.vector.col - newV.col) == 2: + self.passed = True + self.rank += 1 + self.rank += 1 + super().move(newV) + + def postAction(self, *args, **kwargs): + """Do action after piece is moved in board + + Call this after a piece is moved in board + """ + self.passed = False + + def getAttacking(self, *args, **kwargs) -> Tuple[ChessVector]: + """Get the threatened positions of piece + + :returns: Tuple of threatened positions + :rType: ´´tuple´´ + """ + return [self.vector + self.lDiagVec, self.vector + self.rDiagVec] + + +class Rook(Piece): + """Rook object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 5, "R") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + forwardVec = vecTuple[0] + destList.extend(self._getMovesInLine(forwardVec, board)) + return destList + + +class Knight(Piece): + """Knight object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 3, "N") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + offsetList = [ + (1, 2), + (1, -2), + (-1, 2), + (-1, -2), + (2, 1), + (2, -1), + (-2, 1), + (-2, -1) + ] + vecList = [ChessVector(offset) for offset in offsetList] + + for offsetVec in vecList: + destVec = self.vector + offsetVec + if self.canMove(destVec, board): + destList.append(destVec) + return destList + + +class Bishop(Piece): + """Bishop object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 3, "B") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + destList.extend(self._getMovesInLine(vecTuple[1], board)) + return destList + + +class King(Piece): + """King object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, int(1e10), "K") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for offsetVec in removeDupes([vec for vecList in _directions.values() for vec in vecList]): + destVec = self.vector + offsetVec + if self.canMove(destVec, board): + destList.append(destVec) + return destList + + +class Queen(Piece): + """Queen object + + :param color: Color of piece + """ + + def __init__(self, color: str, *args, **kwargs): + super().__init__(color, 9, "Q") + + def getStandardMoves(self, board: "Board") -> List[ChessVector]: + """Returns standard destinations of piece in board + + :param board: Board to check in + :returns: List of standard posssible destinations + :rtype: ``list`` + """ + destList = [] + for vecTuple in _directions.values(): + destList.extend(self._getMovesInLine(vecTuple[0], board)) + destList.extend(self._getMovesInLine(vecTuple[1], board)) + return destList + + +class Disabled(): + """Disabled object + + Object for representing disabled positions in chessboard + + :param vector: Position of disabled square + """ + + def __init__(self, vector: ChessVector, *args, **kwargs): + self.vector = vector + + def __str__(self): + return " " + + def move(self, vec: ChessVector): + """Move disabled object + + Move the disabled square + + :param vec: New position + """ + self.vector = vec + + +class Empty(): + """Empty object + + Object for representing empty positions in chessboard + + :param vector: Position of empty square + """ + + def __init__(self, vector: ChessVector, *args, **kwargs): + self.vector = vector + + def __str__(self): + return "__" + + def move(self, vec: ChessVector): + """Move empty object + + Move the empty square + + :param vec: New position + """ + self.vector = vec + + +pieceNotations = { + "P": Pawn, + "N": Knight, + "B": Bishop, + "R": Rook, + "Q": Queen, + "K": King +} + +if __name__ == "__main__": + + # Do some testing + pass diff --git a/pawnshop/Utils.py b/pawnshop/Utils.py new file mode 100644 index 0000000..0eb9da3 --- /dev/null +++ b/pawnshop/Utils.py @@ -0,0 +1,159 @@ +# Utils.py + +from typing import List, Generator, TYPE_CHECKING +from string import ascii_lowercase +import sys, os + +if TYPE_CHECKING: + from .ChessBoard import Board + from .ChessVector import ChessVector + from .Pieces import Piece + +def _catchOutofBounce(func): + """Decorator for catching out of bounce ´´IndexError´´""" + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except IndexError: + return False + return wrapper + + +def _positivePos(func): + """Decorator for ensuring a position is not negative""" + def wrapper(pInstance, vector, bInstance, *args, **kwargs): + if not vector.row < 0 and not vector.col < 0: + return func(pInstance, vector, bInstance, *args, **kwargs) + else: + return False + return wrapper + + +def removeDupes(vectorList: List["ChessVector"]) -> List["ChessVector"]: + """Remove duplicate positions + + :param vectorList: List to remove duplicates from + :returns: List without duplicates + :rtype: ``list`` + """ + for i, superVec in enumerate(vectorList): + if superVec.matches(vectorList[i + 1::]): + vectorList.remove(superVec) + return removeDupes(vectorList) + else: + return vectorList + + +def createNotation(board: "Board", startPiece: "Piece", targetVec: "ChessVector", isPawn=False, capture=False) -> str: + """Create a notation for a move + + Creates notation of move according to standard chess notation. + + :param startPiece: Piece to be moved + :param targetVec: Destination of move + :param **Flags: Flags to create notation + :returns: Notation of move + :rtype: ``str`` + + :**Flags: + :isPawn (True): + :capture (True): + """ + notation = "" + targetNot = targetVec.getStr(board) + + if not isPawn: + notation = startPiece.symbol + for piece in board.iterPieces(startPiece.color): + if piece is not startPiece and isinstance(piece, type(startPiece)): + if targetVec.matches(piece.getMoves(board, ignoreCheck=True)): + if piece.vector.col == startPiece.vector.col: + notation += inverseIdx(startPiece.vector.row, board) + else: + notation += toAlpha(startPiece.vector.col) + break + elif capture: + notation = toAlpha(startPiece.vector.col) + + if capture: + notation += "x" + + notation += targetNot + return notation + + +def countAlpha() -> Generator[str, None, None]: + """Generator to count in alphabetical order + + Counts in alphabetical order. + a->b->c->...->aa->ab->...->ba->... + + :yields: Character + :ytype: ``generator`` + """ + stringList = [0] + num = 0 + while True: + yield (num, "".join([ascii_lowercase[num] for num in stringList])) + i = 1 + num += 1 + + while True: + if i > len(stringList): + stringList.insert(0, 0) + break + else: + changeTo = stringList[-i] + 1 + if changeTo >= len(ascii_lowercase): + stringList[-i::] = [0] * (i) + i += 1 + continue + else: + stringList[-i] = changeTo + break + + +def inverseIdx(idx: int, board: "Board") -> str: + """Inverse index + + Inverses idx given board rows and returns string + + :param idx: Index to reverse + :param board: Board to reverse according to rows + :returns: Reversed index + :rtype: ``str`` + """ + return str(board.getRows() - idx) + + +def toAlpha(num: int) -> str: + """Convert number to alphabetical + + Counts through all alpha until reaching number. + (I tried to make it not have to count through all alphas, + however, since my alpha system doesn't match any regular + base number system I was not able to.) + + :param num: Number to convert + :returns: Alphabetical string from num + :rtype: str + """ + for n, notation in countAlpha(): + if num == n: + return notation + +def getResourcePath(relative_path): + """ + Get pyinstaller resource + """ + + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + + return os.path.join(os.path.abspath("."), relative_path) + + +if __name__ == "__main__": + # Do some testing + + pass diff --git a/pawnshop/__init__.py b/pawnshop/__init__.py new file mode 100644 index 0000000..f27784b --- /dev/null +++ b/pawnshop/__init__.py @@ -0,0 +1,6 @@ +# import pawnshop.ChessBoard +# import pawnshop.ChessVector +# import pawnshop.Utils +# import pawnshop.GameNotations +__all__ = ["ChessVector", "ChessBoard", "GameNotations", "Utils", "Moves", "Utils", "Pieces", "Exceptions"] +from . import * diff --git a/pawnshop/__pycache__/ChessBoard.cpython-37.pyc b/pawnshop/__pycache__/ChessBoard.cpython-37.pyc new file mode 100644 index 0000000..3fb1774 Binary files /dev/null and b/pawnshop/__pycache__/ChessBoard.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/ChessBoard.cpython-39.pyc b/pawnshop/__pycache__/ChessBoard.cpython-39.pyc new file mode 100644 index 0000000..f519673 Binary files /dev/null and b/pawnshop/__pycache__/ChessBoard.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/ChessVector.cpython-37.pyc b/pawnshop/__pycache__/ChessVector.cpython-37.pyc new file mode 100644 index 0000000..5f8b18f Binary files /dev/null and b/pawnshop/__pycache__/ChessVector.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/ChessVector.cpython-39.pyc b/pawnshop/__pycache__/ChessVector.cpython-39.pyc new file mode 100644 index 0000000..2a5b744 Binary files /dev/null and b/pawnshop/__pycache__/ChessVector.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/ClassicConfig.cpython-37.pyc b/pawnshop/__pycache__/ClassicConfig.cpython-37.pyc new file mode 100644 index 0000000..aa42caa Binary files /dev/null and b/pawnshop/__pycache__/ClassicConfig.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/ClassicConfig.cpython-39.pyc b/pawnshop/__pycache__/ClassicConfig.cpython-39.pyc new file mode 100644 index 0000000..70ce3d8 Binary files /dev/null and b/pawnshop/__pycache__/ClassicConfig.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/Exceptions.cpython-37.pyc b/pawnshop/__pycache__/Exceptions.cpython-37.pyc new file mode 100644 index 0000000..74c5294 Binary files /dev/null and b/pawnshop/__pycache__/Exceptions.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/Exceptions.cpython-39.pyc b/pawnshop/__pycache__/Exceptions.cpython-39.pyc new file mode 100644 index 0000000..311efe6 Binary files /dev/null and b/pawnshop/__pycache__/Exceptions.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/FourPlayerConfig.cpython-37.pyc b/pawnshop/__pycache__/FourPlayerConfig.cpython-37.pyc new file mode 100644 index 0000000..e7e6f5e Binary files /dev/null and b/pawnshop/__pycache__/FourPlayerConfig.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/FourPlayerConfig.cpython-39.pyc b/pawnshop/__pycache__/FourPlayerConfig.cpython-39.pyc new file mode 100644 index 0000000..861831f Binary files /dev/null and b/pawnshop/__pycache__/FourPlayerConfig.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/GameNotations.cpython-39.pyc b/pawnshop/__pycache__/GameNotations.cpython-39.pyc new file mode 100644 index 0000000..ec8e9d8 Binary files /dev/null and b/pawnshop/__pycache__/GameNotations.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/Moves.cpython-37.pyc b/pawnshop/__pycache__/Moves.cpython-37.pyc new file mode 100644 index 0000000..85cb88f Binary files /dev/null and b/pawnshop/__pycache__/Moves.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/Moves.cpython-39.pyc b/pawnshop/__pycache__/Moves.cpython-39.pyc new file mode 100644 index 0000000..bd72137 Binary files /dev/null and b/pawnshop/__pycache__/Moves.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/Pieces.cpython-37.pyc b/pawnshop/__pycache__/Pieces.cpython-37.pyc new file mode 100644 index 0000000..43501bd Binary files /dev/null and b/pawnshop/__pycache__/Pieces.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/Pieces.cpython-39.pyc b/pawnshop/__pycache__/Pieces.cpython-39.pyc new file mode 100644 index 0000000..6c05d30 Binary files /dev/null and b/pawnshop/__pycache__/Pieces.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/Utils.cpython-37.pyc b/pawnshop/__pycache__/Utils.cpython-37.pyc new file mode 100644 index 0000000..23da5f6 Binary files /dev/null and b/pawnshop/__pycache__/Utils.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/Utils.cpython-39.pyc b/pawnshop/__pycache__/Utils.cpython-39.pyc new file mode 100644 index 0000000..b04792f Binary files /dev/null and b/pawnshop/__pycache__/Utils.cpython-39.pyc differ diff --git a/pawnshop/__pycache__/__init__.cpython-37.pyc b/pawnshop/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..a8f4e9e Binary files /dev/null and b/pawnshop/__pycache__/__init__.cpython-37.pyc differ diff --git a/pawnshop/__pycache__/__init__.cpython-39.pyc b/pawnshop/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..27d5691 Binary files /dev/null and b/pawnshop/__pycache__/__init__.cpython-39.pyc differ diff --git a/pawnshop/configurations/ClassicConfig.py b/pawnshop/configurations/ClassicConfig.py new file mode 100644 index 0000000..039cf47 --- /dev/null +++ b/pawnshop/configurations/ClassicConfig.py @@ -0,0 +1,66 @@ +# ClassicConfig.py + +from pawnshop.Pieces import * +from pawnshop.ChessVector import ChessVector +from pawnshop.Moves import * + +_colors = ("black", "white") +_black, _white = _colors +_classicPieces = { + Rook(_black): ChessVector((0, 0)), + Knight(_black): ChessVector((0, 1)), + Bishop(_black): ChessVector((0, 2)), + Queen(_black): ChessVector((0, 3)), + King(_black): ChessVector((0, 4)), + Bishop(_black): ChessVector((0, 5)), + Knight(_black): ChessVector((0, 6)), + Rook(_black): ChessVector((0, 7)), + Pawn(_black, "down"): ChessVector((1, 0)), + Pawn(_black, "down"): ChessVector((1, 1)), + Pawn(_black, "down"): ChessVector((1, 2)), + Pawn(_black, "down"): ChessVector((1, 3)), + Pawn(_black, "down"): ChessVector((1, 4)), + Pawn(_black, "down"): ChessVector((1, 5)), + Pawn(_black, "down"): ChessVector((1, 6)), + Pawn(_black, "down"): ChessVector((1, 7)), + + Rook(_white): ChessVector((7, 0)), + Knight(_white): ChessVector((7, 1)), + Bishop(_white): ChessVector((7, 2)), + Queen(_white): ChessVector((7, 3)), + King(_white): ChessVector((7, 4)), + Bishop(_white): ChessVector((7, 5)), + Knight(_white): ChessVector((7, 6)), + Rook(_white): ChessVector((7, 7)), + Pawn(_white, "up"): ChessVector((6, 0)), + Pawn(_white, "up"): ChessVector((6, 1)), + Pawn(_white, "up"): ChessVector((6, 2)), + Pawn(_white, "up"): ChessVector((6, 3)), + Pawn(_white, "up"): ChessVector((6, 4)), + Pawn(_white, "up"): ChessVector((6, 5)), + Pawn(_white, "up"): ChessVector((6, 6)), + Pawn(_white, "up"): ChessVector((6, 7)) +} + +for piece, vector in _classicPieces.items(): + piece.vector = vector + +_pieceDict = {color: [piece for piece in _classicPieces.keys() if piece.color == color] for color in _colors} +_moveDict = {color: [Standard, CastleK, CastleQ, EnPassant] for color in _colors} +_promoteToDict = {color: [Queen, Rook, Knight, Bishop] for color in _colors} +_promoteFromDict = {color: [Pawn] for color in _colors} +_promoteAtDict = {color: 8 for color in _colors} + +CONFIG = { + "rows": 8, + "cols": 8, + "pieces": _pieceDict, + "moves": _moveDict, + "promoteTo": _promoteToDict, + "promoteFrom": _promoteFromDict, + "promoteAt": _promoteAtDict, + "turnorder": ["white", "black"] +} + +if __name__ == "__main__": + print(CONFIG) diff --git a/pawnshop/configurations/DefaultConfig.JSON b/pawnshop/configurations/DefaultConfig.JSON new file mode 100644 index 0000000..4de45ae --- /dev/null +++ b/pawnshop/configurations/DefaultConfig.JSON @@ -0,0 +1,12 @@ +{ + "rows": 8, + "cols": 8, + "colors": [], + "disabled": [], + "pieces": {}, + "moves": {}, + "promoteTo": {}, + "promoteFrom": {}, + "promoteAt": {}, + "turnorder": [] +} diff --git a/pawnshop/configurations/FourPlayerConfig.py b/pawnshop/configurations/FourPlayerConfig.py new file mode 100644 index 0000000..56ab996 --- /dev/null +++ b/pawnshop/configurations/FourPlayerConfig.py @@ -0,0 +1,139 @@ +# FourPlayerConfig.py + +from pawnshop.Pieces import * +from pawnshop.Moves import * +from pawnshop.ChessVector import ChessVector + +_colors = ("yellow", "green", "red", "blue") +_yellow, _green, _red, _blue = _colors + +_fourPlayerPieces = { + Rook(_yellow): ChessVector((0, 3)), + Knight(_yellow): ChessVector((0, 4)), + Bishop(_yellow): ChessVector((0, 5)), + Queen(_yellow): ChessVector((0, 6)), + King(_yellow): ChessVector((0, 7)), + Bishop(_yellow): ChessVector((0, 8)), + Knight(_yellow): ChessVector((0, 9)), + Rook(_yellow): ChessVector((0, 10)), + Pawn(_yellow, "down"): ChessVector((1, 3)), + Pawn(_yellow, "down"): ChessVector((1, 4)), + Pawn(_yellow, "down"): ChessVector((1, 5)), + Pawn(_yellow, "down"): ChessVector((1, 6)), + Pawn(_yellow, "down"): ChessVector((1, 7)), + Pawn(_yellow, "down"): ChessVector((1, 8)), + Pawn(_yellow, "down"): ChessVector((1, 9)), + Pawn(_yellow, "down"): ChessVector((1, 10)), + + Rook(_green): ChessVector((3, 13)), + Knight(_green): ChessVector((4, 13)), + Bishop(_green): ChessVector((5, 13)), + Queen(_green): ChessVector((6, 13)), + King(_green): ChessVector((7, 13)), + Bishop(_green): ChessVector((8, 13)), + Knight(_green): ChessVector((9, 13)), + Rook(_green): ChessVector((10, 13)), + Pawn(_green, "left"): ChessVector((3, 12)), + Pawn(_green, "left"): ChessVector((4, 12)), + Pawn(_green, "left"): ChessVector((5, 12)), + Pawn(_green, "left"): ChessVector((6, 12)), + Pawn(_green, "left"): ChessVector((7, 12)), + Pawn(_green, "left"): ChessVector((8, 12)), + Pawn(_green, "left"): ChessVector((9, 12)), + Pawn(_green, "left"): ChessVector((10, 12)), + + Rook(_red): ChessVector((13, 3)), + Knight(_red): ChessVector((13, 4)), + Bishop(_red): ChessVector((13, 5)), + Queen(_red): ChessVector((13, 6)), + King(_red): ChessVector((13, 7)), + Bishop(_red): ChessVector((13, 8)), + Knight(_red): ChessVector((13, 9)), + Rook(_red): ChessVector((13, 10)), + Pawn(_red, "up"): ChessVector((12, 3)), + Pawn(_red, "up"): ChessVector((12, 4)), + Pawn(_red, "up"): ChessVector((12, 5)), + Pawn(_red, "up"): ChessVector((12, 6)), + Pawn(_red, "up"): ChessVector((12, 7)), + Pawn(_red, "up"): ChessVector((12, 8)), + Pawn(_red, "up"): ChessVector((12, 9)), + Pawn(_red, "up"): ChessVector((12, 10)), + + Rook(_blue): ChessVector((3, 0)), + Knight(_blue): ChessVector((4, 0)), + Bishop(_blue): ChessVector((5, 0)), + Queen(_blue): ChessVector((6, 0)), + King(_blue): ChessVector((7, 0)), + Bishop(_blue): ChessVector((8, 0)), + Knight(_blue): ChessVector((9, 0)), + Rook(_blue): ChessVector((10, 0)), + Pawn(_blue, "right"): ChessVector((3, 1)), + Pawn(_blue, "right"): ChessVector((4, 1)), + Pawn(_blue, "right"): ChessVector((5, 1)), + Pawn(_blue, "right"): ChessVector((6, 1)), + Pawn(_blue, "right"): ChessVector((7, 1)), + Pawn(_blue, "right"): ChessVector((8, 1)), + Pawn(_blue, "right"): ChessVector((9, 1)), + Pawn(_blue, "right"): ChessVector((10, 1)) +} +_disabled = [ + ChessVector((0, 0)), + ChessVector((0, 1)), + ChessVector((0, 2)), + ChessVector((1, 0)), + ChessVector((1, 1)), + ChessVector((1, 2)), + ChessVector((2, 0)), + ChessVector((2, 1)), + ChessVector((2, 2)), + ChessVector((0, 11)), + ChessVector((0, 12)), + ChessVector((0, 13)), + ChessVector((1, 11)), + ChessVector((1, 12)), + ChessVector((1, 13)), + ChessVector((2, 11)), + ChessVector((2, 12)), + ChessVector((2, 13)), + ChessVector((11, 0)), + ChessVector((11, 1)), + ChessVector((11, 2)), + ChessVector((12, 0)), + ChessVector((12, 1)), + ChessVector((12, 2)), + ChessVector((13, 0)), + ChessVector((13, 1)), + ChessVector((13, 2)), + ChessVector((11, 11)), + ChessVector((11, 12)), + ChessVector((11, 13)), + ChessVector((12, 11)), + ChessVector((12, 12)), + ChessVector((12, 13)), + ChessVector((13, 11)), + ChessVector((13, 12)), + ChessVector((13, 13)) +] +for piece, vector in _fourPlayerPieces.items(): + piece.vector = vector + +_pieceDict = {color: [piece for piece in _fourPlayerPieces.keys() if piece.color == color] for color in _colors} +_moveDict = {color: [Standard, CastleK, CastleQ] for color in _colors} +_promoteToDict = {color: [Queen, Rook, Knight, Bishop] for color in _colors} +_promoteFromDict = {color: [Pawn] for color in _colors} +_promoteAtDict = {color: 8 for color in _colors} + +CONFIG = { + "rows": 14, + "cols": 14, + "pieces": _pieceDict, + "moves": _moveDict, + "promoteTo": _promoteToDict, + "promoteFrom": _promoteFromDict, + "promoteAt": _promoteAtDict, + "disabled": _disabled, + "turnorder": ["red", "blue", "yellow", "green"] +} + +if __name__ == "__main__": + print(CONFIG) diff --git a/pawnshop/configurations/__init__.py b/pawnshop/configurations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pawnshop/configurations/__pycache__/ClassicConfig.cpython-39.pyc b/pawnshop/configurations/__pycache__/ClassicConfig.cpython-39.pyc new file mode 100644 index 0000000..83d8cfa Binary files /dev/null and b/pawnshop/configurations/__pycache__/ClassicConfig.cpython-39.pyc differ diff --git a/pawnshop/configurations/__pycache__/FourPlayerConfig.cpython-39.pyc b/pawnshop/configurations/__pycache__/FourPlayerConfig.cpython-39.pyc new file mode 100644 index 0000000..75d943b Binary files /dev/null and b/pawnshop/configurations/__pycache__/FourPlayerConfig.cpython-39.pyc differ diff --git a/pawnshop/configurations/__pycache__/__init__.cpython-39.pyc b/pawnshop/configurations/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..a2dfc98 Binary files /dev/null and b/pawnshop/configurations/__pycache__/__init__.cpython-39.pyc differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..462691e --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setuptools.setup( + name="pawnshop", # Replace with your own username + version="1.0.3", + author="Nils Forssén", + author_email="forssennils@gmail.com", + description="A simple chess library as hobby project.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/NilsForssen/pawnshop", + packages=setuptools.find_packages(), + package_data={ + "pawnshop": ["configurations\\*.JSON"] + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', + extras_requires={ + "dev": [ + "pytest>=3.7", + ], + }, +) diff --git a/tests/__pycache__/test_1.cpython-39-pytest-6.2.2.pyc b/tests/__pycache__/test_1.cpython-39-pytest-6.2.2.pyc new file mode 100644 index 0000000..d07f0c2 Binary files /dev/null and b/tests/__pycache__/test_1.cpython-39-pytest-6.2.2.pyc differ diff --git a/tests/__pycache__/test_1.cpython-39.pyc b/tests/__pycache__/test_1.cpython-39.pyc new file mode 100644 index 0000000..b44f75d Binary files /dev/null and b/tests/__pycache__/test_1.cpython-39.pyc differ diff --git a/tests/__pycache__/test_2.cpython-39-pytest-6.2.2.pyc b/tests/__pycache__/test_2.cpython-39-pytest-6.2.2.pyc new file mode 100644 index 0000000..f187dd3 Binary files /dev/null and b/tests/__pycache__/test_2.cpython-39-pytest-6.2.2.pyc differ diff --git a/tests/__pycache__/test_2.cpython-39.pyc b/tests/__pycache__/test_2.cpython-39.pyc new file mode 100644 index 0000000..3b33a5c Binary files /dev/null and b/tests/__pycache__/test_2.cpython-39.pyc differ diff --git a/tests/test_1.py b/tests/test_1.py new file mode 100644 index 0000000..c324f48 --- /dev/null +++ b/tests/test_1.py @@ -0,0 +1,128 @@ +# test_1.py + +from pawnshop.ChessVector import ChessVector +from pawnshop.ChessBoard import initClassic +from pawnshop.Exceptions import IllegalMove, CheckMate +from pawnshop.GameNotations import board2FEN +from pawnshop.Pieces import Queen + + +board = initClassic() + + +class UnsuccessfulTest(Exception): + pass + + +def move(start, target, **kwargs): + board.movePiece(ChessVector(start, board), ChessVector(target, board), printout=False, **kwargs) + + +def test_moves(): + movelist = [ + ("a2", "a3"), + ("e7", "e5"), + ("b1", "c3"), + ("d7", "d5"), + ("c3", "d5"), + ("d8", "h4") + ] + for m in movelist: + move(*m) + + try: + move("f2", "f4") + raise UnsuccessfulTest + except IllegalMove: + pass + + move("d5", "c7") + + try: + move("h4", "f2") + raise UnsuccessfulTest + except IllegalMove: + pass + + try: + move("b7", "b6") + raise UnsuccessfulTest + except IllegalMove: + pass + + movelist = [ + ("e8", "d8"), + ("c7", "a8"), + ("f8", "c5"), + ("a8", "b6") + ] + + for m in movelist: + move(*m) + + try: + move("b7", "b6") + raise UnsuccessfulTest + except IllegalMove: + pass + + move("a7", "b6") + + move("h4", "f2", ignoreOrder=True) + + try: + move("e1", "f2", ignoreOrder=True) + raise UnsuccessfulTest + except CheckMate: + pass + + move("d2", "d4", ignoreCheck=True, ignoreOrder=True, ignoreMate=True) + move("d1", "d3", ignoreCheck=True) + move("c1", "e3", ignoreCheck=True, ignoreOrder=True) + + try: + move("e1", "a1") + raise UnsuccessfulTest + except IllegalMove: + pass + + move("f2", "f5", ignoreOrder=True) + move("f5", "d3", ignoreOrder=True) + + try: + move("e1", "a1") + raise UnsuccessfulTest + except IllegalMove: + pass + + move("d3", "f5", ignoreOrder=True) + move("e1", "c1", ignoreOrder=True) + move("d4", "e5") + move("g8", "h6", ignoreCheck=True) + + try: + move("d8", "h8", ignoreCheck=True, ignoreOrder=True) + raise UnsuccessfulTest + except IllegalMove: + pass + + try: + move("d8", "d7", ignoreOrder=True) + raise UnsuccessfulTest + except IllegalMove: + pass + + move("d8", "e7", ignoreOrder=True) + move("f5", "h5") + move("f7", "f5", ignoreOrder=True) + move("e5", "f6", ignoreOrder=True) + move("e7", "e8", ignoreOrder=True) + move("f6", "f7", ignoreOrder=True) + move("f7", "f8", promote=Queen) + move("e8", "f8") + + print(board) + + +def test_FEN(): + assert board2FEN(board) == "1nb2k1r/1p4pp/1p5n/2b4q/8/P3B3/1PP1P1PP/2KR1BNR" diff --git a/tests/test_2.py b/tests/test_2.py new file mode 100644 index 0000000..cf64b74 --- /dev/null +++ b/tests/test_2.py @@ -0,0 +1,29 @@ +# test_2.py + +from pawnshop.ChessBoard import init4P +from pawnshop.ChessVector import ChessVector + +board = init4P() + +def move(start, target, **kwargs): + board.movePiece(ChessVector(start, board), ChessVector(target, board), **kwargs) + +def test_moves(): + move("f2", "f3") + + move("g2", "g3", ignoreOrder=True) + + move("g1", "g2", ignoreOrder=True) + + move("b6", "c6", ignoreOrder=True) + + move("m7", "l7", ignoreOrder=True) + + move("n7", "m7", ignoreOrder=True) + + move("h2", "h3", ignoreOrder=True) + + move("g2", "g1", ignoreOrder=True) + + move("b5", "c5", ignoreOrder=True) +