-
Notifications
You must be signed in to change notification settings - Fork 1
The Main File
The main file of this program incorporates the basic logic of the program, and is home to GUI of the program. It is responsible for handling all the user events, as well as carrying a mainloop()
function to indefinitely check for events and updating the GUI based on the events. It is responsible for updating the current GameState of the program, changing behaviour of the program based on the selected game mode and broadcasting to the user appropriate prompts whenever a player is checkmated or stalemated.
The draw_game_state()
method is there for updating the current GUI of the program based on user events.
def draw_game_state(screen: pygame.display, gs: board.GameState) -> None:
draw_squares(screen)
draw_pieces(screen, gs.board)
It calls two methods whenever it is called:
-
draw_squares()
- It is used to draw the squares visible to the user, based on values stored inConstants.py
file. It uses the valuesWIDTH
,HEIGHT
,DIMENSION
andboard_colors[][]
to be able to do so. -
draw_pieces()
- It draws the pieces on the board based on the current arrangement of all the pieces in theboard
array and placing them on appropriate squares with the piece images taken from theIMAGES
dictionary, initialized at the beginning of the program.
The Main
class is responsible for everything in the program, be it calling appropriate methods wherever necessary, and handling user events with the correct methods.
Code for the __init()__
method looks like this:
def __init__(self) -> None:
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
self.screen.fill((255, 255, 255))
self.clock = pygame.time.Clock()
self.undo = False
self.gs = board.GameState()
load_images()
This method first initializes the pygame
library, then initializes the screen where the GUI will be present, loads the images, initalizes the pygame clock which is responsible for the frame rate of the program, and most importantly, it initializes the gs
or the GameState
variable, which is responsible for handling all the internal logic of the program.
The mainloop()
method is responsible for repeatedly looking out for user interaction, and doing the necessary steps to respond to a user's actions.
def mainloop(self) -> int:
square_selected = ()
player_clicks = []
The first block of the mainloop()
function initializes the variables responsible for recording the user's clicks.
The next few blocks check for the selected GameMode
and runs the infinite while
loop accordingly. The explanation will just be for the GameMode
GameMode.PLAYER_VS_AI
, because on understanding the logic for this GameMode
, others will seem relatively easy. If any incorrect GameMode
is fed in the Constants.py
file, the program will raise
the InvalidGameModeError
, and the program gets terminated.
if self.gs.is_checkmate(self.gs.active_player()):
print(f"{self.gs.active_player()} has been checkmated!")
return 0
if self.gs.is_stalemate(self.gs.active_player()):
print(f"{self.gs.active_player()} has been stalemated!")
return 0
The above block of code runs on every iteration of the infinite while
loop and checks if any of players have been checkmated or stalemated. Once the condition is satisfied, it breaks out the infinite loop, and displays the appropriate message in the console, and the program gets terminated.
if not self.gs.white_to_move:
ai.make_move(self.gs)
Since the GameMode
is GameMode.PLAYER_VS_AI
, the AI plays as black. Thus, whenever black is to move, the ai.make_move()
method is called with the current GameState
as the parameter, and the AI does the appropriate job of making a move.
pos = pygame.mouse.get_pos()
file = pos[0] // SQ_SIZE
rank = pos[1] // SQ_SIZE
The pos
variable stores the position of the user's mouse click as rank
and file
are extracted by dividing the coordinates by the size of the squares in pixels. Note that the coordinates returned by pygame.mouse.get_pos()
do not depend on the position of the pygame
window on the screen. So it's easy to extract the exact coordinates of mouse clicks.
if square_selected == (rank, file):
square_selected = ()
player_clicks = []
reset_colors()
The above block of code filters out duplicate clicks on the same square, so that duplicate clicks act as a way to cancel the current move.
elif len(player_clicks) == 0:
if isinstance(self.gs.board[rank][file], WhiteSpace.WhiteSpace):
square_selected = ()
player_clicks = []
reset_colors()
else:
square_selected = (rank, file)
player_clicks.append(square_selected)
if (rank + file) % 2 == 0:
board_colors[rank][file] = highlight_colors[0]
else:
board_colors[rank][file] = highlight_colors[1]
# Highlight the legal moves of the selected piece.
if self.gs.board[rank][file].get_color() == self.gs.active_player():
pseudo_legal_moves = move_generator.legal_moves(self.gs.board[rank][file], self.gs.board)
lgl_moves = self.gs.legal_moves(pseudo_legal_moves).union(self.gs.special_moves(self.gs.board[rank][file]))
for i in lgl_moves:
target_rank = i.end_rank
target_file = i.end_file
if (target_rank + target_file) % 2 == 0:
board_colors[target_rank][target_file] = legal_move_colors[0]
else:
board_colors[target_rank][target_file] = legal_move_colors[1]
Firstly, the above block of code checks if the initial selected square is a WhiteSpace
(empty square), if it is a WhiteSpace
the square is not selected. If it is not a WhiteSpace
and is a square containing a piece, the selected square is higlighted according to the oddness and evenness of the pseudo_legal_moves
are generated for the piece, and then the legal_moves()
method from the GameState
class filters out the illegal moves. At last the special_moves
are generated (Castle, En-passant and Promotion) if applicable for the piece, and then added to the set
of legal_moves
with the help of union()
method. This block of code is repeated many times for every applicable unique click.
if len(player_clicks) == 2:
m = move.Move(player_clicks[0], player_clicks[1], self.gs.board)
valid = self.gs.make_move(m)
initial_rank = player_clicks[0][0]
initial_file = player_clicks[0][1]
final_rank = player_clicks[1][0]
final_file = player_clicks[1][1]
The above block of code checks if the user has clicked on two distinct squares. If so, it creates a Move
object and triggers the make_move()
function from the GameState
class passing the created Move
object. The make_move()
function returns a boolean
value, either True
or False
depicting the validity of the move. The returned boolean
value is then stored in the valid
variable which is used afterwards. Then the necessary parameters from the player_clicks
list are extracted and are used afterwards.
if valid:
player_clicks = []
square_selected = ()
reset_colors()
The make_move()
function takes care of moving the piece on the board, and returning the appropriate signal. If True
is returned, it means that the move was valid, and has been executed. All the variables and the colors of the board are reset to their default values afterwards. If the make_move()
function returned False
, it means either the move was invalid, or the player clicked on a friendly square. Afterwards the necessary checks are performed and the legal_move
generation and board highlighting are performed if applicable.
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_z:
self.gs.undo_move()
square_selected = ()
player_clicks = []
self.undo = True
The above block of code checks if the Z
key is clicked upon, which then triggers the undo_move()
function from the GameState
class. So, the Z
key acts as a way to undo your moves.
draw_game_state(self.screen, self.gs)
pygame.display.update()
self.clock.tick(MAX_FPS)
The above block of code is run at every
if __name__ == '__main__':
main = Main()
main.mainloop()
pygame.quit()
sys.exit()
The above code block checks if the file was run directly, and was not imported. Then it creates a Main()
object which is responsible for the initialization of the pygame
display. Then the mainloop()
function is called to start the game logic. Once the player quits the game, or the program returns from the mainloop()
, pygame.quit()
closes pygame
and sys.exit()
exits the interpreter.