DEV Community

Cover image for Creating a chess game with Python, pygame and chess (Pt. 2)
Prince
Prince

Posted on

Creating a chess game with Python, pygame and chess (Pt. 2)

Hey there, I hope you're having a nice day so far. We will be continuing from where we left off in part 1.

In this article we will be looking at how to create the AI part of the game and how we will plug that in with our existing code base.

We will break the article into the following sections:

  • Creating a parent AIPlayer class
  • Plugging the AIPlayer class into our existing code
  • Creating a player that selects the best move available
  • Creating a player that looks ahead a couple of moves and selects the move that gives the best outcome using the minmax algorithm.

Before we get started, please create a folder called ai in your project's root directory and in that folder add the following files: __init__.py and players.py.
Your project should look similar to this now:
chess-game/
---|ai
------|__init__.py
------|players.py
---|gui_components
---|skins
main.py

Creating a parent AIPlayer class

Seeing as we will be creating multiple ai players, it is important that we create a parent class, that way they all have some common behavior inherited from this class and can be used interchangeably in our application. We will see this later.

We need our AIPlayer to be able to do the following:

  • Get the legal moves in the game
  • Choose a move from the legal moves
  • Make a move without affecting the board (in order to evaluate possible future states of the board)
  • Make a move on a ChessBoard object.

In your players.py file type the following code

class AIPlayer: def __init__(self, board: chess.Board, color: str) -> None: self.board = board self.color = color def get_legal_moves(self, board: chess.Board=None) -> list: if not board: board = self.board return list(board.legal_moves) def choose_move(self, board: chess.Board=None): legal_moves = self.get_legal_moves() random.shuffle(legal_moves) chosen_move = None for move in legal_moves: evaluation_before = self.evaluate_board() fake_board = self.false_move(move) evaluation_after = self.evaluate_board(fake_board) if chosen_move is None: chosen_move = move else: # if the player is white and the move results in a higher material for white if evaluation_after > evaluation_before and self.color == "w": chosen_move = move # if the player is black and the move results in higher material for black elif evaluation_before > evaluation_after and self.color == "b": chosen_move = move return chosen_move def false_move(self, move: chess.Move=None, board: chess.Board=None) -> chess.Board: # make a move without affecting the game's current state # make a copy of the board for move testing if not board: board_copy = copy.deepcopy(self.board) else: board_copy = board if not move: move = self.play(board_copy) board_copy.push(move) return board_copy def make_move(self, chess_board: ChessBoard): # make a move an a ChessBoard object move = self.choose_move() chess_board._play(move=move) 
Enter fullscreen mode Exit fullscreen mode

This player doesn't implement any sophisticated techniques, it simply selects a random move from the list of available moves.

After writing this, we simply have to modify the main.py file so that our AI plays as black instead of the user.
Now in your main.py file add the following line at the top with the other imports from ai import players ai_players.
Where you have

players = { True: "user", False: "user" } 
Enter fullscreen mode Exit fullscreen mode

Change it to

players = { True: "user", False: ai_players.AIPlayer(board, "b") } 
Enter fullscreen mode Exit fullscreen mode

And towards the bottom of your main.py file in the while loop after the call to draw_chessboard function add the following code:

if not isinstance(players[TURN], str) and IS_FIRST_MOVE: # the first move is an AI so it plays automatically play() elif not isinstance(players[TURN], str) and not turns_taken[TURN]: play() 
Enter fullscreen mode Exit fullscreen mode

In the end your main.py should look like

