Connect Four Game - Human vs. AI

PythonPythonBeginner
Practice Now

Introduction

This project is a Python implementation of the classic Connect Four game where a player can compete against an AI. It employs the Pygame library for the game's interface and control. The AI's decision-making is based on the Monte Carlo tree search algorithm, and the difficulty level is adjustable, allowing players to challenge themselves with smarter AI opponents.

Key Concepts:

  • Utilizing Pygame for game development.
  • Implementing the Monte Carlo tree search algorithm for AI decision-making.

👀 Preview

Connect Four Game

🎯 Tasks

In this project, you will learn:

  • How to build a game using Pygame
  • How to implement the Monte Carlo tree search algorithm for AI decision-making
  • How to customize and enhance the AI's difficulty level
  • How to create a fun and interactive Connect Four game for human vs. AI battles

🏆 Achievements

After completing this project, you will be able to:

  • Develop games using Python and Pygame
  • Understand the principles of the Monte Carlo tree search algorithm
  • Tune the difficulty of an AI opponent to create a challenging gaming experience
  • Enhance user interfaces to make the gaming experience more engaging

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("`Python`")) -.-> python/BasicConceptsGroup(["`Basic Concepts`"]) python(("`Python`")) -.-> python/FileHandlingGroup(["`File Handling`"]) pygame(("`Pygame`")) -.-> pygame/CoreConceptsGroup(["`Core Concepts`"]) pygame(("`Pygame`")) -.-> pygame/AdvancedGraphicsGroup(["`Advanced Graphics`"]) python(("`Python`")) -.-> python/ControlFlowGroup(["`Control Flow`"]) python(("`Python`")) -.-> python/DataStructuresGroup(["`Data Structures`"]) python(("`Python`")) -.-> python/FunctionsGroup(["`Functions`"]) python(("`Python`")) -.-> python/ModulesandPackagesGroup(["`Modules and Packages`"]) python(("`Python`")) -.-> python/ErrorandExceptionHandlingGroup(["`Error and Exception Handling`"]) python(("`Python`")) -.-> python/AdvancedTopicsGroup(["`Advanced Topics`"]) python(("`Python`")) -.-> python/PythonStandardLibraryGroup(["`Python Standard Library`"]) python/BasicConceptsGroup -.-> python/comments("`Comments`") python/FileHandlingGroup -.-> python/with_statement("`Using with Statement`") pygame/CoreConceptsGroup -.-> pygame/display("`Display Management`") pygame/CoreConceptsGroup -.-> pygame/event("`Event Handling`") pygame/CoreConceptsGroup -.-> pygame/image("`Image Processing`") pygame/CoreConceptsGroup -.-> pygame/time("`Timing Functions`") pygame/AdvancedGraphicsGroup -.-> pygame/transform("`Image Transformations`") python/BasicConceptsGroup -.-> python/variables_data_types("`Variables and Data Types`") python/BasicConceptsGroup -.-> python/numeric_types("`Numeric Types`") python/BasicConceptsGroup -.-> python/booleans("`Booleans`") python/BasicConceptsGroup -.-> python/type_conversion("`Type Conversion`") python/ControlFlowGroup -.-> python/conditional_statements("`Conditional Statements`") python/ControlFlowGroup -.-> python/for_loops("`For Loops`") python/ControlFlowGroup -.-> python/while_loops("`While Loops`") python/ControlFlowGroup -.-> python/break_continue("`Break and Continue`") python/DataStructuresGroup -.-> python/lists("`Lists`") python/DataStructuresGroup -.-> python/tuples("`Tuples`") python/DataStructuresGroup -.-> python/dictionaries("`Dictionaries`") python/FunctionsGroup -.-> python/function_definition("`Function Definition`") python/FunctionsGroup -.-> python/default_arguments("`Default Arguments`") python/FunctionsGroup -.-> python/scope("`Scope`") python/ModulesandPackagesGroup -.-> python/importing_modules("`Importing Modules`") python/ModulesandPackagesGroup -.-> python/using_packages("`Using Packages`") python/ModulesandPackagesGroup -.-> python/standard_libraries("`Common Standard Libraries`") python/ErrorandExceptionHandlingGroup -.-> python/custom_exceptions("`Custom Exceptions`") python/AdvancedTopicsGroup -.-> python/iterators("`Iterators`") python/PythonStandardLibraryGroup -.-> python/math_random("`Math and Random`") python/PythonStandardLibraryGroup -.-> python/data_collections("`Data Collections`") python/PythonStandardLibraryGroup -.-> python/os_system("`Operating System and System`") python/FunctionsGroup -.-> python/build_in_functions("`Build-in Functions`") subgraph Lab Skills python/comments -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/with_statement -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} pygame/display -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} pygame/event -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} pygame/image -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} pygame/time -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} pygame/transform -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/variables_data_types -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/numeric_types -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/booleans -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/type_conversion -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/conditional_statements -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/for_loops -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/while_loops -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/break_continue -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/lists -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/tuples -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/dictionaries -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/function_definition -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/default_arguments -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/scope -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/importing_modules -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/using_packages -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/standard_libraries -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/custom_exceptions -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/iterators -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/math_random -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/data_collections -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/os_system -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} python/build_in_functions -.-> lab-298858{{"`Connect Four Game - Human vs. AI`"}} end

