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!