Skip to content

The Main File

Aayush Shukla edited this page Apr 24, 2023 · 8 revisions

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.

Drawing the game state

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 in Constants.py file. It uses the values WIDTH, HEIGHT, DIMENSION and board_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 the board array and placing them on appropriate squares with the piece images taken from the IMAGES dictionary, initialized at the beginning of the program.

The Main class

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.

The init method

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

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 $(x-coordinate, y-coordinate)$, and then individual values for 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 ${rank + file}$ quantity. Then 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 ${1/60}^{th}$ second and updates the current game display accordingly and if any event is triggered, the necessary updates are pushed to the display with the help of this block.

The name-main idiom

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.