Development Preparation

The Four-In-A-Row game is played on a grid of size 7*6. Players take turns to drop their pieces from the top of a column. The piece will fall into the bottommost empty space in that column. The player who connects four pieces in a straight line (horizontal, vertical, or diagonal) wins the game.

Create a file named fourinrow.py in the ~/project directory to store the code for this project. Additionally, we need to install the Pygame library to implement the game's interface and support operations.

cd ~/project
touch fourinrow.py
sudo pip install pygame

You can find the required image resources for this project in the ~/project/images directory.

To better understand the code in this project, it is recommended that you study it alongside the complete solution's code.

Initializing Variables

The variables used include the width and height of the chessboard (can be modified to design chessboards of different sizes), the difficulty level, the size of the chess pieces, and the setting of some coordinate variables.

In the fourinrow.py file, enter the following code:

import random, copy, sys, pygame
from pygame.locals import *

BOARDWIDTH = 7  ## Number of columns on the game board
BOARDHEIGHT = 6 ## Number of rows on the game board
assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.'

## The python assert statement is used to declare that its given boolean expression must be true.
## If the expression is false, it raises an exception.

DIFFICULTY = 2 ## Difficulty level, number of moves the computer can consider
               ## Here, 2 means considering 7 possible moves of the opponent and how to respond to those 7 moves

SPACESIZE = 50 ## Size of the chess pieces

FPS = 30 ## Screen refresh rate, 30/s
WINDOWWIDTH = 640  ## Width of the game screen in pixels
WINDOWHEIGHT = 480 ## Height of the game screen in pixels

XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2)  ## X-coordinate of the left edge of the grid
YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2) ## Y-coordinate of the top edge of the grid
BRIGHTBLUE = (0, 50, 255) ## Blue color
WHITE = (255, 255, 255) ## White color

BGCOLOR = BRIGHTBLUE
TEXTCOLOR = WHITE

RED = 'red'
BLACK = 'black'
EMPTY = None
HUMAN = 'human'
COMPUTER = 'computer'

In addition, we also need to define some global variables of pygame. These global variables will be called multiple times in various modules later. Many of them are variables that store loaded images, so the preparation work is a bit long, please be patient.

## Initialize pygame modules
pygame.init()

## Create a Clock object
FPSCLOCK = pygame.time.Clock()

## Create the game window
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

## Set the game window title
pygame.display.set_caption(u'four in row')

## Rect(left, top, width, height) is used to define position and size
REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

## Create the bottom left and bottom right chess pieces in the window
BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

## Load the red chess piece image
REDTOKENIMG = pygame.image.load('images/4rowred.png')

## Scale the red chess piece image to SPACESIZE
REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE))

## Load the black chess piece image
BLACKTOKENIMG = pygame.image.load('images/4rowblack.png')

## Scale the black chess piece image to SPACESIZE
BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE))

## Load the chessboard image
BOARDIMG = pygame.image.load('images/4rowboard.png')

## Scale the chessboard image to SPACESIZE
BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE))

## Load the human winner image
HUMANWINNERIMG = pygame.image.load('images/4rowhumanwinner.png')

## Load the AI winner image
COMPUTERWINNERIMG = pygame.image.load('images/4rowcomputerwinner.png')

## Load the tie image
TIEWINNERIMG = pygame.image.load('images/4rowtie.png')

## Return a Rect object
WINNERRECT = HUMANWINNERIMG.get_rect()

## Center the winner image on the game window
WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

## Load the arrow image for user instructions
ARROWIMG = pygame.image.load('images/4rowarrow.png')

## Return a Rect object
ARROWRECT = ARROWIMG.get_rect()

## Set the left position of the arrow image
ARROWRECT.left = REDPILERECT.right + 10

## Align the arrow image vertically with the red chess piece below it
ARROWRECT.centery = REDPILERECT.centery

To better understand the code in this project, it is recommended that you study it alongside the complete solution's code.

Board Design

Initially, clear the two-dimensional list representing the board, and then set the corresponding positions on the board with colors based on the moves of the player and AI.

def drawBoard(board, extraToken=None):
    ## DISPLAYSURF is our interface, defined in the variable initialization module.
    DISPLAYSURF.fill(BGCOLOR) ## Fill the game window background color with blue.
    spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE) ## Create a Rect instance.
    for x in range(BOARDWIDTH):
        ## Determine the top-left position coordinates of each cell in each row of each column.
        for y in range(BOARDHEIGHT):
            spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))

            ## When x = 0 and y = 0, it is the first cell in the first row of the first column.
            if board[x][y] == RED: ## If the cell value is red,
                ## draw a red token in the game window within spaceRect.
                DISPLAYSURF.blit(REDTOKENIMG, spaceRect)
            elif board[x][y] == BLACK: ## Otherwise, draw a black token.
                DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect)

    ## extraToken is a variable that contains position information and color information.
    ## It is used to display a specified token.
    if extraToken != None:
        if extraToken['color'] == RED:
            DISPLAYSURF.blit(REDTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))
        elif extraToken['color'] == BLACK:
            DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))

    ## Draw the token panels.
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))
            DISPLAYSURF.blit(BOARDIMG, spaceRect)

    ## Draw the tokens at the bottom left and bottom right of the game window.
    DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) ## Left red token.
    DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) ## Right black token.


def getNewBoard():
    board = []
    for x in range(BOARDWIDTH):
        board.append([EMPTY] * BOARDHEIGHT)
    return board ## Return the board list with BOARDHEIGHT number of None values.

In above code, the drawBoard() function draws the board and the tokens on the board. The getNewBoard() function returns a new board data structure.

AI Algorithm for Optimal Movement

Briefly explain the idea of Monte Carlo tree search:

Use the one-dimensional Monte Carlo method to evaluate the Go game board. Specifically, when a specific chessboard situation is given, the program randomly selects a point from all the available points in the current situation and places a chess piece on it. This random selection of available points (roll point) process is repeated until neither side has any available points (the game ends), and then the resulting win or loss of this final state is fed back as the basis for evaluating the current situation.

In this project, the AI continuously chooses different columns and evaluates the results of both sides' victories. The AI will ultimately choose a strategy with a higher evaluation.

Before looking at the pictures and text below, please take a look at the code at the end, and then refer to the corresponding explanations.

Observing the confrontation between AI and player in the figure below:

4.5-1

Some variables in the project can intuitively reflect the process of AI's chess piece operations:

PotentialMoves: Returns a list that represents the possibility of AI winning when moving a chess piece to any column in the list. The values are random numbers from -1 to 1. When the value is negative, it means that the player may win in the next two moves, and the smaller the value, the greater the possibility of the player winning. If the value is 0, it means that the player will not win, and AI will also not win. If the value is 1, it means that AI can win.

