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)
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"
}
Change it to
players = {
True: "user",
False: ai_players.AIPlayer(board, "b")
}
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()
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()
Now if you run the main.py
file you should be able to play with the AI that randomly selects moves.
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]
]
}
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 ]
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)
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
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"
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
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
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")
}
To
players = {
True: "user",
False: ai_players.PlayerWithEvaluation(board, "b")
}
You should be able to play with this new model after running the main.py
file.
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)
Nice! I tried creating something similar in turtle (the python package). My code is here: github.com/Akul2010/TurtleChess