11
\$\begingroup\$

I'm developing chess in python, and I've decided to make everything text-based before creating a GUI because it'd be my first time doing the latter. I haven't got around to the actual gameplay, but I've modeled the possible movement of any piece.

Note that I cannot implement En Passant and castling without turn data.

from time import sleep

class Config:
    DEMTIME = .8  # seconds
    types = {'min': 1, 'miniature': 3, 'small': 5, 'default': 8, 'extended': 11, 'large': 15, 'massive': 20, 'max': 26}
    letters = tuple('abcdefghijklmnopqrstuvwxyz')
    white_pieces = {'Pawn' : "♙", 'Rook' : "♖", 'Knight' : "♘", 'Bishop' : "♗", 'King' : "♔", 'Queen' : "♕" }
    black_pieces = {'Pawn' : "♟", 'Rook' : "♜", 'Knight' : "♞", 'Bishop' : "♝", 'King' : "♚", 'Queen' : "♛" }
    board = 'UNINITIALIZED'
    b_len = 'UNINITIALIZED'

    @classmethod
    def new_board(cls, btype='default'):
        def size(x):
            return [['___' for _ in range(x)] for _ in range(x)], x

        s = False

        if btype is not None:
            btype = btype.lower()

            if 'custom' in btype:
                btype = int(btype.replace('custom', '').strip())
                if 1 <= btype <= 26:
                    cls.board, cls.b_len = size(btype)
                    s = True
                else:
                    btype = None
                    cls.new_board(btype)
            elif btype in cls.types:
                cls.board, cls.b_len = size(cls.types[btype])
                s = True
            else:
                print(f'Unable to initialize board of unknown type {btype}')

        else:
            print('Unable to initalize board with a size lower than 1 or greater than 26')

        if s: cls.print_board()

    @classmethod
    def print_board(cls):
        if Config.b_len != 'UNINITIALIZED':
            def printl():
                if len(str(cls.b_len)) == 2:
                    print(' ', end='')
                for x in range(cls.b_len):
                    print(' '*6 + f'{cls.letters[x]}', end='')
                print('\n')

            print('\n'*2)
            printl()
            for x in range(cls.b_len):
                print(f'{cls.b_len-x:0{len(str(cls.b_len))}}  {cls.board[x]}  {cls.b_len-x:0{len(str(cls.b_len))}}\n')
            printl()
            print('\n'*4)

        else:
            print('Unable to print board of uninitialized type')

    @classmethod
    def tile_convert(cls, x, display_tile=False):
        if not display_tile:
            if isinstance(x, str):
                return cls.letters.index(x)
            else:
                return cls.letters[x]
        else:  # display_tile converts the letter in {letter}{number} to a number
            return cls.b_len - int(x)

    @classmethod
    def l_num_to_coord(cls, pos):
        return Config.b_len - int(pos[1]), int(Config.tile_convert(pos[0]))

    @classmethod
    def coord_to_tile(cls, x, y):
        return f'{Config.tile_convert(x)}{Config.tile_convert(y, True)}'

    @classmethod
    def c_convert(cls, color):
        if color == 'White':
            return 'b'
        if color == "Black":
            return 'w'