bestMoveFitness: Fitness is the maximum value selected from PotentialMoves.

bestMoves: If there are multiple maximum values in PotentialMoves, it means that the player's chances of winning are the smallest when AI moves the chess piece to the columns where these values are located. Therefore, these columns are added to the list bestMoves.

column: When there are multiple values in bestMoves, randomly select one column from bestMoves as AI's movement. If there is only one value, column is this unique value.

In the project, by printing these bestMoveFitness, bestMoves, column, and potentialMoves, we can deduce the parameters of AI's each step in the above figure.

AI moves

steps potentialMoves bestMoveFitness bestMoves column
1 [0, 0, 0, 0, 0, 0, 0] 0 [0, 1, 2, 3, 4, 5, 6] 0
2 [0, 0, 0, 0, 0, 0, 0] 0 [0, 1, 2, 3, 4, 5, 6] 6
3 [-0.12, -0.12, -0.12, 0, -0.12, -0.12, -0.12] 0 [3] 3
4 [-0.34, -0.22, 0, -0.34, -0.34, -0.22, -0.34] 0 [2] 2
4.5-2

By examining AI's selection in the third step, we can have a better understanding of the algorithm:

The figure below illustrates some of the AI's moves, showing the possible choices for the player if AI places a piece in the first column, and the impact of AI's next move on the player's chances of winning. Through this search and iteration process, the AI can determine the winning situations for both the opponent and itself in the next two steps, and make decisions accordingly.

4.5-3

The figure below is a flowchart of calculating the fitness value for AI. In this project, the difficulty coefficient is 2, and we need to consider 7^4=2041 cases:

4.5-4

From the above flowchart, it is not difficult to find that if AI places its first piece in column 0, 1, 2, 4, 5, or 6, the player can always place the remaining two pieces in column 3 and win. For ease of expression, we use a sequence to represent various combinations, where the first element represents AI's first move, the second number represents the player's response, and the third number represents AI's response. "X" represents any valid move. Therefore, [0,0,x]=0, and it can be deduced that when the sequence is [0,x<>3,x], the player cannot win. Only when the player's second piece is in column 3, and AI's second move is not in column 3, can AI win. Therefore, [0,x=3,x<>3] = -1, and there are 6 such cases. The final result is (0+0+...(43 times)-1*6)/7/7 = -0.12.

By the same reasoning, the results for the other four cases are all -0.12. If AI's first move is in column 3, the player cannot win, and AI cannot win either, so the value is 0. AI chooses the move with the highest fitness value, which means it will place its piece in column 3.

The same analysis can be applied to AI's subsequent moves. In summary, the higher the possibility of the player winning after AI's move, the lower the fitness value for AI, and AI will choose the move with a higher fitness value to prevent the player from winning. Of course, if AI can win itself, it will prioritize the move that leads to its own victory.

