15
\$\begingroup\$

Since I have barely used inheritance in Python, I wanted to make something that would make use of it so I have attempted to create a game of chess, where each piece uses inheritance. I also did this just for fun.

I have not added anything related to Checks, Turns (you can move any piece of any colour), game over conditions, en passant or even checking if the square you selected to move is a valid piece (it will just throw invalid move and do nothing instead). Currently it is just all of the pieces, validating moves, pawn promotion and taking other pieces.

The code is currently working quite well and I plan to add what it is missing at a later stage, but I thought it would be a good idea to get it looked over now, just in case I am doing something completely wrong that will cause more issues later.

The folder structure is as follows (if you want to run it yourself):

+ main.py
+ board.py
/ Pieces
    + __init__.py
    + bishop.py
    + king.py
    + knight.py
    + pawn.py
    + piece.py
    + queen.py
    + rook.py

main.py

from board import Board


LETTERS = "ABCDEFGH"
NUMBERS = "12345678"

if __name__ == "__main__":
    board = Board()
    print(board.board[8:0, 7:9])

    while True:
        board.display()
        while True:
            start = input("What piece would you like to move? (eg. A2)\n")
            if len(start) == 2:
                if start[0].upper() in LETTERS and start[1] in NUMBERS:
                    break
            print("Please write the coordinate in the form A2 (Letter)(Number)")
        while True:
            end = input("What square would you like to move to? (eg. A3)\n")
            if len(end) == 2:
                if end[0].upper() in LETTERS and end[1] in NUMBERS:
                    break
            print("Please write the coordinate in the form A2 (Letter)(Number)")
        board.movePiece(start.upper(), end.upper())

board.py

from typing_extensions import TypedDict
import Pieces as p
import numpy as np


class Coordinate(TypedDict):
    x: int
    y: int


class Board():
    def __init__(self):
        self.board = np.full((8, 8), p.Piece("", -1, -1), dtype=p.Piece)
        self.setBoard()

    def setBoard(self):
        """"Initialise the board by creating and placing all pieces for both colours"""
        colours = ["black", "white"]
        for i in range(8):
            self.board[1][i] = p.Pawn("black", i, 1)
            self.board[6][i] = p.Pawn("white", i, 6)
        for j in (0, 1):
            pos = j * 7
            for i in (0, 7):
                self.board[pos][i] = p.Rook(colours[j], i, pos)
            for i in (1, 6):
                self.board[pos][i] = p.Knight(colours[j], i, pos)
            for i in (2, 5):
                self.board[pos][i] = p.Bishop(colours[j], i, pos)
            self.board[pos][3] = p.Queen(colours[j], 3, pos)
            self.board[pos][4] = p.King(colours[j], 4, pos)

    def display(self):
        """Print the board with borders"""
        print("-" * len(self.board) * 3)
        for i in range(len(self.board)):
            print('|' + '|'.join(map(str, self.board[i])) + '|')
            print("-" * len(self.board) * 3 + '-')

    def convertToCoords(self, position: str) -> Coordinate:
        """Convert coordinates from the Chess Syntax (ie. A2) to usable array coordinates"""
        letters = "ABCDEFGH"
        return {"y": abs(int(position[1]) - 8), "x": letters.find(position[0])}

    def convertToAlphaCoords(self, coord: Coordinate) -> str:
        """Convert coordinates from usable array coordinates to the Chess Syntax coordinates (ie. A2)"""
        letters = "ABCDEFGH"
        return letters[coord["x"]] + str(abs(coord["y"] - 8))

    def checkCollisions(self, piece: p.Piece, victim: p.Piece, start: dict, end: dict):
        """Check whether a move will collide with a piece.
           This function only applies to Rooks, Bishops and Queens."""
        minY = min(start["y"], end["y"])
        maxY = max(start["y"], end["y"])
        minX = min(start["x"], end["x"])
        maxX = max(start["x"], end["x"])
        rookMove = False
        if isinstance(piece, p.Rook) or isinstance(piece, p.Queen):
            if start["x"] == end["x"]:
                claim = self.board[minY:maxY, start["x"]]
                rookMove = True
            elif start["y"] == end["y"]:
                claim = self.board[start["y"], minX:maxX]
                rookMove = True
            if rookMove:
                for i in claim:
                    if i != piece and i != victim:
                        if i.initialised:
                            block = self.convertToAlphaCoords({"x": i.x, "y": i.y})
                            raise p.InvalidMove(f"This move is blocked by a piece at {block}")

        if isinstance(piece, p.Bishop) or isinstance(piece, p.Queen):
            claim = []
            for i in range(minX, maxX):
                for j in range(minY, maxY):
                    if abs(i - start["x"]) == abs(j - start["y"]):
                        claim.append(self.board[j][i])

            for i in claim:
                if i != piece and i != victim:
                    if i.initialised:
                        block = self.convertToAlphaCoords({"x": i.x, "y": i.y})
                        raise p.InvalidMove(f"This move is blocked by a piece at {block}")

    def movePiece(self, start: str, end: str):
        """Move a piece. It takes a starting position and an ending position,
           checks if the move is valid and then moves the piece"""
        start = self.convertToCoords(start)
        end = self.convertToCoords(end)
        piece = self.board[start["y"]][start["x"]]
        victim = self.board[end["y"]][end["x"]]
        try:
            piece.checkMove(end["x"], end["y"], victim)
            self.checkCollisions(piece, victim, start, end)
            piece.move(end)
            if isinstance(piece, p.Pawn):
                if end["y"] == 7 or end["y"] == 0:
                    piece = piece.promote()
            self.board[end["y"]][end["x"]] = piece
            self.board[start["y"]][start["x"]] = p.Piece("", -1, -1)
        except p.InvalidMove as e:
            print(e.message)

__init__.py

"""Taken from https://stackoverflow.com/a/49776782/12115915"""
import os
import sys

dir_path = os.path.dirname(os.path.abspath(__file__))
files_in_dir = [f[:-3] for f in os.listdir(dir_path)
                if f.endswith('.py') and f != '__init__.py']
for f in files_in_dir:
    mod = __import__('.'.join([__name__, f]), fromlist=[f])
    to_import = [getattr(mod, x) for x in dir(mod) if isinstance(getattr(mod, x), type)]  # if you need classes only

    for i in to_import:
        try:
            setattr(sys.modules[__name__], i.__name__, i)
        except AttributeError:
            pass

piece.py

class Piece():
    def __init__(self, team: str, x: int, y: int, initisalised: bool = False):
        self.team = team
        self.x = x
        self.y = y
        self.initialised = initisalised
        self.moved = False

    def __str__(self) -> str:
        return "  "
    
    def checkMove(self, x: int, y: int, other):
        raise InvalidMove("You cannot move an empty square")

    def move(self, coord: dict):
        self.x = coord["x"]
        self.y = coord["y"]
        self.moved = True


class InvalidMove(Exception):
    def __init__(self, message):
        self.message = message
        super().__init__(message)

    def __str__(self) -> str:
        return self.message

bishop.py

from .piece import Piece
from .piece import InvalidMove