import chess import pygame from pygame import mixer mixer.init() from gui_components.board import ChessBoard from gui_components.components import BorderedRectangle from ai import players as ai_players pygame.init() screen = pygame.display.set_mode([500, 500]) board = chess.Board() players = { True: "user", False: ai_players.AIPlayer(board, "b") } turns_taken = { True: False, # set False: False } move_sound = mixer.Sound("sound_effects/piece_move.mp3") check_sound = mixer.Sound("sound_effects/check.mp3") checkmate_sound = mixer.Sound("sound_effects/checkmate.mp3") SOURCE_POSITION = None DESTINATION_POSITION = None PREVIOUSLY_CLICKED_POSITION = None POSSIBLE_MOVES = [] TURN = True IS_FIRST_MOVE = True running = True LIGHT_COLOR = (245, 245, 245) DARK_COLOR = ( 100, 100, 100 ) WHITE_COLOR = (255, 255, 255) BLACK_COLOR = (0, 0, 0) chess_board = ChessBoard( 50, 50, 400, 400, 0, 0, board=board ) def draw_bordered_rectangle(rectangle: BorderedRectangle, screen): pygame.draw.rect( screen, rectangle.border_color, rectangle.outer_rectangle, width=rectangle.outer_rectangle_border_width ) pygame.draw.rect( screen, rectangle.background_color, rectangle.inner_rectangle, width=rectangle.inner_rectangle_border_width ) def draw_chessboard(board: ChessBoard): ranks = board.squares bordered_rectangle = BorderedRectangle(10, 10, 480, 480, (255, 255, 255), DARK_COLOR, 10) # draw_bordered_rectangle(bordered_rectangle, screen) # board_border_rect = pygame.Rect( 40, 40, 400, 400 ) # pygame.draw.rect(screen, DARK_COLOR, board_border_rect, width=1) board_bordered_rectangle = BorderedRectangle(25, 25, 450, 450, WHITE_COLOR, DARK_COLOR, 48) draw_bordered_rectangle(board_bordered_rectangle, screen) pygame.draw.rect( screen, board_bordered_rectangle.border_color, board_bordered_rectangle.inner_rectangle, width=1 ) board_top_left = board.rect.topleft board_top_right = board.rect.topright board_bottom_left = board.rect.bottomleft for i, rank in enumerate(ranks): rank_number = ChessBoard.RANKS[ 7 - i ] file_letter = ChessBoard.RANKS[i] font_size = 15 # font size for the ranks and files # add the text rectangle on the left and right of the board font = pygame.font.SysFont('helvetica', font_size) # render the ranks (1-8) for _i in range(1): if _i == 0: _rect = pygame.Rect( board_top_left[0] - font_size, board_top_left[1] + (i*board.square_size), font_size, board.square_size ) else: _rect = pygame.Rect( board_top_right[0], board_top_right[1] + (i*board.square_size), font_size, board.square_size ) text = font.render(f"{rank_number}", True, DARK_COLOR) text_rect = text.get_rect() text_rect.center = _rect.center screen.blit(text, text_rect) # render the files A-H for _i in range(1): if _i == 0: _rect = pygame.Rect( board_top_left[0] + (i*board.square_size), board_top_left[1] - font_size, board.square_size, font_size ) else: _rect = pygame.Rect( board_top_left[0] + (i*board.square_size), board_bottom_left[1], board.square_size, font_size ) text = font.render(f"{file_letter}", True, DARK_COLOR) text_rect = text.get_rect() text_rect.center = _rect.center screen.blit(text, text_rect) for j, square in enumerate(rank): if square is board.previous_move_square: pygame.draw.rect( screen, board.previous_square_highlight_color, square ) elif square is board.current_move_square: pygame.draw.rect( screen, board.current_square_highlight_color, square ) else: pygame.draw.rect( screen, square.background_color, square ) if square.piece: try: image = square.piece.get_image() image_rect = image.get_rect() image_rect.center = square.center screen.blit( image, image_rect ) except TypeError as e: raise e except FileNotFoundError as e: print(f"Error on the square on the {i}th rank and the {j}th rank") raise e if square.is_possible_move and board.move_hints: # draw a circle in the center of the square pygame.draw.circle( screen, (50, 50, 50), square.center, board.square_size*0.25 ) def play_sound(board): if board.is_checkmate(): mixer.Sound.play(checkmate_sound) elif board.is_check(): mixer.Sound.play(check_sound) elif board.is_stalemate(): pass else: mixer.Sound.play(move_sound) def play(source_coordinates: tuple=None, destination_coordinates: tuple=None): global board, TURN, IS_FIRST_MOVE, chess_board turn = board.turn player = players[turn] turns_taken[turn] = not turns_taken[turn] print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}") if not isinstance(player, str): # AI model to play player.make_move(chess_board) play_sound(board) TURN = not TURN if isinstance(players[TURN], ai_players.AIPlayer): # if the next player is an AI, automatically play print("Next player is AI, making a move for them automaically") # sleep(5) else: if source_coordinates and destination_coordinates: # user to play print("User is making move") chess_board.play(source_coordinates, destination_coordinates) play_sound(board) TURN = not TURN if IS_FIRST_MOVE: IS_FIRST_MOVE = False turns_taken[turn] = not turns_taken[turn] print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}") def click_handler(position): global SOURCE_POSITION, POSSIBLE_MOVES, TURN current_player = players[TURN] if isinstance(current_player, str): if SOURCE_POSITION is None: POSSIBLE_MOVES = chess_board.get_possible_moves(position) SOURCE_POSITION = position if POSSIBLE_MOVES else None else: # getting the squares in the possible destinations that correspond to the clicked point destination_square = [ square for square in POSSIBLE_MOVES if square.collidepoint(position) ] if not destination_square: chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True) SOURCE_POSITION = None else: destination_square = destination_square[0] print(f"In main.py, about to play, the source and destination are {SOURCE_POSITION} and {position} respectively") chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True) # chess_board.play( SOURCE_POSITION, position ) play(SOURCE_POSITION, position) SOURCE_POSITION = None current_player = players[TURN] while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN: MOUSE_CLICKED_POSITION = pygame.mouse.get_pos() click_handler(MOUSE_CLICKED_POSITION) screen.fill( (255, 255, 255) ) draw_chessboard(chess_board) if not isinstance(players[TURN], str) and IS_FIRST_MOVE: print("It is the first move and there is no human player") play() elif not isinstance(players[TURN], str) and not turns_taken[TURN]: play() pygame.display.flip() pygame.quit() 
Enter fullscreen mode Exit fullscreen mode

Now if you run the main.py file you should be able to play with the AI that randomly selects moves.

User vs Random AI player

Creating a player that selects the best move available

The base AIPlayer class selected a random move from the list of possible moves. We will now create a player that can analyze all the possible moves on the board and select the best one, to do this we will need to use some algorithm that can evaluate the board, this algorithm will take into account the pieces on the board and the positions of those pieces. We will need to go and modify our pieces.py file.
In your pieces.py file, add the following code in your Piece class after the colors_and_notations_and_values dictionary definition:

 # Gives a score based on the position of the piece on the board # this score is then added to the piece's value # to give its value relative to its position piece_square_tables = { "k": [ [-3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0], [-3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0], [-3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0], [-3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0], [-2.0, -3.0, -3.0, -4.0, -4.0, -3.0, -3.0, -2.0], [-1.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -1.0], [2.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 2.0], [2.0, 3.0, 1.0, 0.0, 0.0, 1.0, 3.0, 2.0] ], "q": [ [-2.0, -1.0, -1.0, -0.5, -0.5, -1.0, -1.0, -2.0], [-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0], [-1.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -1.0], [-0.5, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -0.5], [0.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -0.5], [-1.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.0, -1.0], [-1.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, -1.0], [-2.0, -1.0, -1.0, -0.5, -0.5, -1.0, -1.0, -2.0] ], "r": [ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5], [-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5], [-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5], [-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5], [-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5], [-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5], [0.0, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0] ], "b": [ [-2.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -2.0], [-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0], [-1.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0, -1.0], [-1.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5, -1.0], [-1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0], [-1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0], [-1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, -1.0], [-2.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -2.0] ], "n": [ [-5.0, -4.0, -3.0, -3.0, -3.0, -3.0, -4.0, -5.0], [-4.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0, -4.0], [-3.0, 0.0, 1.0, 1.5, 1.5, 1.0, 0.0, -3.0], [-3.0, 0.5, 1.5, 2.0, 2.0, 1.5, 0.5, -3.0], [-3.0, 0.0, 1.5, 2.0, 2.0, 1.5, 0.0, -3.0], [-3.0, 0.5, 1.0, 1.5, 1.5, 1.0, 0.5, -3.0], [-4.0, -2.0, 0.0, 0.5, 0.5, 0.0, -2.0, -4.0], [-5.0, -4.0, -3.0, -3.0, -3.0, -3.0, -4.0, -5.0] ], "p": [ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0], [1.0, 1.0, 2.0, 3.0, 3.0, 2.0, 1.0, 1.0], [0.5, 0.5, 1.0, 2.5, 2.5, 1.0, 0.5, 0.5], [0.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0], [0.5, -0.5, -1.0, 0.0, 0.0, -1.0, -0.5, 0.5], [0.5, 1.0, 1.0, -2.0, -2.0, 1.0, 1.0, 0.5], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ] } 
Enter fullscreen mode Exit fullscreen mode

This is a dictionary that contains the points to be added to a piece's value based on its position on the board. The positions start from the 8th rank (ranks are rows in chess, starting from white's side) to the 1st rank (i.e. piece_square_tables['k'][0] contains the positional value of the white king on the 8th rank and piece_square_tables['k'][7] contains those for the 1st rank). This dictionary shows only the positional values of white pieces, to get that for black we have to reverse the lists and negate the values of the elements.

