Home > database >  Infinite recursive call minimax algorithm
Infinite recursive call minimax algorithm

Time:02-11

I have recently implemented the code for a 4X4 tic tac toe game which is using minimax algorithm. However, my minimax function is calling itself recursively infinite no. of times.

Initial Board (4X4) tic tac toe ->

board = np.array([['_','_','_', '_'],['_','_','_', '_'],['_','_','_', '_'],['_','_','_', '_']])

Code for the computer's turn ->

import math
from utility import *

def minimax(board, spotsLeft, maximizing):
    bestIdx = (0,0)
    p1 = 'X'
    p2 = 'O'
    res, stat = checkGameOver(board, spotsLeft)
    if res==True:
        if stat == 'X': return 17-spotsLeft, bestIdx
        elif stat == 'O': return -17 spotsLeft, bestIdx
        else: return 0, bestIdx
    
    if maximizing:
        # return max score
        bestMove = -math.inf
        for i in range(4):
            for j in range(4):
                if board[i][j] == '_':
                    board[i][j] = p1
                    score, idx = minimax(board, spotsLeft-1, False)
                    print(score, idx)
                    board[i][j] = '_'
                    if bestMove<score:
                        bestMove = score
                        bestIdx = (i,j)
        return bestMove, bestIdx
    else:
        # return min score
        bestMove = math.inf
        for i in range(4):
            for j in range(4):
                if board[i][j] == '_':
                    board[i][j] = p2
                    score, idx = minimax(board, spotsLeft-1, True)
                    print(score, idx)
                    board[i][j] = '_'
                    if bestMove>score:
                        bestMove = score
                        bestIdx = (i,j)
        return bestMove, bestIdx
    

def ai(board):
    spotsLeft = 16
    p1 = 'X'    # Computer
    p2 = 'O'    # Player
    turn = p1
    while True:
        res, stat = checkGameOver(board, spotsLeft)
        if res==False:
            if turn == p1:
                # AI
                boardCopy = board[:]
                score, idx = minimax(boardCopy, spotsLeft, True)
                board[idx[0]][idx[1]] = turn
                turn = p2
                spotsLeft-=1
            else:
                # Human
                inp = int(input(f"Your turn: "))
                i, j = spotToIdx(inp)
                if board[i][j]=='_':
                    board[i][j] = turn
                    turn = p1
                    spotsLeft-=1
        else: return stat
        printBoard(board)

In the above code spotsLeft is the empty places on board, checkGameOver returns "X" (if Player X wins), returns "O" (if Player O wins) & returns "T" (if game is tied)

checkGameOver function ->

def checkWin(board):
    # Check Row
    for i in board:
        if len(set(i)) == 1 and i[0]!='_':
            return i[0]
        
    # Check Column
    for i in range(4):
        if (board[0][i] == board[1][i] == board[2][i] == board[3][i]) and board[0][i]!='_':
            return board[0][i]
    
    # Check Diagonals
    if (board[0][0]==board[1][1]==board[2][2]==board[3][3]) and board[0][0]!='_':
            return board[0][0]
    if (board[0][3]==board[1][2]==board[2][1]==board[3][0]) and board[0][3]!='_':
        return board[0][3]
    
    # No One Won Yet
    return -1
    

def checkGameOver(board, spotsLeft):
    # t -> Game Tied
    # x -> Player X won
    # o -> Player O won
    
    # if tied - all spots filled
    if spotsLeft == 0:
        return True, 'T'
    
    # if any player won the game
    result = checkWin(board)
    if result!=-1:
        return True, result
    
    return False, -1

CodePudding user response:

I think your code is fine, and doing what you want it to do. The issue is most likely due to the complexity of the problem, and for a lower dimension tic tac toe it would work fine.

Let's first simplify and look at a 2x2 case. For the first turn, you call minimax from the ai function, which will at the first level call itself 4 times. At the next level, each one of those calls will also call minimax, but one time less than at the previous level. To put it as a list:

  • level 0 (ai function): 1 call to minimax
  • level 1: 4 calls
  • level 2: 4x3 calls (each of the 4 calls above make 3 new calls)
  • level 3: 4x3x2 calls
  • level 4: 4x3x2x1 calls

Now using n-factorial notation as n!, we can compute the total number of calls as:

4!/4!   4!/(4-1)!   4!/(4-2)!   4!/(4-3)!   4!/(4-4!)

Which is the sum of n!/(n-k)! for k between 0 and n (included), n begin the number of cells on your board. The result here is 65 calls to minimax for a 2x2 board.

Put into a python code:

def factorial(n):
    if n > 1: return n*factorial(n-1)
    else: return 1

def tictactoeComplexity(gridsize):
    return sum([factorial(gridsize)/factorial(gridsize-k) for k in range(gridsize 1)])

print(tictactoeComplexity(2*2)) # result is 65

Now let's check for a 3*3 board:

print(tictactoeComplexity(3*3)) # result is 986,410

Just going from 2x2 to 3x3, we go from around 100 to around 1,000,000. You can guess the result for a 4x4 board:

print(tictactoeComplexity(4*4)) # result is 56,874,039,553,217

So your program does what you are asking it to do, but you're asking quite a lot.

CodePudding user response:

As Jenny has very well explained, the search tree is too large. Even if you would make improvements to the data structure and move-mechanics to make them more efficient and reduce the memory footprint, it will still be a challenge to have this search finish within an acceptable time.

In general you would go for the following measures to cut down on the search tree:

  • Stop at a certain search depth (like 4) and perform a static evaluation of the board instead. This evaluation will be a heuristic. For instance it will give a high value to a 3-in-a-row with a free cell available to complete it to a win. It would also give a significant value to a pair on a line that is not blocked by the opponent, ...etc.

  • Use alpha-beta pruning to avoid searching in branches that could never lead to a better choice at an earlier stage in the search tree.

  • Use killer moves: a move that was found good after a certain opponent move, could also turn out to be good in reply to another opponent move. And trying that one first may work well in combination with alpha-beta pruning.

  • Memoize positions that were already evaluated (by swapping of moves), and mirror positions to equivalent positions to reduce the size of that dictionary.

But to be honest, 4x4 Tic Tac Toe is quite boring: it is very easy to play a draw, and it requires really inferior moves for a player to give the game away to the other. For instance, neither player can make a bad first move. Whatever they play on their first move, it will still be a draw with correct play.

So... I would propose to only use a heuristic evaluation function, and not search at all. Or, perform a shallow minimax, and use such a heuristic evaluation at that fixed depth. But even replacing the minimax algorithm with just an evaluation function works quite well.

What to change

To implement that idea, proceed as follows:

  1. Replace the AI part with this:

         if turn == p1:
             # AI -- a heuristic based approach
             bestScore = -math.inf
             bestMove = None
             for i in range(4):
                 for j in range(4):
                     if board[i][j] == '_':
                         board[i][j] = turn
                         score = evaluate(board, turn)
                         if score > bestScore:
                             bestScore = score
                             bestMove = (i, j)
                         board[i][j] = '_'
             i, j = bestMove
             board[i][j] = turn
             turn = p2
             spotsLeft-=1
    

    This calls evaluate which will give a numeric score that is higher the better it is for the given player (argument).

  2. Add the definition for evaluate:

    # winning lines
    lines = [
        [(i, j) for i in range(4)] for j in range(4)
    ]   [
        [(j, i) for i in range(4)] for j in range(4)
    ]   [
        [(i, i) for i in range(4)],
        [(3-i, i) for i in range(4)]
    ]
    
    def evaluate(board, player):
        score = 0
        for line in lines:
            discs = [board[i][j] for i, j in line]
            # The more discs in one line the better
            value = [1000, 10, 6, 1, 0][sum(ch == "_" for ch in discs)]
            if "X" in discs:
                if not "O" in discs: # X could still win in this line
                    score  = value
            elif "O" in discs: # O could still win in this line
                score -= value
        # Change the sign depending on which player has just played
        return score if player == "X" else -score
    

That's it! Optionally you can use the evaluate function to simplify the checkWin function:

def checkWin(board):
    score = evaluate(board, "X")
    if abs(score) > 500:  # Take into account there could be some reduction
        return "X" if score > 0 else "O"
    # No One Won Yet
    return -1

With this implementation you don't need the minimax function anymore, but you might want to keep it, and limit the search depth. When that depth is reached, make the call to evaluate, ...etc. Still I found the above implementation to play fine. You can't win a game against it.

  • Related