def getPotentialMoves(board, tile, lookAhead):
    if lookAhead == 0 or isBoardFull(board):
        '''
        If the difficulty coefficient is 0 or the board is full,
        return a list with all values set to 0. This means that
        the fitness value is equal to the potential moves for each column.
        In this case, AI will drop the piece randomly and lose its intelligence.
        '''
        return [0] * BOARDWIDTH

    ## Determine the color of the opponent's piece
    if tile == RED:
        enemyTile = BLACK
    else:
        enemyTile = RED
    potentialMoves = [0] * BOARDWIDTH
    ## Initialize a list of potential moves, with all values set to 0
    for firstMove in range(BOARDWIDTH):
        ## Iterate over each column and consider any move by either side as the firstMove
        ## The move by the other side is then considered as the counterMove
        ## Here, our firstMove refers to AI's move and the opponent's move is considered as counterMove
        ## Take a deep copy of the board to prevent mutual influence between board and dupeBoard
        dupeBoard = copy.deepcopy(board)
        if not isValidMove(dupeBoard, firstMove):
        ## If the move of placing a black piece in the column specified by firstMove is invalid in dupeBoard
            continue
            ## Continue to the next firstMove
        makeMove(dupeBoard, tile, firstMove)
        ## If it is a valid move, set the corresponding grid color
        if isWinner(dupeBoard, tile):
        ## If AI wins
            potentialMoves[firstMove] = 1
            ## The winning piece automatically gets a high value to indicate its chances of winning
            ## The larger the value, the higher the chances of winning, and the lower the chances of the opponent winning
            break
            ## Do not interfere with the calculation of other moves
        else:
            if isBoardFull(dupeBoard):
            ## If there are no empty grids in dupeBoard
                potentialMoves[firstMove] = 0
                ## It is not possible to move
            else:
                for counterMove in range(BOARDWIDTH):
                ## Consider the opponent's move
                    dupeBoard2 = copy.deepcopy(dupeBoard)
                    if not isValidMove(dupeBoard2, counterMove):
                        continue
                    makeMove(dupeBoard2, enemyTile, counterMove)
                    if isWinner(dupeBoard2, enemyTile):
                        potentialMoves[firstMove] = -1
                        ## If the player wins, the fitness value for AI in this column is the lowest
                        break
                    else:
                        ## Recursively call getPotentialMoves
                        results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1)
                        ## Use floating-point representation here for more accurate results
                        ## This ensures that the values in potentialMoves are within the range [-1, 1]
                        potentialMoves[firstMove] += (sum(results)*1.0 / BOARDWIDTH) / BOARDWIDTH
    return potentialMoves

Player Operation

Drag the chess piece, determine the square where the chess piece is located, validate the chess piece, call the chess piece drop function, and complete the operation.

def getHumanMove(board, isFirstMove):
    draggingToken = False
    tokenx, tokeny = None, None
    while True:
        ## Use pygame.event.get() to handle all events
        for event in pygame.event.get():
            if event.type == QUIT: ## Stop and exit
                pygame.quit()
                sys.exit()

            elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos):
                ## If the event type is mouse button down, draggingToken is True, and the mouse click position is inside REDPILERECT
                draggingToken = True
                tokenx, tokeny = event.pos

            elif event.type == MOUSEMOTION and draggingToken: ## If the red piece is dragged
                tokenx, tokeny = event.pos ## Update the position of the dragged piece

            elif event.type == MOUSEBUTTONUP and draggingToken:
                ## If the mouse is released, and the chess piece is dragged
                ## If the chess piece is dragged directly above the board
                if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN:
                    column = int((tokenx - XMARGIN) / SPACESIZE) ## Determine the column where the chess piece will drop based on the x coordinate of the chess piece (0,1...6)
                    if isValidMove(board, column): ## If the chess piece move is valid
                        """
                        Drop into the corresponding empty square,
                        This function only shows the dropping effect
                        The chess piece filling the square can also be achieved without this function by the following code
                        """
                        animateDroppingToken(board, column, RED)

                        ## Set the bottom most square in the empty column to red
                        board[column][getLowestEmptySpace(board, column)] = RED
                        drawBoard(board) ## Draw the red chess piece in the dropped square
                        pygame.display.update() ## Window update
                        return
                tokenx, tokeny = None, None
                draggingToken = False

        if tokenx != None and tokeny != None: ## If a chess piece is dragged, display the dragged chess piece
            drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED})
            ## Adjust the x, y coordinates so that the mouse is always at the center position of the chess piece during dragging

        else:
            drawBoard(board) ## When it is an invalid move, after the mouse is released, because all the values in the board are none
            ## When calling drawBoard, the operations performed are to display the two chess pieces below, which is equivalent to returning the chess piece to the location where it started dragging

        if isFirstMove:
            DISPLAYSURF.blit(ARROWIMG, ARROWRECT) ## AI moves first, display the hint operation image

        pygame.display.update()
        FPSCLOCK.tick()

In above code, the getHumanMove() function handles the player's move. The animateDroppingToken() function animates the dropping of the token. The getLowestEmptySpace() function returns the lowest empty space in a column.

AI Operations

