Search code examples
pythonpython-3.xminimax

(Python) Minimax Connect 4 Algorithm Not Blocking Player Moves


(Connect 4 is a game where you drop circles and try to create 4 connected circles of your color in either vertical, horizontal, or diagonal form.)

I'm building a connect 4 minimax algorithm, and it fails to prevent a loss, but seemingly only strives to win. It only attempts to connect its own pieces while not stopping me from connecting mine.

Algorithm: (X is Player, O is Computer)

def minimax(board, depth, is_maximizing, depthLimit):
    if detect(board, ' O '):
        return 100 - depth
    elif detect(board, ' X '):
        if is_maximizing:
            return 100 - depth
        else:
            return -100
    elif boardFull(board):
        return 0
    elif depth == depthLimit + 1:
        return 0

    if is_maximizing:
        max_eval = float('-inf')
        for row in range(6):
            for column in range(7):
                if board[row][column] == '   ' and not isColumnFull(board, column):
                    board[row][column] = ' O '
                    eval = minimax(board, depth + 1, False, depthLimit)
                    board[row][column] = '   '
                    max_eval = max(max_eval, eval)
        return max_eval
    else:
        min_eval = float('inf')
        for row in range(6):
            for column in range(7):
                if board[row][column] == '   ' and not isColumnFull(board, column):
                    board[row][column] = ' X '
                    eval = minimax(board, depth + 1, True, depthLimit)
                    board[row][column] = '   '
                    min_eval = min(min_eval, eval)
        return min_eval

def isColumnFull(board, column):
    for row in range(6):
        if board[row][column] == '   ':
            return False
    return True

def computerMove(board):
    best_score = float('-inf')
    best_move = None
    
    for row in range(6):
        for column in range(7):
            print(f'{round(((row/6)*100)+(column/7)*10, 2)}% Done Calculating')
            if board[row][column] == '   ' and not isColumnFull(board, column):
                board[row][column] = ' O '
                score = minimax(board, 0, False, depth)
                board[row][column] = '   '
                if score > best_score:
                    best_score = score
                    best_move = column

    for row in range(6):
        if board[row][best_move] != '   ':
            board[row - 1][best_move] = ' O '
            break
        elif row == 5:
            board[row][best_move] = ' O '
            break

    if detect(board, ' O '):
        print('O Wins!')
        return False
    return True

'board' is essentially a matrix containing the, well, board: board = [[' ' for i in range(7)] for f in range(6)]

Example: As you can see, the minimax algorithm had a very unambiguous way of preventing its loss here.

I've also ruled out the detect() function being buggy, as it works fine for detecting a win. (detect() essentially checks if there are 4 connected pieces of the same color/player)


Solution

  • The problem looks to be here.

    def minimax(board, depth, is_maximizing, depthLimit):
        if detect(board, ' O '):
            return 100 - depth
        elif detect(board, ' X '):
            if is_maximizing:
                return 100 - depth
            else:
                return -100
    

    If O wins, it always returns a large positive. If X wins, it returns a negative or a positive, depending on whose turn it is. If we're maximizing, then it is X's turn, so we return a large positive number. In other words, the AI is indifferent between O winning and X winning on X's turn.

    The fix is to make X the opposite of O:

    def minimax(board, depth, is_maximizing, depthLimit):
        if detect(board, ' O '):
            return 100 - depth
        elif detect(board, ' X '):
            return -100 + depth
    

    After making this change, it can figure out how to block X from winning.