Hello, and welcome to the final for Let's Learn Software Development with Python! In this challenge will be developing a class that simulates the game-play of chess.
Chess requires critical thinking, analytical skills, and the ability to anticipate and react to your opponent's moves. And just like chess, this final will challenge you with the goal of making you a better programmer. Having played the game is not necessary, however having some passing knowledge of the game will be helpful in this project, please refer to this chess.com tutorial on how to play the game.
These questions are potentially on the harder side, however we have high expectations of you, and hope you will take it slow, think things out, try different pieces of code, and eventually reach a solution.
For your reference, here is the layout of a chess board. Algebraic notation represents a tile on the board. For example 'h8' is the top right in the image and 'd4' is near the center. This notation is composed of two parts, rank and file. Rank refers to the first character and is in the range [a,h]. File refers to the second character and is in the range [1,8].
Good luck!
This two part question will aim to place chess pieces on a board. The method
place_piece is part of a Chessboard class. If you
get stuck, use the hint at the bottom of this question, it include the entire
code surrounding this method.
There is already quite a bit of helper code available to you. Here are some notes about them:
{Pawn, Rook, Bishop, Knight, King,
Queen} exist. They take (player: Chessplayer,
algebraic_notation: str) as arguments.self.board is a list of lists where values can be
either None or a Chesspiece object. One can get
to tiles on the board by indexing into it, such as
self.board[0][7]piece = Pawn(player, 'a1'),
and then one looks on the board for it, it will be found
at self.board[0][7] instead of self.board[0][0]
as one might expect. Please use piece.rank()
and piece.file() instead of integer indexing, these functions
will correctly map algebraic notation to a position on the boardThe parameters are as such
piece_name: enum for the piece, such
as Piece.PAWN and Piece.ROOKalgebraic_notation: string for the position on the board,
such as 'a1' and 'c3'player: an object of type ChessplayerPlease fill out the method place_piece This is the first part
of this method. Here, we are only looking to instantiate the correct
object and place it on the board. This function will be called multiple
times to set up a chess board.
Expect to
Chesspiece
classAssume any call to this function is unreliable. If any of these fail, the piece shouldn't be placed on the board. That means to say, you should check that:
algebraic_notation is valid (a1-h8)piece_name is one of {Piece.PAWN, Piece.ROOK, Piece.BISHOP, Piece.KNIGHT,
Piece.KING, Piece.QUEEN}This is a continuation of the previous question, now we aim to place a piece on the board. Please re-use code you used to create the object. Then, find the correct place to put the piece on the board. A return object is no longer necessary as a reference should now be in the board.
Expect to
Assume any call to this function is unreliable. If any of these fail, the piece shouldn't be placed on the board. That means to say, you should check that:
# ------ constants.py ------- #
import enum
class Color(enum.Enum):
WHITE = 0
BLACK = 1
class Piece(enum.Enum):
EMPTY = enum.auto()
PAWN = enum.auto()
ROOK = enum.auto()
KNIGHT = enum.auto()
BISHOP = enum.auto()
KING = enum.auto()
QUEEN = enum.auto()
chrs = {
(Color.WHITE, Piece.EMPTY): "◻",
(Color.WHITE, Piece.PAWN): "♟",
(Color.WHITE, Piece.ROOK): "♜",
(Color.WHITE, Piece.KNIGHT): "♞",
(Color.WHITE, Piece.BISHOP): "♝",
(Color.WHITE, Piece.KING): "♚",
(Color.WHITE, Piece.QUEEN): "♛",
(Color.BLACK, Piece.EMPTY): "◼",
(Color.BLACK, Piece.PAWN): "♙",
(Color.BLACK, Piece.ROOK): "♖",
(Color.BLACK, Piece.KNIGHT): "♘",
(Color.BLACK, Piece.BISHOP): "♗",
(Color.BLACK, Piece.KING): "♔",
(Color.BLACK, Piece.QUEEN): "♕",
}
files = { k: v for v, k in enumerate(["a", "b", "c", "d", "e", "f", "g", "h"])}
files_rvs = { v: k for k, v in files.items() }
ranks = { str(k): 8-k for k in range(1,9)}
ranks_rvs = { v: k for k, v in ranks.items() }
class DegreesOfFreedom:
# degrees of freedom correspond to degrees in euclidean space
zero = [ (0, x) for x in range(1,9) ] # 0 degrees
one = [ (x, -x) for x in range(1,9) ] # pi/4 degrees
two = [ (0, -x) for x in range(1,9) ] # pi/2
three = [ (-x, -x) for x in range(1,9) ] # 3pi/4
four = [ (-x, 0) for x in range(1,9) ] # pi
five = [ (-x, x) for x in range(1,9) ] # 5pi/4
six = [ (x, 0) for x in range(1,9) ] # 3pi/2
seven = [ (x, x) for x in range(1,9) ] # 7pi/4
# ------- player.py ------- #
class Chessplayer:
def __init__(self, color: Color):
self.color = color
def __str__(self):
s = f"{self.color} player"
return s
def get_color(self):
return self.color
# ------- piece.py ------- #
def calc_direction(position, direction):
"""
A1 + (0,2) = A3
A1 + (1,1) = B2
B1 - (1,0) = A1
A1 + (0,-1) = invalid
x,y corresponds to current position
i,j corresponds to next position
"""
x, y = files[position[0]], ranks[position[1]]
i, j = x+direction[0], y+direction[1]
res = "-1"
if i in files_rvs and j in ranks_rvs:
res = files_rvs[i] + ranks_rvs[j]
return res
class Chesspiece:
def __init__(self, enum_piece, player, movements, position):
self.name = enum_piece
self.owner = player
self.color = player.color
self.movements = movements
self.active = True
self.has_moved = False
self.position = position # A1
self.tile = (player.color, enum_piece)
def __str__(self):
s = f"{self.name} - {self.color} at {self.position}"
return s
def __eq__(self, other):
try:
return hash((self.name, self.color, self.position)) == hash((other.name, other.color, other.position))
except:
return False
def rank(self):
return ranks[self.position[1]]
def file(self):
return files[self.position[0]]
def update_position(self, algebraic_notation):
self.position = algebraic_notation
def get_color(self):
return self.color
def degrees_of_movement(self):
"""
map the pieces freedom of movement to places on board
eg. res = [["A2"], ["B1", "C1"]]
"""
res = []
for direction in self.movements:
res_direction = []
for move in direction:
move_tmp = calc_direction(self.position, move)
if move_tmp != "-1":
res_direction.append(move_tmp)
if res_direction:
res.append(res_direction)
#print(f"{self} can move to {res}")
return res
# move (right, up)
# move up one = (0, 1)
# move diagonal left (-1, 1)
class Pawn(Chesspiece):
def __init__(self, player, starting_position):
movements = [[(0, -1), (0, -2)]]
Chesspiece.__init__(self, Piece.PAWN, player, movements, starting_position)
class Knight(Chesspiece):
def __init__(self, player, starting_position):
movements = [[(1, 2), (2, 1)],
[(2, -1), (1, -2)],
[(-1, -2), (-2, -1)],
[(-2, 1), (-1, 2)]]
Chesspiece.__init__(self, Piece.KNIGHT, player, movements, starting_position)
class Bishop(Chesspiece):
def __init__(self, player, starting_position):
movements = [DegreesOfFreedom.one, DegreesOfFreedom.three, DegreesOfFreedom.five, DegreesOfFreedom.seven]
Chesspiece.__init__(self, Piece.BISHOP, player, movements, starting_position)
class Rook(Chesspiece):
def __init__(self, player, starting_position):
movements = [DegreesOfFreedom.zero, DegreesOfFreedom.two, DegreesOfFreedom.four, DegreesOfFreedom.six]
Chesspiece.__init__(self, Piece.ROOK, player, movements, starting_position)
class King(Chesspiece):
def __init__(self, player, starting_position):
movements = [[(1,0)], [(1,1)], [(0,1)], [(-1,1)], [(-1,0)], [(-1,-1)], [(0,-1)], [(1,-1)]]
Chesspiece.__init__(self, Piece.KING, player, movements, starting_position)
class Queen(Chesspiece):
def __init__(self, player, starting_position):
movements = [DegreesOfFreedom.zero, DegreesOfFreedom.one, DegreesOfFreedom.two, DegreesOfFreedom.three,
DegreesOfFreedom.four, DegreesOfFreedom.five, DegreesOfFreedom.six, DegreesOfFreedom.seven]
Chesspiece.__init__(self, Piece.QUEEN, player, movements, starting_position)
# ------- board.py ------- #
class Chessboard:
def place_piece(self, piece_name: Piece, algebraic_notation: str, player: Chessplayer):
pass
def move(self, player: Chessplayer, move_from: str, move_to: str):
"""
should
- check that the move is valid
- if enemy piece on 'move_to', remove from board
- remove the piece at the old location
- add the piece at the new location
Parameters:
- player: the player who is moving the piece
- move_from: the location of the piece to be moved
- move_to: the location where the piece should be moved to
Returns:
- boolean: whether the move was successful
"""
pass
def __init__(self):
self.board = self.init_board()
self.num_files = len(self.board[0])
self.num_ranks = len(self.board)
def __getitem__(self, algebraic_notation: str):
"""
ranks and files are dictionaries to help map a character to an index
'a1' is located at self.board[0][7]
'c3' is located at self.board[2][5]
"""
filei, rankj = algebraic_notation[0], algebraic_notation[1]
if filei not in files or rankj not in ranks:
return None
return self.board[ranks[rankj]][files[filei]]
def __eq__(self, other):
res = False
cnt = 0
for j in range(self.num_ranks):
for i in range(self.num_files):
if self.board[j][i] == other.board[j][i]:
cnt += 1
return cnt == self.num_ranks * self.num_files
def init_board(self):
return [[None] * 8 for _ in range(8)]
def get_piece(self, algebraic_notation):
return self.__getitem__(algebraic_notation)
def flip(self):
return [row[::-1] for row in reversed(self.board)]
def display(self, flip_board=False):
"""
pretty print board using utf
"""
board = self.board if not flip_board else self.flip(self.board)
for i, col in enumerate(board):
col_strings = []
for j, tile in enumerate(col):
# tile is either none or a piece
tile_utf = chrs[(Color((i + j) % 2), Piece.EMPTY)]
if tile:
tile_utf = chrs.get(tile.tile)
col_strings.append(tile_utf)
print("".join(col_strings))
print()
def get_valid_moves(self, player, piece):
"""
filters a pieces freedom of movement to restrictions of other pieces
on the board
eg. white rook at a1, white pawn at a3, black bishop at d1 will return
[['a2'], ['b1', 'c1', 'd1']]
"""
valid_moves = []
for directions in piece.degrees_of_movement():
valid_moves_i = []
for move in directions:
if self.__getitem__(move):
collision_piece = self.get_piece(move)
if collision_piece.get_color() == player.get_color():
break
else:
# it's an enemy piece, can take, but shouldn't look any further
valid_moves_i.append(move)
break
valid_moves_i.append(move)
if valid_moves_i:
valid_moves.append(valid_moves_i)
return valid_moves
def remove(self, piece: Chesspiece):
"""
will remove the piece from the board, should be validated before this step
"""
self.board[piece.rank()][piece.file()] = None
Please fill out the function move. Use the comments in the
code to help determine what steps should be taken.
Assume any call to this function is unreliable. If any of these fail, the board shouldn't change. That means to say, you should check that:
algebraic_notation for move_from
and move_to are valid (a1-h8)move_fromWe are already providing some helper methods baked into the
Chessboard and Piece classes for you to use,
these are listed below:
self.get_piece(algebraic_notation: str) returns
None if no piece found, otherwise returns the piece at that
tileself.get_valid_moves(player: Chesslayer, piece:
Chesspiece) returns a list of lists of algebraic notation tiles
a piece can move to given the pieces type and current position; these
are valid positions that considers the dimensions of the board, the
players own pieces, and the opponents piecespiece.update_position(algebraic_notation: str) updates a pieces
knowledge of where it is located on the boardpiece.rank() and piece.file() are helpful again for indexing
into self.board