class Bishop(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wB"
        return "bB"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Bishops cannot take their own pieces")

        if abs(self.x - x) != abs(self.y - y):
            raise InvalidMove("Bishops can only move diagonally")

king.py

from .piece import Piece
from .piece import InvalidMove


class King(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wK"
        return "bK"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Kings cannot take their own pieces")

        if abs(x - self.x) > 1 or abs(y - self.y) > 1:
            raise InvalidMove("Kings can only move one space in any direction")

knight.py

from .piece import Piece
from .piece import InvalidMove


class Knight(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wN"
        return "bN"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Knights cannot take their own pieces")

        minMove = min(abs(x - self.x), abs(y - self.y))
        maxMove = max(abs(x - self.x), abs(y - self.y))
        if minMove != 1 or maxMove != 2:
            raise InvalidMove("Knights can only move in an L shape (2 in one direction, 1 in another)")

pawn.py

from .piece import Piece
from .piece import InvalidMove
from .rook import Rook
from .bishop import Bishop
from .knight import Knight
from .queen import Queen


class Pawn(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wP"
        return "bP"

    def promote(self) -> Piece:
        self.promoted = True
        promotions = "RNBQ"
        while True:
            choice = input("What would you like to promote the Pawn to? (R, N, B, Q) ").upper()
            if choice in promotions:
                if choice == "R":
                    return Rook(self.team, self.x, self.y)
                elif choice == "N":
                    return Knight(self.team, self.x, self.y)
                elif choice == "B":
                    return Bishop(self.team, self.x, self.y)
                elif choice == "Q":
                    return Queen(self.team, self.x, self.y)

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Pawns cannot take their own pieces")
        
        moveDist = abs(y - self.y)
        if self.x != x:
            if moveDist != 1:
                raise InvalidMove("Pawns cannot move sideways unless taking another piece diagonally one space away")
            if not other.initialised:
                raise InvalidMove("Pawns cannot move diagonally into an empty space")
        else:
            if other.initialised:
                raise InvalidMove("Pawns cannot take pieces in front of them")
        
        if moveDist > 2:
            raise InvalidMove("Pawns can only move 1 space (or two if it is their first move)")

        if moveDist == 2:
            if self.moved:
                raise InvalidMove("Pawns can only move two spaces on their first move")
        
        if self.team == "white":
            if y > self.y:
                raise InvalidMove("White Pawns cannot move down")
        else:
            if y < self.y:
                raise InvalidMove("Black Pawns cannot move up")

queen.py

from .piece import Piece
from .piece import InvalidMove


class Queen(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wQ"
        return "bQ"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Queens cannot take their own pieces")

        if not ((self.x == x or self.y == y) or (abs(self.x - x) == abs(self.y - y))):
            raise InvalidMove("Queens can only move diagonally, horizontally or vertically any amount")

rook.py

from .piece import Piece
from .piece import InvalidMove


class Rook(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wR"
        return "bR"

    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove("Rooks cannot take their own pieces")

        if self.x != x and self.y != y:
            raise InvalidMove("Rooks can only move in one direction, not diagonally")

Any feedback would be greatly appreciated!

\$\endgroup\$
4
  • \$\begingroup\$ This is not about your code, but might be interesting for you. Maybe have a look at bitboards as an efficient and fast method for storing the board and calculating moves chessprogramming.org/Bitboards \$\endgroup\$
    – jjj
    Sep 3 at 18:24
  • \$\begingroup\$ @jjj I did consider using bitboards however I wanted to make this as a fun little project that implements class inheritance somewhat between the pieces. As far as I know, bitboards do not really use a form of class inheritance for the pieces so I didn't look much further into it. I may be wrong though, so please do correct me if I am. \$\endgroup\$
    – Brade
    Sep 8 at 7:38
  • \$\begingroup\$ I mainly wanted to point the the possibility of using them. You can combine classes and bitboards, but if all you want is a playable game it's probably now worth it (the human player is way slower anyway). I would recommend using bitboards when you implement a chess engine. \$\endgroup\$
    – jjj
    Sep 8 at 8:22
  • \$\begingroup\$ Oh I see. Well, if I eventually get around to making a chess engine, I will definitely look into bitboards more. Thanks! \$\endgroup\$
    – Brade
    Sep 8 at 23:05
5
\$\begingroup\$
  • Refractor the variable naming to fit Python's usual style: checkMove -> check_move (and similar for all methods and variables)
  • Package names should be lowercase. Pieces -> pieces
  • Further, variable names should be significative: import Pieces as p -> import Pieces
  • main.py is repeating a lot of logic which could be in functions.

    def ask_square():
        while True:
            square = input("What piece would you like to move? (eg. A2)\n")
            if len(start) == 2:
                if square[0].upper() in LETTERS and square[1] in NUMBERS:
                    return square
            print("Please write the coordinate in the form A2 (Letter)(Number)")
            
            
    if __name__ == "__main__":
        board = Board()
        print(board.board[8:0, 7:9])
    
        while True:
            board.display()
            start = ask_square()
            end = ask_square()
            board.movePiece(start.upper(), end.upper())

  • Consider using a named tuple instead of a full-blown class for Coordinate. more about it here
  • colours = ["black", "white"] is being used as a constant. Move it outside the method and as COLOURS = ('black', 'white') (proper naming, use of immutable tuple instead of list)
  • Use of enumerate? for j in (0, 1): -> for j, colour in enumerate(colours):
  • convertToCoords and similar methods are only used within the class Board. Why not make them private? Call them _convertToCoords (or following proper naming convention: _convert_to_coords.
  • Pass it through black or a similar code formatter to have it cleaned (black.vercel.app/).
  • Avoid use of inline magic values. Instead create constants. E.g., BLACK = 'black', WHITE = 'white', COLOURS = (BLACK, WHITE) and use them in places like:

    def __str__(self) -> str:
        if self.team == "white":
            return "wR"
        return "bR"

  • If all pieces can be promoted, why not move that to the Piece class to avoid code duplication?

    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False  # move this to super and remove in all pieces

  • Again, avoid the use of magic, non-constant, inline values.

    def promote(self) -> Piece:
        self.promoted = True
        promotions = "RNBQ"
        while True:
            choice = input("What would you like to promote the Pawn to? (R, N, B, Q) ").upper()
            if choice in promotions:
                if choice == "R":
                    return Rook(self.team, self.x, self.y)
                elif choice == "N":
                    return Knight(self.team, self.x, self.y)
                elif choice == "B":
                    return Bishop(self.team, self.x, self.y)
                elif choice == "Q":
                    return Queen(self.team, self.x, self.y)

  • On a more general scope, in regards to the previous code snippet as well, you are mixing the user control with the piece model. These "should" be two separate layers to ease decoupling and e.g. easy replacement to use CLI or a WEB or some UI. Further, this makes it more testable (the way your code is designed mixing user control with the model and logic makes it difficult to unit test). In other words, you should have
    def promote(self, piece: type(Piece)) -> Piece

e.g.

    queen = pawn.promote(Queen)

And have another class in charge of calling this method and asking input to the user.

  • There is surely more, but this is enough for one CR! Post again the cleaned code whenever you feel like it.
\$\endgroup\$
10
\$\begingroup\$

I think this looks pretty good. A few things I would recommend:

  1. parts of your checkCollision logic should be move into your piece subclasses. Usually when your logic includes isinstance checks, that's a good sign its in the wrong place and not OO.
  2. There's some duplication in the checkMove validation. No piece should be able to move on itself or take out its own team, so common validations should go in the base class.
  3. There's potential for the Queen to use multi-inheritance or expose some of the Bishop/Rook logic as a mixin so it can be reused by the Queen
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for that. With your first point, I was considering putting the checkCollision logic in each class but I didn't think passing the board through to each piece was a good idea but it does make more sense. With the checkMove validation, I couldn't figure out how to do something like that. Do you have a link to somewhere that I can read up on that while also keeping the default exception if it is called from the base piece class itself? Finally, I didn't even think about multi-inheritance but this does fall into the same thing as the checkMove about not understanding how. Thanks again! \$\endgroup\$
    – Brade
    Sep 3 at 3:53
4
\$\begingroup\$

I have one big piece of advice for you: learn to love super().

When writing classes in python, the super() function allows a child class to delegate part (or all) of a method call to a parent class*. For example, if I have classes Base and Derived, like so:

class Base:
    def greet_user(self):
        print('Hello! Nice to meet you.')

class Derived(Base):
    def greet_user(self):
        super().greet_user()
        print("Isn't the weather nice today?")

Then, if I instantiate an instance of Derived and call greet_user, you'll see the following output:

>>> d = Derived()
>>> d.greet_user()
Hello! Nice to meet you.
Isn't the weather nice today?

Derived.greet_user() has successfully delegated part of its method call to Base.greet_user().

With this in mind, we can significantly cut down on a lot of the repetitive code in your game at the moment. Your Piece class can be refactored to the following, incorporating the logic in the .checkMove() method that's common to all subclasses:

class Piece:
    def __init__(self, team: str, x: int, y: int, initisalised: bool = False) -> None:
        self.team = team
        self.x = x
        self.y = y
        self.initialised = initisalised
        self.moved = False

    def __str__(self) -> str:
        return "  "
    
    def checkMove(self, x: int, y: int, other):
        if self.x == x and self.y == y:
            raise InvalidMove("You cannot move to the same spot")

        if other.team == self.team:
            raise InvalidMove(
               f"{self.__class__.__name__}s cannot take their own pieces"
            )

    def move(self, coord: dict):
        self.x = coord["x"]
        self.y = coord["y"]
        self.moved = True

Now, your King class can be made much shorter:

class King(Piece):
    def __init__(self, team: str, x: int, y: int):
        Piece.__init__(self, team, x, y, True)
        self.promoted = False

    def __str__(self) -> str:
        if self.team == "white":
            return "wK"
        return "bK"

    def checkMove(self, x: int, y: int, other):
        super().checkMove(x, y, other)

        if abs(x - self.x) > 1 or abs(y - self.y) > 1:
            raise InvalidMove("Kings can only move one space in any direction")

And all your other classes that subclass Piece can be refactored in much the same way.


*It's important to note that when multiple inheritance is in play, it doesn't always delegate to a direct parent (it sometimes delegates to a sibling) — but that's beyond the scope of this answer.

\$\endgroup\$
2
\$\begingroup\$

You commonly would not have checkMove method in a chess program instead you would have getValidMoves for each piece and then base checkMove on getValidMoves. The getValidMoves would return a list of possible moves which can be used in a simple computer chess engine. Also the base class Piece is missing logic to allow subclasses to describe actual movement by directions or similar simple logic, consider adding a move class.

\$\endgroup\$
1
\$\begingroup\$
  1. Pieces do not need to store their own locations. You can send both the start and end squares to the checkMove() method. Let the Board class handle positions.
  2. For the coordinate class, use domain-specific names: file and rank.
  3. Don't break apart structures so often. In checkMoves(), don't send the x and y coordinates as separate arguments. Send a Coordinate instance. Keep structures together until you actually need the individual data members. This will make your code easier to read. Instead of piece.checkMove(end["x"], end["y"], victim), write piece.checkMove(end, victim). Or, with suggestion #1, piece.checkMove(start, end, victim).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.