1
\$\begingroup\$

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
```
asked Sep 28, 2021 at 4:11
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.