initial main
authorNils Forssén <forssennils@gmail.com>
Thu, 18 Sep 2025 00:00:02 +0000 (02:00 +0200)
committerNils Forssén <forssennils@gmail.com>
Thu, 18 Sep 2025 00:00:02 +0000 (02:00 +0200)
62 files changed:
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
build/lib/pawnshop/ChessBoard.py [new file with mode: 0644]
build/lib/pawnshop/ChessVector.py [new file with mode: 0644]
build/lib/pawnshop/Exceptions.py [new file with mode: 0644]
build/lib/pawnshop/GameNotations.py [new file with mode: 0644]
build/lib/pawnshop/Moves.py [new file with mode: 0644]
build/lib/pawnshop/Pieces.py [new file with mode: 0644]
build/lib/pawnshop/Utils.py [new file with mode: 0644]
build/lib/pawnshop/__init__.py [new file with mode: 0644]
build/lib/pawnshop/configurations/ClassicConfig.py [new file with mode: 0644]
build/lib/pawnshop/configurations/DefaultConfig.JSON [new file with mode: 0644]
build/lib/pawnshop/configurations/FourPlayerConfig.py [new file with mode: 0644]
build/lib/pawnshop/configurations/__init__.py [new file with mode: 0644]
config.pypirc [new file with mode: 0644]
dist/pawnshop-1.0.3-py3-none-any.whl [new file with mode: 0644]
dist/pawnshop-1.0.3.tar.gz [new file with mode: 0644]
pawnshop.egg-info/PKG-INFO [new file with mode: 0644]
pawnshop.egg-info/SOURCES.txt [new file with mode: 0644]
pawnshop.egg-info/dependency_links.txt [new file with mode: 0644]
pawnshop.egg-info/top_level.txt [new file with mode: 0644]
pawnshop/ChessBoard.py [new file with mode: 0644]
pawnshop/ChessVector.py [new file with mode: 0644]
pawnshop/Exceptions.py [new file with mode: 0644]
pawnshop/GameNotations.py [new file with mode: 0644]
pawnshop/Moves.py [new file with mode: 0644]
pawnshop/Pieces.py [new file with mode: 0644]
pawnshop/Utils.py [new file with mode: 0644]
pawnshop/__init__.py [new file with mode: 0644]
pawnshop/__pycache__/ChessBoard.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/ChessBoard.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/ChessVector.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/ChessVector.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/ClassicConfig.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/ClassicConfig.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/Exceptions.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/Exceptions.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/FourPlayerConfig.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/FourPlayerConfig.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/GameNotations.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/Moves.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/Moves.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/Pieces.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/Pieces.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/Utils.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/Utils.cpython-39.pyc [new file with mode: 0644]
pawnshop/__pycache__/__init__.cpython-37.pyc [new file with mode: 0644]
pawnshop/__pycache__/__init__.cpython-39.pyc [new file with mode: 0644]
pawnshop/configurations/ClassicConfig.py [new file with mode: 0644]
pawnshop/configurations/DefaultConfig.JSON [new file with mode: 0644]
pawnshop/configurations/FourPlayerConfig.py [new file with mode: 0644]
pawnshop/configurations/__init__.py [new file with mode: 0644]
pawnshop/configurations/__pycache__/ClassicConfig.cpython-39.pyc [new file with mode: 0644]
pawnshop/configurations/__pycache__/FourPlayerConfig.cpython-39.pyc [new file with mode: 0644]
pawnshop/configurations/__pycache__/__init__.cpython-39.pyc [new file with mode: 0644]
setup.py [new file with mode: 0644]
tests/__pycache__/test_1.cpython-39-pytest-6.2.2.pyc [new file with mode: 0644]
tests/__pycache__/test_1.cpython-39.pyc [new file with mode: 0644]
tests/__pycache__/test_2.cpython-39-pytest-6.2.2.pyc [new file with mode: 0644]
tests/__pycache__/test_2.cpython-39.pyc [new file with mode: 0644]
tests/test_1.py [new file with mode: 0644]
tests/test_2.py [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..623c1ae
Binary files /dev/null and b/LICENSE differ
diff --git a/README.md b/README.md
new file mode 100644 (file)
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 (file)
index 0000000..b945643
--- /dev/null
@@ -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 (file)
index 0000000..8aee5bc
--- /dev/null
@@ -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 (file)
index 0000000..5b1144d
--- /dev/null
@@ -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 (file)
index 0000000..4c011e5
--- /dev/null
@@ -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*(?P<castleQ>O-O-O)|(?P<castleK>O-O)|(?P<piece>[A-Z]*)(?P<pcol>[a-h]?)(?P<capture>[x]?)(?P<col>[a-h]+)(?P<rank>\d+)=?(?P<promote>[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 (file)
index 0000000..0784822
--- /dev/null
@@ -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 (file)
index 0000000..fe9b84c
--- /dev/null
@@ -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 (file)
index 0000000..0eb9da3
--- /dev/null
@@ -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 (file)
index 0000000..f27784b
--- /dev/null
@@ -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 (file)
index 0000000..039cf47
--- /dev/null
@@ -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 (file)
index 0000000..4de45ae
--- /dev/null
@@ -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 (file)
index 0000000..56ab996
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/config.pypirc b/config.pypirc
new file mode 100644 (file)
index 0000000..787305b
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..cc6a754
--- /dev/null
@@ -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 (file)
index 0000000..2e512d2
--- /dev/null
@@ -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 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/pawnshop.egg-info/top_level.txt b/pawnshop.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..de2b74e
--- /dev/null
@@ -0,0 +1 @@
+pawnshop
diff --git a/pawnshop/ChessBoard.py b/pawnshop/ChessBoard.py
new file mode 100644 (file)
index 0000000..b945643
--- /dev/null
@@ -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 (file)
index 0000000..8aee5bc
--- /dev/null
@@ -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 (file)
index 0000000..5b1144d
--- /dev/null
@@ -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 (file)
index 0000000..4c011e5
--- /dev/null
@@ -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*(?P<castleQ>O-O-O)|(?P<castleK>O-O)|(?P<piece>[A-Z]*)(?P<pcol>[a-h]?)(?P<capture>[x]?)(?P<col>[a-h]+)(?P<rank>\d+)=?(?P<promote>[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 (file)
index 0000000..0784822
--- /dev/null
@@ -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 (file)
index 0000000..fe9b84c
--- /dev/null
@@ -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 (file)
index 0000000..0eb9da3
--- /dev/null
@@ -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 (file)
index 0000000..f27784b
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..039cf47
--- /dev/null
@@ -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 (file)
index 0000000..4de45ae
--- /dev/null
@@ -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 (file)
index 0000000..56ab996
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..c324f48
--- /dev/null
@@ -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 (file)
index 0000000..cf64b74
--- /dev/null
@@ -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)
+