class ChessPiece:
    def __init__(self, pos, color, num, piece):
        self.x = int(Config.tile_convert(pos[0]))
        self.y = Config.b_len - int(pos[1])
        self.color = color
        self.piece = piece
        self.pieceid = num
        self.moves = 0
        self.captured = []
        self.erased = False
        self.set_id()
        self.create()
        Config.print_board()

    def __str__(self):
        return self.piece

    def __repr__(self):
        return self.pieceid

    def set_id(self):
        if self.__class__.__name__ != "Knight":
            self.pieceid = f'{self.piece[0]}{self.pieceid}'
        else:
            self.pieceid = f'N{self.pieceid}'

        if self.color is not None:
            if self.color.lower() in ('black', 'white', 'b', 'w'):
                self.pieceid = self.color.lower()[0] + self.pieceid
                if self.color.lower() == 'b':
                    self.color = 'Black'
                elif self.color.lower() == 'w':
                    self.color = 'White'
            else:
                self.color = None
                print("Invalid color input. Color not set.")
                self.set_id()
        else:
             self.pieceid = '_' + self.pieceid


    def create(self):
        if Config.board[self.y][self.x] != '___':
            po = Config.board[self.y][self.x]
            print(f'Piece {po} erased to make room for {self.pieceid}')

        Config.board[self.y][self.x] = self.pieceid

    def teleport(self, pos, record=False):
        Config.board[self.y][self.x] = '___'

        if record:
            self.moves += 1
            coord = Config.l_num_to_coord(pos)
            if Config.board[coord[0]][coord[1]] != '___':
                self.captured.append(Config.board[coord[0]][coord[1]])
                print(f'{self.pieceid} has captured {Config.board[coord[0]][coord[1]]}!')

        self.x = Config.tile_convert(pos[0])
        self.y = Config.tile_convert(pos[1], True)

        Config.board[self.y][self.x] = self.pieceid

        Config.print_board()

    def move(self, pos):
        if pos in self.possible_moves():
            coord = Config.l_num_to_coord(pos)
            if Config.board[coord[0]][coord[1]] != '___':
                self.captured.append(Config.board[coord[0]][coord[1]])
                print(f'{self.pieceid} has captured {Config.board[coord[0]][coord[1]]}!')
                # Erase piece

            if self.__class__ == Pawn:
                if abs(int(pos[1]) - Config.tile_convert(self.y, True)) == 2:
                    self.two_move = True

            self.teleport(pos)
            self.moves += 1

        else:
            print(f'Unable to move to {pos}')


    def get_info(self):
        print(f'{self.__class__.__name__}:\n')
        print('ID: ', self.pieceid)
        print('Position: ', Config.tile_convert(self.x), Config.tile_convert(self.y, True), sep='')
        print('Color: ', self.color)

    def erase(self):  # Doesn't delete the piece. It can be brought back by moving it to a square
        Config.board[self.y][self.x] = '___'
        self.erased = True

    def demo(self, rec=True):  # default board
        for pos in self.demo_moves:
            self.teleport(pos, rec)
            sleep(Config.DEMTIME)

        if self.__class__ == Pawn:
            self.promote2(Queen)

    @staticmethod
    def castle(king, rook):
        if not king.moves and not rook.moves:
            if not king.in_check:
                pass


class Pawn(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8')
        self.two_move = False

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Forward
        if not self.moves: # Starting Position
            if self.color != 'White':
                for new_y in (y+1, y+2):
                    try:
                        if Config.board[new_y][x] == '___':
                            pos_moves.append(Config.coord_to_tile(x, new_y))
                        else: break
                    except IndexError: pass

            if self.color != 'Black':
                for new_y in (y-1, y-2):
                    try:
                        if Config.board[new_y][x] == '___':
                            pos_moves.append(Config.coord_to_tile(x, new_y))
                        else: break
                    except IndexError: pass

        else:  # Post-Start
            if self.color != 'White':
                try:
                    if Config.board[y+1][x] == '___':
                        pos_moves.append(Config.coord_to_tile(x, y+1))
                except IndexError: pass

            if self.color != 'Black':
                try:
                    if Config.board[y-1][x] == '___':
                        pos_moves.append(Config.coord_to_tile(x, y-1))
                except IndexError: pass

        # Capturing
        if self.color != 'White':
            if self.color is not None:
                try:
                    if Config.c_convert(self.color) in Config.board[y+1][x+1]:
                        pos_moves.append(Config.coord_to_tile(x+1, y+1))
                except IndexError: pass
            else:
                try:
                    if Config.board[y+1][x+1] != '___':
                        pos_moves.append(Config.coord_to_tile(x+1, y+1))
                except IndexError: pass

        if self.color != 'Black':
            if self.color is not None:
                try:
                    if Config.c_convert(self.color) in Config.board[y-1][x-1]:
                        pos_moves.append(Config.coord_to_tile(x-1, y-1))
                except IndexError: pass
            else:
                try:
                    if Config.board[y+1][x+1] != '___':
                        pos_moves.append(Config.coord_to_tile(x-1, y-1))
                except IndexError: pass

        # En Passant


        return sorted(pos_moves)

    def promote(self, piece):  # oringal_piece = original_piece.promote(new_piece)
        pos = f'{Config.tile_convert(self.x)}{Config.tile_convert(self.y, True)}'

        return piece(pos, color=self.color, num='p')

    def promote2(self, piece):
        pos = f'{Config.tile_convert(self.x)}{Config.tile_convert(self.y, True)}'

        self.__class__ = piece
        self.__init__(pos, self.color, 'p')


class Knight(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e1', 'f3', 'g5', 'h7', 'f8', 'e6', 'c5', 'd3', 'e1')

    def possible_moves(self):
        pos_moves = []
        for x_off, y_off in ( (1, 2), (-1, 2), (1, -2), (-1, -2), (2, 1), (-2, 1), (2, -1), (-2, -1) ):
            new_x = self.x + x_off
            new_y = self.y + y_off
            if 0 <= new_x < Config.b_len and 0 <= new_y < Config.b_len:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)


class Bishop(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'e5', 'b8', 'h2', 'e5', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        right_up = zip(range(x + 1, Config.b_len), range(y - 1, -1, -1))
        right_down = zip(range(x + 1, Config.b_len), range(y + 1, Config.b_len))

        left_up = zip(range(x - 1, -1, -1), range(y - 1, -1, -1))
        left_down = zip(range(x - 1, -1, -1), range(y + 1, Config.b_len))

        for r in (right_up, right_down, left_up, left_down):
            for new_x, new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                        if Config.board[new_y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)


class Rook(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'a8', 'h8', 'h1', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Horizontal
        for r in (range(x+1, Config.b_len), reversed(range(x))):
            for new_x in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, y))
                        if Config.board[y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, y))
                    if Config.board[new_y][new_x] != '___': break

        # Vertical
        for r in (range(y+1, Config.b_len), reversed(range(y))):
            for new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][x]:
                        pos_moves.append(Config.coord_to_tile(x, new_y))
                        if Config.board[new_y][x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(x, new_y))
                    if Config.board[new_y][new_x] != '___': break

        return sorted(pos_moves)


