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

```
\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.