Search code examples
pythonalgorithmartificial-intelligencetic-tac-toeminimax

My unbeatable Tic Tac Toe program is failing


I have tried to use the minimax algorithm to make a program that cannot lose in tic tac toe. But, in a few cases, it is failing. For example, when there are two spots left on the tic tac toe board (in a few cases), the program stops playing and asks the user for two consecutive inputs. Also, in some cases when there is an obvious win for the computer it is not making the right choice of moves.

This is for an assignment, and any kind of help today would be really appreciated.

Thanks a lot!

Edit: Please note that the code allows the user to overwrite previous moves. I will fix that as soon as I can get this working. However, even if I don't overwrite the previous chances, I don't get results. I have tested the code and the problem seems to be in the minimax function, but I've kept the whole code in case I'm wrong and the real problem lies elsewhere.

Edit 2: Sorry for the incomplete post! The test case to reproduce the problem is below. After I enter my move (position 5), the program stops playing and asks me to play all the chances.

Would you like to go first (Y/N)?: n  
. . .  
. . .   
. . .   

x . .   
. . .   
. . .   

Enter your choice (1-9): 5  
x . .   
. o .   
. . .   

x x .   
. o .   
. . .   

Enter your choice (1-9): 7  
x x .   
. o .   
o . .   

x x .   
. o .   
o . .   

Enter your choice (1-9): 

Also, I know my code is messy and amateur - but despite using global variables, I should be able to make it work. If you can help me with this I'll clean it all up. Thanks again!

Edit 3: Another test case: Would you like to go first (Y/N)?: y

. . . 
. . . 
. . . 

Enter your choice (1-9): 5
. . . 
. o . 
. . . 

x . . 
. o . 
. . . 

Enter your choice (1-9): 3
x . o 
. o . 
. . . 

x . o 
. o . 
x . . 

Enter your choice (1-9): 2
x o o 
. o . 
x . . 

x o o 
. o . 
x . . 

Enter your choice (1-9): 6
x o o 
. o o 
x . . 

x o o 
. o o 
x . . 

Enter your choice (1-9): 9
You win!

My code is in Python 3.6 and is below:

move = -1
n = 0
def evaluateBoard(board):
    global n
    #Checking for rows
    cnt = 0
    for i in range(n):
        res = 0
        for j in range(n):
           res += board[cnt * n + j] 
        cnt += 1
        if res == n:
            return 1
        elif res == -n:
            return -1

    #Checking for columns
    for i in range(n):
        res = 0
        for j in range(n):
            res += board[i + n * j]
        if res == n:
            return 1
        elif res == -n:
            return -1

    #Checking for diagonals
    res = res2 = 0
    for i in range(n):
        res += board[i * (n + 1)]   
        res2 += board[(i + 1) * (n - 1)]
    if n in [res, res2]:
        return 1
    elif -n in [res, res2]:
        return -1

    return 0

def checkNonTerminal(board):
   for pos in board:
       if pos == 0:
           return 1
   return 0

def getScore(board, depth):
    if evaluateBoard(board) == 1:
        return 10 - depth
    elif evaluateBoard(board) == -1:
        return depth - 10
    else:
        return 0

def minimax(board, turn, depth):
    if evaluateBoard(board) == 0 and checkNonTerminal(board) == 0:
        return getScore(board, depth)
    global move
    moves = list()
    scores = list()

    for square, pos in enumerate(board):
        if pos == 0:
            #print(board)
            new_board = board.copy()
            new_board[square] = turn
            moves.append(square)
            #print("Moves:", moves, "depth:", depth, "turn:", turn, checkNonTerminal(new_board) == 0)
            if evaluateBoard(new_board) in [1, -1] or checkNonTerminal(new_board) == 0:
                return getScore(new_board, depth)
            scores.append(minimax(new_board, turn * -1, depth + 1))
        #print("scores", scores) 

    if turn == 1:
        move = moves[scores.index(max(scores))]
        return max(scores)
    elif turn == -1:
        move = moves[scores.index(min(scores))]
        return min(scores)

def displayBoard(board):
    global n
    for i in range(n):
        for j in range(n):
            if board[n*i+j] == 1:
                print("x", end = " ")
            elif board[n*i+j] == -1:
                print("o", end = " ")
            else:
                print(".", end = " ")
        print()

def main():      
    global n 
    global move
    n = 3
    first_turn = input("Would you like to go first (Y/N)?: ")
    if first_turn in ['Y', 'y']:
        first_turn = -1
        cnt = 1
    else:
        first_turn = 1
        cnt = 2
    board = [0] * 9

    while evaluateBoard(board) == 0 and checkNonTerminal(board) == 1:
        displayBoard(board)
        if cnt % 2 == 0:
            score = minimax(board, 1, 0)
            print(score)
            board[move] = 1
        else:
            choice = eval(input("Enter your choice (1-9): "))
            board[choice - 1] = -1
        cnt += 1

    if evaluateBoard(board) == 1:
        print("You lose!")
    elif evaluateBoard(board) == -1:
        print("You win!")
    else:
        print("It's a draw!")

main()

Solution

  • If your first move checked is a game-ender, you're returning without setting a move. Probably the cause of your failed win-logic as well as the skipped turn.

    To put it a little more programmerly:
    Your recursion termination condition is being triggered prematurely and you need to handle that case too!

    for square, pos in enumerate(board):
        if pos == 0:
            #print(board)
            new_board = board.copy()
            new_board[square] = turn
            moves.append(square)
            #print("Moves:", moves, "depth:", depth, "turn:", turn, checkNonTerminal(new_board) == 0)
            if evaluateBoard(new_board) in [1, -1] or checkNonTerminal(new_board) == 0:
                return getScore(new_board, depth) <----here
            scores.append(minimax(new_board, turn * -1, depth + 1))
        #print("scores", scores) 
    

    Too busy to check, but I believe you can just set your move variable there as well--if you're just popping up your recursion stack, it will get overwritten later.

    PS, that's another reason to use proper return variables instead of just setting a global variable ;)