Making the chess functional

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:

Making the chess functional

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.

Engage Thrusters

Step 1 – updating the board for change in FEN notation

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])

Step 2 – binding mouse click event

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.

Step 3 – adding attribute to track selected piece and remaining pieces

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

Step 4 – identifying square clicked

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:

  • The first part of code calculates the coordinates for the piece clicked. Based on the calculated coordinates, it stores the corresponding letter notation in a variable named pos.
  • It then tries to assign the variable piece to the corresponding piece instance. If there is no piece instance on the clicked square, it simply ignores the click.
  • The second part of the method checks if this is the second click intended to move a piece to a destination square. If this is the second click, it calls the shift method, passing in the source and destination coordinates as its two arguments.
  • If shift succeeds, it sets back all previously set attributes to their original empty values and calls our draw_board and draw_pieces method to redraw the board and pieces.
  • If this is the first click, it calls a method named 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.

Step 5 – getting the source and destination position

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.

Step 6 – collecting list of moves to highlight

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)))

Step 7 – modifying draw_board to highlight allowed moves

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:

  • The additions made to our existing draw_board method are highlighted in the preceding code. We first define an attribute named highlightcolor, and assign it a color.
  • In essence, the code has been modified to handle the clicks. The first section of highlighted code fills a different color to highlight all available moves.
  • The second section of highlighted code changes the coordinates of the piece instance to be located on new coordinates. Note the use of canvas.coords(name, x0, y0) to change the coordinates.
  • The last two lines change the precedence of options specified by tags.

Note

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.

Step 8 – defining attributes to keep game statistics

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 = []

Step 9 – preshift validations

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:

  • The code first checks if there exists a piece on the destination.
  • It then checks if it is a valid turn for the player. If not, it raises an exception.
  • It then checks if the move is proposed to occur to a valid location. If a player attempts to move a piece to an invalid location, it raises a corresponding exception.
  • It then checks if there is a check on the king. To do that, it calls a method named 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)
  • This method creates a deep temporary copy of the object and tries to move the piece on the temporary copy. As a note, shallow copy of a collection is a copy of the collection structure, not the elements. When you do a shallow copy, the two collections now share the individual elements, so a modification at one place affects the other as well. In contrast, deep copies makes copy of everything, the structure as well as the elements. We need to create a deep copy of the board, because we want to check if the king makes a valid move before it actually moves and we want to do that without modifying the original object state in any way.
  • After executing the move on the temporary copy, it checks if the king is in check to return 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.
  • Similarly, it checks for possible occurrence of checkmate or draw and raises exceptions accordingly.
  • If no exceptions are made, it finally calls a method named move, which actually executes the move.

Step 10 – actual movement of pieces

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

Step 11 – Post movement updates

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:

  • Keeps track of statistics, such as number of moves, halfmove clock
  • Changes the player's turn
  • Checks if a pawn has been moved so as to reset the halfmove clock
  • And finally, appends the last move to our history list

Step 12 – classes to handle exceptions and errors

Finally, we add the following empty classes for various exceptions raised by us:

class Check(ChessError): pass
classInvalidMove(ChessError): pass
classCheckMate(ChessError): pass
class Draw(ChessError): pass
classNotYourTurn(ChessError): pass

Objective Complete – Mini Debriefing

Let's summarize things that we did in this iteration

  • We started by binding a mouse click event to a method named square_clicked.
  • We added attributes to track selected piece and remaining pieces on the board.
  • We then identified the square clicked, followed by collecting the source and destination position.
  • We also collected a list of all possible moves for the selected piece, and then highlighted them.
  • We then defined attributes to keep vital game statistics.
  • We then did some preshift validations, followed by actual movement of pieces on the board.
  • After a piece had been moved, we updated statistics about the game.
  • We had defined several exceptions in this iteration. We simply defined empty classes to handle them silently.

Our chess game is now functional. Two players can now play a game of chess on our application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.217.198