Put this code beneath the one above to get the complete piece_square_tables.

 piece_square_tables = { "w": piece_square_tables, "b": { key: value[::-1] # reversing the previous lists for key, value in piece_square_tables.items() } } # negating the values in black's list for key, value in piece_square_tables["b"].items(): piece_square_tables["b"][key] = [ [ -j for j in rank ] for rank in value ] 
Enter fullscreen mode Exit fullscreen mode

Since we will be evaluating a chess.Board object it is important that we can easily get the different pieces on the board without having to loop through all the squares in our gui board. Let's take a look at a board, open a python shell and type in the following:

import chess board = chess.Board() print(board) 
Enter fullscreen mode Exit fullscreen mode

It should give you an output similar to the following:

r n b q k b n r p p p p p p p p . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . P P P P P P P P R N B Q K B N R 
Enter fullscreen mode Exit fullscreen mode

The black pieces are at the top and the whites down. It is important to note that the black pieces are written in lowercase and the white in upper. From this representation we can easily get the colors of the pieces and their positions on the board.

In our Piece class we will add a static method that gets the piece's color based on a notation passed. Add the following to the Piece class

 def get_piece_color_based_on_notation(notation) -> str: return "w" if notation.isupper() else "b" 
Enter fullscreen mode Exit fullscreen mode

We also need to add a method that gets a piece's value when given its notation, color, rank_number and file_number

def get_piece_value_from_notation_and_position(notation: str, color: str, rank_number, file_number): """ Gets a piece's value relative to its color, notation, rank_number and file_number rank_number ranges from 0-7 with 0 => rank 8 and 7 => rank 1 file_number ranges from 0-7 with 0 => file A and 7 => file H """ position_value = Piece.piece_square_tables[color][notation.lower()][rank_number][file_number] # negating the value obtained from the piece squares table if the piece is black # position_value = -position_value if color == "b" else position_value piece_value = Piece.colors_notations_and_values[color][notation.lower()] return position_value + piece_value 
Enter fullscreen mode Exit fullscreen mode

After laying the groundwork in out Piece class, we will create an evaluate_board method in our new Player class that actually does the board evaluation. Open the ai/players.py file and type the following code under the AIPlayer class

class PlayerWithEvaluation(AIPlayer): def evaluate_board(self, board: chess.Board=None) -> int: if board is None: board = self.board regex = re.compile("\w") string = board.__str__() material_sum = 0 ranks = [ row.split(' ') for row in string.split('\n')] for i, rank in enumerate(ranks): for j, notation in enumerate(rank): if regex.search(notation): piece_color = Piece.get_piece_color_based_on_notation(notation) piece_positional_value = Piece.get_piece_value_from_notation_and_position(notation, piece_color, i, j) material_sum += piece_positional_value return material_sum def choose_move(self, board: chess.Board=None): """ Chooses the move that results in the highest material gain for the player """ legal_moves = self.get_legal_moves() chosen_move = None minimum_evaluation = None maximum_evaluation = None for move in legal_moves: # make a move on the board without affecting it fake_board = self.false_move(move) evaluation_after = self.evaluate_board(fake_board) if chosen_move is None: chosen_move = move if minimum_evaluation is None: minimum_evaluation = evaluation_after if maximum_evaluation is None: maximum_evaluation = evaluation_after else: # if the player is white and the move results in a more positive score if evaluation_after > maximum_evaluation and self.color == "w": chosen_move = move # if the player is black and the move results in a more negative score elif evaluation_after < minimum_evaluation and self.color == "b": chosen_move = move return chosen_move 
Enter fullscreen mode Exit fullscreen mode

It is important to note that a positive score is favorable for white whereas a negative score is favorable for black. So the player will always go for the move that tilts the evaluation in its favor.

Now to test out this new player, simply go to the main.py file and change the line that contains:

players = { True: "user", False: ai_players.AIPlayer(board, "b") } 
Enter fullscreen mode Exit fullscreen mode

To

players = { True: "user", False: ai_players.PlayerWithEvaluation(board, "b") } 
Enter fullscreen mode Exit fullscreen mode

You should be able to play with this new model after running the main.py file.

Player with evaluation

Although this player is an improvement to the previous one it still falls short in one very important aspect of the game of chess, anticipating your opponent's moves. Even from the short illustration we see that the player is losing pieces because it only plays the best move in front of it not taking into account the opponent's move.

We will solve this problem in the next article, where we will build a player using the minmax algorithm.

Top comments (1)

Collapse
 
akul2010 profile image
Akul Goel

Nice! I tried creating something similar in turtle (the python package). My code is here: github.com/Akul2010/TurtleChess