Almost a month ago I asked for some review of my first attempt at Chess in Python, with the main goal being to start learning inheritance in Python. It can be found here: First attempt at chess in Python
I finally have had enough time to make a bunch of the suggested changes and have since implemented changes in my code as well as a few new features. Some changes I have made include:
- En Passant
- King's can't move into Check
- Attempting to remove magic non-constant inline string variables
- Moving some piece logic into better positions
- Replacing "check collisions" per piece to having each piece contain a list "valid moves" (the idea being it will make it significantly easier to implement full king check logic in the future)
- Other smaller feedback that can be found in the previous post
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
Any further feedback would be greatly appreciated.
main.py
from board import Board
from pieces.piece import invalid_move
LETTERS = "ABCDEFGH"
NUMBERS = "12345678"
WHITE = "white"
BLACK = "black"
COLOURS = (BLACK, WHITE)
EXTRA_TO = " to"
def get_input(board: Board, turn: int, extra: str = ""):
while True:
coord = input(f"What position would you like to move{extra}? (eg. A2)\n")
if len(coord) == 2:
if coord[0].upper() in LETTERS and coord[1] in NUMBERS:
if extra != EXTRA_TO:
if board.get_team(coord.upper()) not in COLOURS:
print("You can't move an empty square!")
continue
elif board.get_team(coord.upper()) != COLOURS[turn]:
print("You can only move your own piece!")
continue
return coord.upper()
print("Please write the coordinate in the form A2 (Letter)(Number)")
if __name__ == "__main__":
board = Board()
turn = 1
while True:
board.display()
try:
start = get_input(board, turn)
end = get_input(board, turn, EXTRA_TO)
board.move_piece(start, end)
turn = not turn
except invalid_move:
pass
board.py
import pieces
import numpy as np
ROOK = "R"
KNIGHT = "N"
BISHOP = "B"
QUEEN = "Q"
BLACK = "black"
WHITE = "white"
COLOURS = (BLACK, WHITE)
class Board():
def __init__(self):
self.board = np.full((8, 8), pieces.Piece("", -1, -1), dtype=pieces.Piece)
self.valid_moves = {BLACK: [], WHITE: []}
self.kings = {BLACK: None, WHITE: None}
self.__setBoard()
self.__update_valid_moves()
def __setBoard(self):
""""Initialise the board by creating and placing all pieces for both colours"""
for i in range(8):
self.board[1][i] = pieces.Pawn(BLACK, i, 1)
self.board[6][i] = pieces.Pawn(WHITE, i, 6)
for j, colour in enumerate(COLOURS):
pos = j * 7
for i in (0, 7):
self.board[pos][i] = pieces.Rook(colour, i, pos)
for i in (1, 6):
self.board[pos][i] = pieces.Knight(colour, i, pos)
for i in (2, 5):
self.board[pos][i] = pieces.Bishop(colour, i, pos)
self.board[pos][3] = pieces.Queen(colour, 3, pos)
self.kings[colour] = pieces.King(colour, 4, pos)
self.board[pos][4] = self.kings[colour]
for i, row in enumerate(self.board):
for j, square in enumerate(row):
if not square.initialised:
self.board[i][j] = pieces.Piece("", j, i)
def __convert_to_coords(self, position: str) -> pieces.Coordinate:
"""Convert coordinates from the Chess Syntax (ie. A2) to usable array coordinates"""
letters = "ABCDEFGH"
return pieces.Coordinate(letters.find(position[0]), abs(int(position[1]) - 8))
def __convert_to_alpha_coords(self, coord: pieces.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 __choosePromotion(self) -> pieces.Piece:
"""Logic for choosing what to promote a pawn to"""
while True:
choice = input(f"What would you like to promote the Pawn to? ({ROOK}, {KNIGHT}, {BISHOP}, {QUEEN}) ").upper()
if choice == ROOK:
return pieces.Rook
elif choice == KNIGHT:
return pieces.Knight
elif choice == BISHOP:
return pieces.Bishop
elif choice == QUEEN:
return pieces.Queen
def __update_valid_moves(self):
"""Updates the dictionary containing all valid moves of both sides"""
all_valid_moves = {BLACK: [], WHITE: []}
for row in self.board:
for square in row:
if square.team in COLOURS:
square.set_valid_moves(self.board)
all_valid_moves[square.team] += square.valid_moves
self.valid_moves = all_valid_moves
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 get_team(self, pos: str):
"""Returns the team of a piece at a given position (in the form A2)"""
coord = self.__convert_to_coords(pos)
return self.board[coord.y][coord.x].team
def move_piece(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.__convert_to_coords(start)
end = self.__convert_to_coords(end)
piece = self.board[start.y][start.x]
try:
opponent_team = [team for team in COLOURS if team != piece.team][0]
opponent_moves = self.valid_moves[opponent_team]
piece.move(end, self.__convert_to_alpha_coords, opponent_moves)
if isinstance(self.board[end.y][end.x], pieces.King):
print(f"{piece.team} wins!")
exit()
if isinstance(piece, pieces.Pawn):
if end.y == 7 or end.y == 0:
piece = piece.promote(self.__choosePromotion())
if end.x != start.x and self.board[end.y][end.x].initialised is False:
self.board[start.y][end.x] = pieces.Piece("", start.x, start.y)
print("En Passant!!")
self.board[end.y][end.x] = piece
self.board[start.y][start.x] = pieces.Piece("", start.x, start.y)
self.__update_valid_moves()
except pieces.invalid_move as e:
print(e.message)
raise pieces.invalid_move()
__init__.py (hasn't changed)
"""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
from typing import NamedTuple
class Coordinate(NamedTuple):
x: int
y: int
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
self.valid_moves = []
def __str__(self) -> str:
return " "
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
return []
def move(self, coord: Coordinate, convert_to_alpha_coords, other_team_valid_moves=[]):
if coord not in self.valid_moves:
block = convert_to_alpha_coords(coord)
raise invalid_move(f"{block} is an invalid move!")
self.x = coord.x
self.y = coord.y
self.moved = True
class invalid_move(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 Coordinate
WHITE = "white"
class Bishop(Piece):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Piece.__init__(self, team, x, y, initisalised)
def __str__(self) -> str:
if self.team == WHITE:
return "wB"
return "bB"
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
valid = super().update_valid_moves(board=board)
directions = [(1, 1), (-1, 1), (1, -1), (-1, -1)]
complete_directions = []
for i in range(1, 8):
for direction in directions:
if direction not in complete_directions:
x_val = self.x + i * direction[0]
y_val = self.y + i * direction[1]
if 0 <= x_val <= 7 and 0 <= y_val <= 7:
if board[y_val][x_val].initialised:
if board[y_val][x_val].team != self.team:
valid.append(Coordinate(x_val, y_val))
complete_directions.append(direction)
continue
valid.append(Coordinate(x_val, y_val))
else:
complete_directions.append(direction)
return valid
king.py
from .piece import Piece, invalid_move
from .piece import Coordinate
WHITE = "white"
class King(Piece):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Piece.__init__(self, team, x, y, initisalised)
def __str__(self) -> str:
if self.team == WHITE:
return "wK"
return "bK"
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def move(self, coord: Coordinate, convert_to_alpha_coords, other_team_valid_moves):
if coord in other_team_valid_moves:
raise invalid_move("The King cannot move into check")
super().move(coord, convert_to_alpha_coords, [])
def update_valid_moves(self, board) -> list:
valid = super().update_valid_moves(board=board)
directions = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (1, -1), (-1, -1)]
for direction in directions:
x_val = self.x + direction[0]
y_val = self.y + direction[1]
if 0 <= x_val <= 7 and 0 <= y_val <= 7:
if not board[y_val][x_val].initialised or (board[y_val][x_val].initialised and board[y_val][x_val].team != self.team):
valid.append(Coordinate(x_val, y_val))
return valid
knight.py
from .piece import Piece
from .piece import Coordinate
from itertools import product
WHITE = "white"
class Knight(Piece):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Piece.__init__(self, team, x, y, initisalised)
def __str__(self) -> str:
if self.team == WHITE:
return "wN"
return "bN"
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
# Simple Knight logic found here: https://stackoverflow.com/a/19372692/12115915
# Much nicer looking that my alternative if elif... elif else but functionally the same
moves = list(product([self.x - 1, self.x + 1], [self.y - 2, self.y + 2])) + list(product([self.x - 2, self.x + 2], [self.y - 1, self.y + 1]))
valid_moves = [Coordinate(x, y) for x, y in moves if x >= 0 and y >= 0 and x < 8 and y < 8 and board[y][x].team != self.team]
return valid_moves
pawn.py
from .piece import Piece
from .piece import Coordinate
WHITE = "white"
class Pawn(Piece):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Piece.__init__(self, team, x, y, initisalised)
self.last_move = Coordinate(self.x, self.y)
def __str__(self) -> str:
if self.team == WHITE:
return "wP"
return "bP"
def move(self, coord: Coordinate, convert_to_alpha_coords, other_team_valid_moves=[]):
self.last_move = Coordinate(self.x, self.y)
super().move(coord, convert_to_alpha_coords)
def promote(self, piece: Piece) -> Piece:
return piece(self.team, self.x, self.y)
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
valid = super().update_valid_moves(board)
vertical_direction = 1
if self.team == WHITE:
vertical_direction = -1
y_val = self.y + vertical_direction
y_movements = [y_val]
if not self.moved:
y_movements.append(y_val + vertical_direction)
for y in y_movements:
if 0 <= y <= 7:
for i in (-1, 1):
xval = self.x + i
if 0 <= xval <= 7:
if board[y][xval].initialised and board[y][xval].team != self.team:
valid.append(Coordinate(xval, y))
# En Passant logic
if 3 <= self.y <= 4 and board[self.y][xval].team != self.team:
if isinstance(board[self.y][xval], Pawn):
if board[self.y][xval].last_move.y == self.y + vertical_direction * 2:
valid.append(Coordinate(xval, y))
if not board[y][self.x].initialised:
valid.append(Coordinate(self.x, y))
else:
break
return valid
queen.py
from .rook import Rook
from .bishop import Bishop
WHITE = "white"
class Queen(Bishop, Rook):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Bishop.__init__(self, team, x, y, initisalised)
def __str__(self) -> str:
if self.team == WHITE:
return "wQ"
return "bQ"
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
valid = super(Queen, self).update_valid_moves(board)
return valid
rook.py
from .piece import Piece
from .piece import Coordinate
WHITE = "white"
class Rook(Piece):
def __init__(self, team: str, x: int, y: int, initisalised: bool = True):
Piece.__init__(self, team, x, y, initisalised)
def __str__(self) -> str:
if self.team == WHITE:
return "wR"
return "bR"
def set_valid_moves(self, board):
self.valid_moves = self.update_valid_moves(board)
def update_valid_moves(self, board) -> list:
valid = super().update_valid_moves(board)
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
complete_directions = []
for i in range(1, 8):
for direction in directions:
if direction not in complete_directions:
x_val = self.x + i * direction[0]
y_val = self.y + i * direction[1]
if 0 <= x_val <= 7 and 0 <= y_val <= 7:
if board[y_val][x_val].initialised:
if board[y_val][x_val].team != self.team:
valid.append(Coordinate(x_val, y_val))
complete_directions.append(direction)
continue
valid.append(Coordinate(x_val, y_val))
else:
complete_directions.append(direction)
return valid
```