class Queen(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'h8', 'a8', 'h1', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Horizontal
        for r in (range(x+1, Config.b_len), reversed(range(x))):
            for new_x in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, y))
                        if Config.board[y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(f'{Config.tile_convert(new_x)}{Config.tile_convert(y, True)}')
                    if Config.board[new_y][new_x] != '___': break

        # Vertical
        for r in (range(y+1, Config.b_len), reversed(range(y))):
            for new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][x]:
                        pos_moves.append(Config.coord_to_tile(x, new_y))
                        if Config.board[new_y][x] != '___': break
                    else: break
                else:
                    pos_moves.append(f'{Config.tile_convert(x)}{Config.tile_convert(new_y, True)}')
                    if Config.board[new_y][new_x] != '___': break

        #Diagonal
        right_up = zip(range(x + 1, Config.b_len), range(y - 1, -1, -1))
        right_down = zip(range(x + 1, Config.b_len), range(y + 1, Config.b_len))

        left_up = zip(range(x - 1, -1, -1), range(y - 1, -1, -1))
        left_down = zip(range(x - 1, -1, -1), range(y + 1, Config.b_len))

        for r in (right_up, right_down, left_up, left_down):
            for new_x, new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                        if Config.board[new_y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)


class King(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e4', 'd5', 'c4', 'c5', 'd6', 'e5', 'e4')
        self.in_check = False

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        for x_off, y_off in ( (0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, 1), (-1, -1), (1, -1) ):
            new_x = self.x + x_off
            new_y = self.y + y_off
            if 0 <= new_x < Config.b_len and 0 <= new_y < Config.b_len:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))


        return sorted(pos_moves)


# Some commands to start out with

Config.new_board('default')

r1 = Rook(color='w')
n1 = Knight('a5', color='b')
p1 = Pawn('e1', color='w')
p2 = Pawn('e8', color='b')
p3 = Pawn('f7', color='w')
r1.teleport('b3')

I plan to use the IDs in order to dictate piece images on the GUI.

\$\endgroup\$
7
\$\begingroup\$

Style

The overall style is good and mostly follows the official style guide. Since you have a lot of code and the scope will likely expand even more, I would highly recommend using an automatic style checking tool like pylint, flake8, or the like. There are a lot of Python IDEs which integrate nicely with those style checkers (Visual Studio Code, PyCharm, Spyder, ...) and can even annotate (some also autofix) your code while you write. This will help you to keep a consistent code style even on a larger scope.

The main thing I would critique about the code's style is the sometimes inconsistent usage of vertical whitespace. The usual conventions is to have at most one line of vertical whitespace within functions, methods, and classes. Individual functions and classes should be seperated by two blank lines. There are also usually two blank lines following the imports.

Overall structure

The overall structure is quite good. Nevertheless I would actually recommend splitting the file into three files. A file for the chess pieces, one for the config/board and an application script which ties everything together. In order to pull this off, the classes might need a little bit of work, but we will come to that later.

The application script should also likely make use of the infamous if __name__ == "__main__":, to make it clear which parts are actually supposed to be run as script.

The classes

Config

This class is actually the piece of code I like the least. First, I don't like the name. It is generic and IMHO does not really fit what the class is about. While looking through the code, it becomes more and more apparent that Config is the actual chessboard. So why not call it just like that, Chessboard?

