简体   繁体   中英

Problem with recursion while implementing a minimax algorithm

I'm trying to implement a minimax algorithm to create a bot that plays tic-tac-toe vs a player. The gui features are in another file and is working fine. Whenever it is the bot's turn to make a move the gui file calls the file with the code mentioned below. I've included what each function does with a comment and I believe all of the function except for the minimax() works Whenever I run the script this error is displayed: "RecursionError: maximum recursion depth exceeded in comparison"

If something is not clear do comment I will try my best to simplify it. Thank You for your help

X = "X"
O = "O"
EMPTY = None


def initial_state():
    """
    Returns starting state of the board.
    """
    return [[EMPTY, EMPTY, EMPTY],
            [EMPTY, EMPTY, EMPTY],
            [EMPTY, EMPTY, EMPTY]]


def player(board):
    """
    Returns player who has the next turn on a board.
    """
    o_counter = 0
    x_counter = 0
    for i in board:
        for j in i:
            if j == 'X':
                x_counter += 1
            elif j == 'O':
                o_counter += 1
    if x_counter == 0 and o_counter == 0:
        return 'O'
    elif x_counter > o_counter:
        return 'O'
    elif o_counter > x_counter:
        return 'X'



def actions(board):
    """
    Returns set of all possible actions (i, j) available on the board.
    """
    action = []
    for i in range(3):
        for j in range(3):
            if board[i][j] is None:
                action.append([i, j])
    return action


def result(board, action):
    """
    Returns the board that results from making move (i, j) on the board.
    """
    p = player(board)
    i, j = action
    board[i][j] = p
    return board


def winner(board):
    """
    Returns the winner of the game, if there is one.
    """
    if board[0][0] == board[1][1] == board[2][2]:
        return board[0][0]
    elif board[0][2] == board[1][1] == board[2][0]:
        return board[0][2]
    else:
        for i in range(3):
            if board[i][0] == board[i][1] == board[i][2]:
                return board[i][0]
            elif board[0][i] == board[1][i] == board[2][i]:
                return board[0][i]

def terminal(board):
    """
    Returns True if game is over, False otherwise.
    """
    if winner(board) == 'X' or winner(board) == 'O' :
        return True
    else:
        return False


def utility(board):
    """
    Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
    """
    if winner(board) == 'X':
        return 1
    elif winner(board) == 'O':
        return -1
    else:
        return 0


def minimax(board):
    """
    Returns the optimal action for the current player on the board.
    """
    return_action = [0, 0]
    available_actions = actions(board)
    score = 0
    temp_board = board
    for action in range(len(available_actions)):
        temp_score = 0
        i, j = available_actions[action]
        temp_board[i][j] = player(temp_board)
        if winner(temp_board) == 'X' or winner(temp_board) == 'O':
            temp_score += utility(temp_board)
        else:
            minimax(temp_board)
        if temp_score > score:
            score = temp_score
            return_action = action

    return available_actions[return_action]

Let's consider the minimax algorithm itself, since the rest seems fine:

def minimax(board):
    """
    Returns the optimal action for the current player on the board.
    """
    return_action = [0, 0]
    available_actions = actions(board)
    score = 0
    temp_board = board
    for action in range(len(available_actions)):
        temp_score = 0
        i, j = available_actions[action]
        temp_board[i][j] = player(temp_board)
        if winner(temp_board) == 'X' or winner(temp_board) == 'O':
            temp_score += utility(temp_board)
        else:
            minimax(temp_board)
        if temp_score > score:
            score = temp_score
            return_action = action

    return available_actions[return_action]

There are multiple problems here.

  • temp_board = board does not make a copy; it simply creates a new local name for the same board. As a result, trial moves do not get "erased" as you return from the recursion.

  • It is possible that there are no available_actions (remember that drawn games are possible.), This would mean that the for loop doesn't run, and the last return will try to index into the available_actions - an empty list - with an invalid value (everything would be invalid here, but the initial setting of [0, 0] especially doesn't make sense, as it's not an integer).

  • There is nothing to actually cause the minimax algorithm to alternate min and max. The comparison performed is if temp_score > score: , regardless of which player's move is being considered. That's, er, maximax, and doesn't give you a useful strategy.

  • Most importantly: your recursive call doesn't provide any information to the caller . When you recursively call minimax(temp_board) , you would like to know what the score is on that board. So your overall function needs to return a score as well as the proposed move, and when you make the recursive call, you need to consider that information. (You can ignore the proposed move on the temporary board, since it just tells you what the algorithm expects the player to answer with; but you need the score so that you can determine if this move is a winning one.)

We can also clean up a lot of things:

  • There is no good reason to initialize temp_score = 0 , since we will get an answer either from the recursion or from noticing that the game is over. temp_score += utility(temp_board) also doesn't make sense; we aren't totaling up values, but just using a single one.

  • We can clean up the utility function to account for the possibility of drawn games, and also to generate candidate moves. This gives us a neat way to encapsulate the logic of "if the game has been won, don't consider any moves on the board, even though there are empty spaces".

  • Instead of looping with a for loop and performing a comparison, we can use the built-in min and max functions on a sequence of the recursive results - which we can get by using a generator expression (a neat Python idiom that you will see in a lot of more advanced code). That also gives us a neat way to ensure that min and max stages of the algorithm are alternated: we just pass the appropriate function to the next level of the recursion.


Here's my not-tested attempt:

def score_and_candidates(board):
    # your 'utility', extended to include candidates.
    if winner(board) == 'X':
        return 1, ()
    if winner(board) == 'O':
        return -1, ()
    # If the game is a draw, there will be no actions, and a score of 0
    # is appropriate. Otherwise, the minimax algorithm will have to refine
    # this result.
    return 0, actions(board)

def with_move(board, player, move):
    # Make a deep copy of the board, but with the indicated move made.
    result = [row.copy() for row in board]
    result[move[0]][move[1]] = player
    return result

def try_move(board, player, move):
    next_player = 'X' if player == 'O' else 'O'
    next_board = with_move(board, player, move)
    next_score, next_move = minimax(next_board, next_player)
    # We ignore the move suggested in the recursive call, and "tag" the
    # score from the recursion with the current move. That way, the `min`
    # and `max` functions will sort the tuples by score first, and the
    # chosen tuple will have the `move` that lead to the best line of play.
    return next_score, move

def minimax(board, player):
    score, candidates = score_and_candidates(board)
    if not candidates:
        # The game is over at this node of the search
        # We report the status, and suggest no move.
        return score, None
    # Otherwise, we need to recurse.
    # Since the logic is a bit tricky, I made a separate function to
    # set up the recursive calls, and then we can use either `min` or `max`
    # to combine the results.
    min_or_max = min if player == 'O' else max
    return min_or_max(
        try_move(board, player, move)
        for move in candidates
    )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM