I teach programming, and I currently teach my class about inheritance, abstract classes, and mixins.
I wrote this code as an example, and I want to be sure it is as good as possible before I release it as a code example.
Few assumptions:
- The code should only handle piece movements and not manage an entire game.
- The code shouldn't handle special moves like en-passant, castling, or pawn promotion.
- The code shouldn't force the king to move if another piece threatens it.
from abc import ABC, abstractmethod
class Color:
BLACK = 0
WHITE = 1
def enemy_of(color):
if color == Color.BLACK:
return Color.WHITE
return Color.BLACK
class Board:
BOARD_SIZE = (8, 8)
def __init__(self):
self.reset()
def get_square(self, row, col):
if not self.is_valid_square((row, col)):
return None
return self.board[row][col]
def set_square(self, row, col, piece):
self.board[row][col] = piece
def is_valid_square(self, square):
return (
square[0] in range(self.BOARD_SIZE[0])
and square[1] in range(self.BOARD_SIZE[1])
)
def is_empty_square(self, square):
return self.get_square(*square) is None
def _generate_first_row(self, color):
row_by_color = {Color.BLACK: 0, Color.WHITE: self.BOARD_SIZE[0] - 1}
row = row_by_color[color]
order = (Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook)
params = {'color': color, 'row': row}
return [order[i](col=i, **params) for i in range(self.BOARD_SIZE[0])]
def _generate_pawns_row(self, color):
row_by_color = {Color.BLACK: 1, Color.WHITE: self.BOARD_SIZE[0] - 2}
row = row_by_color[color]
params = {'color': color, 'row': row}
return [Pawn(col=i, **params) for i in range(self.BOARD_SIZE[0])]
def get_pieces(self, color=None):
for row in self.board:
for col in row:
if col is not None and (color is None or col.color == color):
yield col
def get_possible_moves(self, color, with_king=False):
"""Return all player's possible moves."""
pieces = self.get_pieces(color=color)
if not with_king:
pieces = [p for p in pieces if not isinstance(p, King)]
for piece in pieces:
for move in piece.get_valid_moves(self):
yield move
def reset(self):
self.board = [
self._generate_first_row(Color.BLACK),
self._generate_pawns_row(Color.BLACK),
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
self._generate_pawns_row(Color.WHITE),
self._generate_first_row(Color.WHITE),
]
def move(self, source, destination):
piece = self.get_square(*source)
return piece.move(board=self, destination=destination)
def __str__(self):
printable = ""
for row in self.board:
for col in row:
if col is None:
printable = printable + " ▭ "
else:
printable = printable + f" {col} "
printable = printable + '\n'
return printable
class Piece(ABC):
def __init__(self, color, row, col, **kwargs):
super().__init__(**kwargs)
self.color = color
self.row = row
self.col = col
def is_possible_target(self, board, target):
is_target_valid = board.is_valid_square(target)
is_empty_square = board.is_empty_square(target)
is_hitting_enemy = self.is_enemy(board.get_square(*target))
return is_target_valid and (is_empty_square or is_hitting_enemy)
@abstractmethod
def get_valid_moves(self, board):
pass
def get_position(self):
return self.row, self.col
def is_enemy(self, piece):
if piece is None:
return False
return piece.color == Color.enemy_of(self.color)
def move(self, board, destination):
if not self.is_possible_target(board, destination):
return False
if destination not in self.get_valid_moves(board):
return False
board.set_square(*self.get_position(), None)
board.set_square(*destination, self)
self.row, self.col = destination
return True
@abstractmethod
def __str__(self):
pass
class WalksDiagonallyMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not hasattr(self, 'directions'):
self.directions = set()
self.directions.update({
(-1, -1), (1, -1),
(-1, 1), (1, 1),
})
class WalksStraightMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not hasattr(self, 'directions'):
self.directions = set()
self.directions.update({
(0, -1),
(-1, 0), (1, 0),
(0, 1),
})
class WalksMultipleStepsMixin:
def get_valid_moves(self, board):
for row_change, col_change in self.directions:
steps = 1
stop_searching_in_this_direction = False
while not stop_searching_in_this_direction:
new_row = self.row + row_change * steps
new_col = self.col + col_change * steps
target = (new_row, new_col)
is_valid_target = self.is_possible_target(board, target)
if is_valid_target:
yield target
steps = steps + 1
is_hit_enemy = self.is_enemy(board.get_square(*target))
if not is_valid_target or (is_valid_target and is_hit_enemy):
stop_searching_in_this_direction = True
class Pawn(Piece):
DIRECTION_BY_COLOR = {Color.BLACK: 1, Color.WHITE: -1}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.moved = False
self.forward = self.DIRECTION_BY_COLOR[self.color]
def _get_regular_walk(self):
src_row, src_col = self.get_position()
return (src_row + self.forward, src_col)
def _get_double_walk(self):
src_row, src_col = self.get_position()
return (src_row + self.forward * 2, src_col)
def _get_diagonal_walks(self):
src_row, src_col = self.get_position()
return (
(src_row + self.forward, src_col + 1),
(src_row + self.forward, src_col - 1),
)
def is_possible_target(self, board, target):
is_valid_move = board.is_valid_square(target)
is_step_forward = (
board.is_empty_square(target)
and target == self._get_regular_walk()
)
is_valid_double_step_forward = (
board.is_empty_square(target)
and not self.moved
and target == self._get_double_walk()
and self.is_possible_target(board, self._get_regular_walk())
)
is_hitting_enemy = (
self.is_enemy(board.get_square(*target))
and target in self._get_diagonal_walks()
)
return is_valid_move and (
is_step_forward or is_valid_double_step_forward or is_hitting_enemy
)
def move(self, **kwargs):
is_success = super().move(**kwargs)
self.moved = True
return is_success
def get_valid_moves(self, board):
targets = (
self._get_regular_walk(),
self._get_double_walk(),
*self._get_diagonal_walks(),
)
for target in targets:
if self.is_possible_target(board, target):
yield target
def __str__(self):
if self.color == Color.WHITE:
return '♙'
return '♟'
class Bishop(WalksDiagonallyMixin, WalksMultipleStepsMixin, Piece):
def __str__(self):
if self.color == Color.WHITE:
return '♗'
return '♝'
class Rook(WalksStraightMixin, WalksMultipleStepsMixin, Piece):
def __str__(self):
if self.color == Color.WHITE:
return '♖'
return '♜'
class Queen(
WalksStraightMixin, WalksDiagonallyMixin, WalksMultipleStepsMixin, Piece,
):
def __str__(self):
if self.color == Color.WHITE:
return '♕'
return '♛'
class Knight(Piece):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.directions = [
(-2, 1), (-1, 2), (1, 2), (2, 1), # Upper part
(-2, -1), (-1, -2), (1, -2), (2, -1), # Lower part
]
def get_valid_moves(self, board):
for row_change, col_change in self.directions:
row, col = self.get_position()
target = (row + row_change, col + col_change)
if self.is_possible_target(board, target):
yield target
def __str__(self):
if self.color == Color.WHITE:
return '♘'
return '♞'
class King(WalksStraightMixin, WalksDiagonallyMixin, Piece):
def _get_threatened_squares(self, board):
enemy = Color.enemy_of(self.color)
enemy_moves = list(board.get_possible_moves(enemy, with_king=False))
enemy_pieces = board.get_pieces(color=enemy)
king = next(p for p in enemy_pieces if isinstance(p, King))
for move in king.get_squares_threatens(board):
yield move
for move in enemy_moves:
yield move
def is_possible_target(self, board, target):
is_regular_valid = super().is_possible_target(board, target)
threatened_squares = self._get_threatened_squares(board)
return is_regular_valid and target not in threatened_squares
def get_valid_moves(self, board):
for add_row, add_col in self.directions:
target = (add_row + self.row, add_col + self.col)
if self.is_possible_target(board, target):
yield target
def get_squares_threatens(self, board):
for direction in self.directions:
row, col = self.get_position()
row = row + direction[0]
col = col + direction[1]
if board.is_valid_square((row, col)):
yield (row, col)
def __str__(self):
if self.color == Color.WHITE:
return '♔'
return '♚'
Things I know I can improve, but I leave as-is because of my current student's knowledge:
- I can use
yield from
instead offor x in y: yield x
. - Color can inherit
enum.Enum
and useenum.auto()
for the class variables. - I can raise exceptions instead of returning
True
orFalse
.