The second thing I don't like about this class is the way that classmethods are used. I can see why it might look appealing to define everything as attributes and methods of the class in order to avoid having to pass around the board to the chesspieces. But I think there are better alternatives to do this.

Some parts of it can stay as class attributes such as types, white_pieces, and black_pieces1, although I would capitalize their names in order to make it clear that these are supposed to be seen as constants. Since you are also using letters in a few places, it might be also be legetimate to have them as class attribute, though I would at least prepend the name with an _ to make it clear that this is an implementation detail and not supposed to be used by an ordinary user.

So what could the new class look like?

class Chessboard:
    """The chessboard to play on

    The board can have different sizes, the default one has 8x8 fields. There
    is a list of predefined sizes in Chessboard.TYPES, and you can request a
    custom size by passing "custom<n>", where <n> is should be convertible to
    an integer between 1 and 26, as an argument to the constructor.
    """

    TYPES = {
        'min': 1, 'miniature': 3, 'small': 5, 'default': 8,
        'extended': 11, 'large': 15, 'massive': 20, 'max': 26
    }

    WHITE_PIECES = {
        'Pawn': "♙", 'Rook': "♖", 'Knight': "♘",
        'Bishop': "♗", 'King': "♔", 'Queen': "♕"
    }

    BLACK_PIECES = {
        'Pawn': "♟", 'Rook': "♜", 'Knight': "♞", 
        'Bishop': "♝",'King': "♚",'Queen': "♛"
    }

    _LETTERS = string.ascii_lowercase   # this is basically'abcdefghijklmnopqrstuvwxyz'

    def __init__(self, board_type="default", show=False):
        self.b_len = None
        if board_type in self.TYPES:
            self.b_len = self.TYPES[board_type]
        elif board_type.startswith("custom"):
            try:
                self.b_len = int(board_type.replace('custom', '').strip())
            except ValueError:
                pass

        if self.b_len is None:
            raise ValueError(f"Cannot create board for type '{board_type}'")
        if self.b_len < 1 or self.b_len > 26:
            raise ValueError(f"The board size has to be between 1 and 26, was {self.b_len}")

        self.board = self.board = [['___' for _ in range(self.b_len)]
                                   for _ in range(self.b_len)]
        if show:
            self.print_board()

The major changes are that board and b_len are now instance attributes instead of class attributes. Now you have to explicitly create an instance of the board, using board = Chessboard("default") or the like, before being able to use it. This will make all the if self.board != "UNITIALIZED": checks obsolote. Apart from that you could now have multiple boards to play with if you like. The __init__ constructor now more or less replaced new_board from the original implementation. As a bonus features I added an exception to signal the failure to create the board. This will also help you to detect the failure to do so programatically later on. You can read more about exceptions and exception handling in the documentation.

Next up on the list in this class is print_board. Same suggestion as above: no class method needed, make it an instance method. As mentioned above this lets you get rid of the initial check. One level of nesting gone. The method itself is not easily readable and left me thinking for a few moments. You define a helper function, printl as follows:

def printl():
    if len(str(cls.b_len)) == 2:
        print(' ', end='')
    for x in range(cls.b_len):
        print(' '*6 + f'{cls.letters[x]}', end='')
    print('\n')

After staring at it for a while I realized that this is the part where the "coordinate letters" are printed, and that you are trying to determine if you need an extra whitespace upfront to have it aligned correctly for boards with more than 10 fields. All of this can be done quite a bit shorter, for example:

def print_letters():
    print((" " if self.b_len >= 10 else "") 
          + "".join(f"{letter:>7}" for letter in self._LETTERS[:self.b_len]))

Let's break this down: (" " if n >= 10 else "") does add that extra leading whitespace for larger boards, f"{letter:>7}" prints letter right-aligned in a field of width 7 (6+the letter itself), and the letters are taken from self._LETTERS. All these formatted letters are then joined together and printed.

The rest of the code can more or less stay like this. If you are willing to print the numbers with a width of 2 for boards with a size smaller than 10, you can save yourself some code in the function above and in the line where the board is printed. I also advocate to avoid hardcoding additional leading and trailing newlines, since this takes control away from the user on how to have it printed to the console.

def print_board(self, leading=2, trailing=4):
    """Print the board to the console

    The number of leading and trailing newlines can be configured 
    and is 2, respectively 4 by default
    """
    def print_letters():
        print(" " + "".join(f"{letter:>7}" for letter in self._LETTERS[:self.b_len]))

    print("\n"*leading, end="")   # end="" is to avoid newlines for 0
    print_letters()
    for i in range(self.b_len):
        print(
            f'{self.b_len-i:>2}  {self.board[i]}  {self.b_len-i:>2}\n'
        )
    print_letters()
    print("\n"*trailing, end="")

