Now that we have all pieces and board-related validation rules in place, let's now add life to our chess. In this iteration, we will make our chess game fully functional.
In a game between two players, our chessboard would be like one shown in the following screenshot:
The objective for this iteration is to move pieces on click of the left mouse button. When a player clicks on a piece, our code should first check if it is a legitimate turn for that piece.
On the first click, the piece to be moved is selected, and all allowed moves for that piece are highlighted on the board. The second click should happen on the destination square. If the second click is done on a valid destination square, the piece should move from the source square to the destination square.
We also need to code the events of capturing of pieces and check on king. Other attributes to be tracked include list of captured pieces, halfmove clock count, fullmove number count, and history of all previous moves.
So far, we have the ability to take the original FEN notation and display it on board. However, we need a way that takes any FEN notation and updates the display on the board. We define a new method named show()
to do this, as follows:
def show(self, pat): self.clear() pat = pat.split(' ') def expand(match): return ' ' * int(match.group(0)) pat[0] = re.compile(r'd').sub(expand, pat[0]) for x, row in enumerate(pat[0].split('/')): for y, letter in enumerate(row): if letter == ' ': continue coord = self.alpha_notation((7-x,y)) self[coord] = pieces.create_piece(letter) self[coord].place(self) if pat[1] == 'w': self.player_turn = 'white' else: self.player_turn = 'black' self.halfmove_clock = int(pat[2]) self.fullmove_number = int(pat[3])
The pieces need to move on click of the mouse. So, we need to track the mouse click event. We only need to track mouse clicks on the Canvas widget. Let us, therefore, add an event handler to our GUI
class immediately after the code that created the Canvas widget in the init
method as follows (see code 4.06: gui.py, __init__ method):
self.canvas.bind("<Button-1>", self.square_clicked)
This will bind the left mouse click event to a new method, square_clicked
. However, before we sit down and define this method, let's pause and think about the attributes we need to keep tracking our program.
First of all, we need to track all pieces remaining on the board after every move. So we will create a dictionary pieces to keep track of this. We also need to track the name of the piece selected by the mouse click. We store that in an attribute, selected_piece
. When a player clicks on a piece, we need to highlight all valid moves for that piece. We store all valid moves for that piece in a list named focused
. Let's define these three attributes in our GUI
class before defining any of the methods. We modify our GUI
class to include these attributes as follows:
class GUI: pieces = {} selected_piece = None focused = None #other attributes from previous iterations
We will code our square_clicked
method that gets called from the event handler we defined earlier.
The desired functionality of this method is twofold. We should be able to locate the coordinate of a piece being clicked. The first click should select a given piece. The second click should move the piece from the source square to the destination square.
The method is defined as follows(see code 4.06: gui.py):
def square_clicked(self, event): col_size = row_size = self.dim_square selected_column = event.x / col_size selected_row = 7 - (event.y / row_size) pos = self.chessboard.alpha_notation((selected_row, selected_column)) try: piece = self.chessboard[pos] except: pass if self.selected_piece: self.shift(self.selected_piece[1], pos) self.selected_piece = None self.focused = None self.pieces = {} self.draw_board() self.draw_pieces() self.focus(pos) self.draw_board()
The description of the code is listed as follows:
pos
.shift
method, passing in the source and destination coordinates as its two arguments.draw_board
and draw_pieces
method to redraw the board and pieces.focus
to highlight all available moves for the first click, followed by a call to draw the fresh board.While coding the desired functionality for the square_clicked
method, we called several new methods from within it. We need to define those new methods.
We have called the shift
method from the square_clicked
method. The following shift code implemented is simply responsible for collecting the necessary arguments required for the shift operation.
In the spirit of keeping logic separate from presentation, we do not process shift-related rules in this view class. Instead, we delegate the shift
method work from the GUI
to Board
class. Once the logic or validation for shift has been implemented, the visible part of the shift of pieces again takes place in the draw_board
method of our GUI
class.
Although this may seem like overkill at first, structuring logic and presentation in different layers is very important for code reuse, scalability, and maintainability.
The code is as follows:
def shift(self, p1, p2): piece = self.chessboard[p1] try: dest_piece = self.chessboard[p2] except: dest_piece = None if dest_piece is None or dest_piece.color != piece.color: try: self.chessboard.shift(p1, p2) except: pass
The code first checks if there exists a piece on the destination. If a piece does not exist at the destination square, it calls on a method, shift
, from chessboard.py
.
We have also called the focus method from square_clicked
method. The purpose of this method is to collect all possible moves for a given piece in a list named focused
. The actual focusing of available moves takes place in the draw_board
method of our GUI class.
The code is as follows (see code 4.06: gui.py):
def focus(self, pos): try: piece = self.chessboard[pos] except: piece=None if piece is not None and (piece.color == self.chessboard.player_turn): self.selected_piece = (self.chessboard[pos], pos) self.focused = map(self.chessboard.num_notation, (self.chessboard[pos].moves_available(pos)))
In the square_clicked
method, we called the draw_board
method to take care of redrawing or changing the coordinates for our pieces. Our current draw_board
method is not equipped to handle this, because we had designed it in the first iteration only to provide us with a blank board. Let's first modify our draw_board
method to handle this, as follows (see code 4.06: gui.py):
highlightcolor ="khaki" def draw_board(self): color = self.color2 for row in range(self.rows): color = self.color1 if color == self.color2 else self.color2 for col in range(self.columns): x1 = (col * self.dim_square) y1 = ((7-row) * self.dim_square) x2 = x1 + self.dim_square y2 = y1 + self.dim_square if(self.focused is not None and (row, col) in self.focused): self.canvas.create_rectangle(x1, y1, x2, y2, fill=self.highlightcolor, tags="area") else: self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, tags="area") color = self.color1 if color == self.color2 else self.color2 for name in self.pieces: self.pieces[name] = (self.pieces[name][0], self.pieces[name][1]) x0 = (self.pieces[name][1] * self.dim_square) + int(self.dim_square/2) y0 = ((7-self.pieces[name][0]) * self.dim_square) + int(self.dim_square/2) self.canvas.coords(name, x0, y0) self.canvas.tag_raise("occupied") self.canvas.tag_lower("area")
The description of the code is listed as follows:
draw_board
method are highlighted in the preceding code. We first define an attribute named highlightcolor
, and assign it a color.canvas.coords(name, x0, y0)
to change the coordinates.If an object on the canvas is tagged to multiple tags, options defined for tags at the top of the stack have higher precedence. You can, however, change the precedence of tags by using tag_raise(name)
or tag_lower(name)
.
For a complete list of canvas-related options, refer to interactive help for the Canvas widget using help(Tkinter.Canvas)
in the command line.
As a consequence of adding mobility to our pieces, we need to add the following new attributes to our Board
class to keep game statistics, as follows (see code 4.06: chessboard.py):
Class Board(dict): #other attributes from previous iteration captured_pieces = { 'white': [], 'black': [] } player_turn = None halfmove_clock = 0 fullmove_number = 1 history = []
For that, we will code the shift
method of our Board
class, as follows (see code 4.06: chessboard.py):
def shift(self, p1, p2): p1, p2 = p1.upper(), p2.upper() piece = self[p1] try: dest = self[p2] except: dest = None if self.player_turn != piece.color: raise NotYourTurn("Not " + piece.color + "'s turn!") enemy = ('white' if piece.color == 'black' else 'black' ) moves_available = piece.moves_available(p1) if p2 not in moves_available: raise InvalidMove if self.all_moves_available(enemy): if self.is_in_check_after_move(p1,p2): raise Check if not moves_available and self.king_in_check(piece.color): raise CheckMate elif not moves_available: raise Draw else: self.move(p1, p2) self.complete_move(piece, dest, p1,p2)
The description of the code is listed as follows:
is_in_check_after_move
, which is defined as follows:def is_in_check_after_move(self, p1, p2): temp = deepcopy(self) temp.unvalidated_move(p1,p2) returntemp.king_in_check(self[p1].color)
True
or False
. If the king is in check on the temporary board, it raises an exception, not allowing such a move on our actual board.move
, which actually executes the move.Actual movement of pieces can be coded as follows:
def move(self, p1, p2): piece = self[p1] try: dest = self[p2] except: pass del self[p1] self[p2] = piece
After the move has actually been executed, it calls another method named complete_move
, which updates game statistics as follows:
def complete_move(self, piece, dest, p1, p2): enemy = ('white' if piece.color == 'black' else 'black' ) if piece.color == 'black': self.fullmove_number += 1 self.halfmove_clock +=1 self.player_turn = enemy abbr = piece.shortname if abbr == 'P': abbr = '' self.halfmove_clock = 0 if dest is None: movetext = abbr + p2.lower() else: movetext = abbr + 'x' + p2.lower() self.halfmove_clock = 0 self.history.append(movetext)
The preceding method does the following tasks:
Let's summarize things that we did in this iteration
square_clicked
.Our chess game is now functional. Two players can now play a game of chess on our application.
3.138.67.27