Implement the function to animate the computer's movement and landing of AI pieces in the respective positions.

def animateComputerMoving(board, column):
    x = BLACKPILERECT.left ## The left coordinate of the black piece at the bottom
    y = BLACKPILERECT.top ## The top coordinate of the black piece at the bottom
    speed = 1.0
    while y > (YMARGIN - SPACESIZE): ## When y has a larger value, indicating that the piece is below the window
        y -= int(speed) ## Decrease y continuously, which means the piece moves up
        speed += 0.5 ## Increase the speed at which y decreases
        drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
        ## y keeps changing, continuously drawing the black piece, creating an effect of continuous ascent
        pygame.display.update()
        FPSCLOCK.tick()
    ## When the piece ascends to the top of the board
    y = YMARGIN - SPACESIZE ## Reset y, so that the bottom of the piece is aligned with the top of the board
    speed = 1.0
    while x > (XMARGIN + column * SPACESIZE): ## When x is greater than the x coordinate of the desired column
        x -= int(speed) ## Decrease x continuously, which means the piece moves to the left
        speed += 0.5
        drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
        ## At this point, the y coordinate remains unchanged, which means the piece moves horizontally to the column
        pygame.display.update()
        FPSCLOCK.tick()
    ## The black piece lands on the calculated empty space
    animateDroppingToken(board, column, BLACK)

Select the highest number from the list of potentialMoves returned, as the fitness value, and randomly choose from those columns with high fitness values as the final movement target.

def getComputerMove(board):
    potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY) ## Potential moves, a list with BOARDWIDTH values
               ## The values in the list are related to the difficulty level set
    bestMoves = [] ## Create an empty bestMoves list
    bestMoveFitness = -1 ## Since the minimum value in potentialMoves is -1, it serves as the lower limit
    print(bestMoveFitness)
    for i in range(len(potentialMoves)):
        if potentialMoves[i] > bestMoveFitness and isValidMove(board, i):
            bestMoveFitness = potentialMoves[i] ## Continuously update bestMoves, so that each value in bestMoves is the largest
            ## while ensuring that the move is valid.

    for i in range(len(potentialMoves)):
        if potentialMoves[i] == bestMoveFitness and isValidMove(board, i):
            bestMoves.append(i) ## List all the columns where the piece can be moved to. This list may be empty, contain
            ## only one value, or multiple values.
    print(bestMoves)
    return random.choice(bestMoves) ## Randomly choose one of the columns where the piece can be moved to as the target move.

Piece Movement Operation

By continuously changing the corresponding coordinates of the pieces, achieve the animation effect of falling.

def getLowestEmptySpace(board, column):
    ## Return the lowest empty space in a column
    for y in range(BOARDHEIGHT-1, -1, -1):
        if board[column][y] == EMPTY:
            return y
    return -1

def makeMove(board, player, column):
    lowest = getLowestEmptySpace(board, column)
    if lowest != -1:
        board[column][lowest] = player
        '''
        Assign the player (red/black) to the lowest empty space in the column.
        Because the piece is dropped in the lowest empty space in a column,
        it is considered as the color of that space.
        '''

def animateDroppingToken(board, column, color):
    x = XMARGIN + column * SPACESIZE
    y = YMARGIN - SPACESIZE
    dropSpeed = 1.0
    lowestEmptySpace = getLowestEmptySpace(board, column)

    while True:
        y += int(dropSpeed)
        dropSpeed += 0.5
        if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace:
            return
        drawBoard(board, {'x':x, 'y':y, 'color':color})
        pygame.display.update()
        FPSCLOCK.tick()

In above code, the makeMove() function makes a move on the board. The animateDroppingToken() function animates the dropping of the token. The getLowestEmptySpace() function returns the lowest empty space in a column.

Some judging functions

Judge the validity of a piece's move, judge if there are still empty spaces on the chessboard.

def isValidMove(board, column):
    ## Judge the validity of a piece's move
    if column < 0 or column >= (BOARDWIDTH) or board[column][0] != EMPTY:
    ## If the column is less than 0 or greater than BOARDWIDTH, or there is no empty space in the column
        return False
        ## Then it is an invalid move, otherwise it is valid
    return True