If you want to prettify the print any further, you should have a look at the wide range of Unicode box characters.

The modifications to the other functions are mostly straightforward

def tile_convert(self, x, display_tile=False):
    if not display_tile:
        if isinstance(x, str):
            return self._LETTERS.index(x)
        else:
            return self._LETTERS[x]
    else:  # display_tile converts the letter in {letter}{number} to a number
        return self.b_len - int(x)

def l_num_to_coord(self, pos):
    return self.b_len - int(pos[1]), int(self.tile_convert(pos[0]))

def coord_to_tile(self, x, y):
    return f'{self.tile_convert(x)}{self.tile_convert(y, True)}'

def convert_color(self, color):
    if color == 'White':
        return 'b'
    if color == "Black":
        return 'w'

All of them would greatly benefit from a little bit of documentation, since their function is not always obvious from the name.


There is a lot left to discuss. I have not yet touched on the chess pieces or their interaction with the board. Maybe future me (Edit: seems like it did) or an other member of the community will further elaborate on that topic.

Until then: Happy coding!


1 Funny sidenote: I tested your program in a console with a black background. Under these circumstances the black pieces do actually look more like white pieces and vice versa.

\$\endgroup\$
  • \$\begingroup\$ I noticed that too with the pieces. On a tkinter window they look correct though :) \$\endgroup\$ – Alec Alameddine May 24 at 21:20
  • \$\begingroup\$ Have you thought about adding an option to invert the color? \$\endgroup\$ – AlexV May 24 at 21:23
  • 1
    \$\begingroup\$ I did think about that but the ASCII pieces aren’t intended for use in the terminal program. If i get around to defining a different function to “super-pretty-print” the board which doesn’t include IDs, I’ll probably reverse the colors for terminal usage \$\endgroup\$ – Alec Alameddine May 24 at 21:32
  • \$\begingroup\$ This was very helpful! You said there was a lot left to discuss. I added a bounty. \$\endgroup\$ – Alec Alameddine May 27 at 22:07
1
\$\begingroup\$

Since the original answer is quite long, I will add some of my further thoughts as a second answer.


The classes (continued)

ChessPiece

ChessPiece could also need a thought or two.

The idea of having a common base for all chess pieces which is then specialized for each type of piece is a reasonable approach. The way you chose to implement it partly violates the principles of "specialization". For example, set_id() needs to know that there is a subclass called Knight that has to be handled differently from the other pieces. A better approach would be to implement a generic version of set_id (maybe also _set_id to mark it as "private"), which is then overridden in the Knight class, or any other class that might need a special representation. No need to touch the base class for this.

There are other aspects of the class that might need a little touch to get going, e.g. you should definitely use an enum for the color to save yourself some headache.

import enum

class Color(enum.Enum):
    BLACK = 0
    WHITE = 1

I don't know if there is an acutal use-case for uncolored chess pieces, but if you find the need to have them, you could simply add that to the Color class. Using the enum class will help you to stay away from code like

if self.color.lower() in ('black', 'white', 'b', 'w'):
    self.pieceid = self.color.lower()[0] + self.pieceid
    if self.color.lower() == 'b':
        self.color = 'Black'
    elif self.color.lower() == 'w':
        self.color = 'White'

and work with

short_color = "b" if self.color == Color.BLACK else "w"
self.pieceid = short_color + self.pieceid

The change using an enum also sets the journey towards separating the actual object and its properties from the presentation of said object. At the moment chess pieces are represented on the board using their piece ID. You could also simply put the piece instances themselves on the board (empty positions would be filled with None instead of '___' and use their string representation just in case you want to print the board. You already have __str__ and __repr__ for your pieces, so that step would be a no-brainer. You could also move the BLACK_PIECES/WHITE_PIECES dicts away from the board and store the symbol in each (sub)class. I'm not 100% sure which of these approaches I'ld prefer.

I would also recommend to think about the separation of concerns between the board, the pieces, and a (yet to come - maybe?) chess game (controller). At the moment the pieces are closely linked to the board and use the global board in several places. The rules of the game are implemented in possible_moves() which also sometimes needs to know the board situation, to implement moves like en passant and castling. From what I can see from your question and the code you seem to be aware of this. Also things like creation, teleportation, and capturing are more a game-thing than a piece thing I would think. I'm not claiming to be an expert in this area, so take this with a grain of salt.


The second answer is way less detailed than the first answer. I nevertheless hope you will find some helpful hints that may help to improve your code to your satisfaction.

\$\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.