Python Chess Game

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.

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.

• I noticed that too with the pieces. On a tkinter window they look correct though :)
– Alec
May 24, 2019 at 21:20
• Have you thought about adding an option to invert the color? May 24, 2019 at 21:23
• 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
– Alec
May 24, 2019 at 21:32
• This was very helpful! You said there was a lot left to discuss. I added a bounty.
– Alec
May 27, 2019 at 22:07

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.