def isBoardFull(board):
    ## If there are no empty spaces in the grid, return True
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == EMPTY:
                return False
    return True

In above code, the isValidMove() function returns True if the move is valid. The isBoardFull() function returns True if the board is full.

Winning Conditions Judgment

Several diagrams are provided for easy understanding of the four winning conditions. The positions shown in the diagram correspond to the extreme values ​​of x and y.

4.10-1
def isWinner(board, tile):
    ## Check for horizontal situation of pieces
    for x in range(BOARDWIDTH - 3): ## x takes on the values 0, 1, 2, 3
        for y in range(BOARDHEIGHT): ## iterate through all rows
            ## If x = 0, check if the first four pieces in the yth row are all the same tile. This can be used to traverse all horizontal situations of pieces connecting four in a row. If any x, y is true, it can be determined as a win
            if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile:
                return True

    ## Check for vertical situation of pieces, similar to the horizontal situation
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT - 3):
            if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile:
                return True

    ## Check for left-leaning diagonal situation of pieces
    for x in range(BOARDWIDTH - 3): ## x takes on the values 0, 1, 2, 3
        for y in range(3, BOARDHEIGHT): ## because when forming a left-leaning diagonal four in a row, the bottom-most piece must be at least four squares away from the top, i.e. y >= 3
            if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile: ## determine if the left-leaning diagonal four pieces are the same color
                return True

    ## Check for right-leaning diagonal situation of pieces, similar to the left-leaning diagonal situation
    for x in range(BOARDWIDTH - 3):
        for y in range(BOARDHEIGHT - 3):
            if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile:
                return True
    return False

Creating the Game Main Loop

Finally, we create the game main loop to keep the game running continuously.

def main():

    ## Exsiting code omitted

    isFirstGame = True ## Initialize isFirstGame

    while True: ## Keep the game running continuously
        runGame(isFirstGame)
        isFirstGame = False


def runGame(isFirstGame):
    if isFirstGame:
        ## At the start of the first game
        ## Let the AI make the first move so that players can watch how the game is played
        turn = COMPUTER
        showHelp = True
    else:
        ## For the second game and onwards, assign turns randomly
        if random.randint(0, 1) == 0:
            turn = COMPUTER
        else:
            turn = HUMAN
        showHelp = False
    mainBoard = getNewBoard() ## Set up the initial empty board structure
    while True: ## Game main loop
        if turn == HUMAN: ## If it's the player's turn

            getHumanMove(mainBoard, showHelp) ## Call the method for player's move, see getHumanMove method for details
            if showHelp:
                ## If there's a hint image, turn off the hint after AI makes the first move
                showHelp = False
            if isWinner(mainBoard, RED): ## If red chip (player) wins
                winnerImg = HUMANWINNERIMG ## Load the player winning image
                break ## Exit the loop
            turn = COMPUTER ## Hand over the first move to the AI
        else:
            ## If it's the AI's turn
            column = getComputerMove(mainBoard) ## Call the method for AI's move, see getComputerMove method for details
            print(column)
            animateComputerMoving(mainBoard, column) ## Move the black chip
            makeMove(mainBoard, BLACK, column) ## Set the bottom-most empty slot in the column as black
            if isWinner(mainBoard, BLACK):
                winnerImg = COMPUTERWINNERIMG
                break
            turn = HUMAN ## Switch to the player's turn

        if isBoardFull(mainBoard):
            ## If the board is full, it's a tie
            winnerImg = TIEWINNERIMG
            break

Running and Testing

Next, we will run the program and see how it performs.

cd ~/project
python fourinrow.py

Summary

Based on the Monte Carlo algorithm, this project implemented a human-vs-AI chess game using Python with the Pygame module. The project allowed us to become familiar with the basics of creating instances and moving objects in Pygame, and also gave us a preliminary understanding of the specific application of the Monte Carlo algorithm.

Other Python Tutorials you may like