2

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()
  • @rassar: That site only takes working code. – user2357112 Oct 17 '18 at 00:16
  • There's a lot of code here. Try to test individual functions and isolate which particular function contains the failing logic. – user2357112 Oct 17 '18 at 00:19
  • @AvantikaDasgupta Your move logic seems to allow for players to choose something more than once. Also, you shouldn't use global variables, as they're not needed. You can use a tuple to return more than one value from a function. – Chai T. Rex Oct 17 '18 at 00:23
  • 1
    Welcome to StackOverflow. Please read and follow the posting guidelines in the help documentation, as suggested when you created this account. [Minimal, complete, verifiable example](http://stackoverflow.com/help/mcve) applies here. We cannot effectively help you until you post your MCVE code and accurately describe the problem. We should be able to paste your posted code into a text file and reproduce the problem you described. First of all, your posting requires me to make up a test case -- that's your responsibility. Second, I see no tracing output, a standard part of debugging. – Prune Oct 17 '18 at 00:26
  • @Prune thank you for the guidelines. I am sorry for the inconvenience. I've tried to add the missing information to the post. Could you please have a look at it again? Thank you so much. – Avantika Dasgupta Oct 17 '18 at 00:41
  • @user2357112 I think the problem is in the minimax function, but I've kept all of it because I'm not sure. – Avantika Dasgupta Oct 17 '18 at 00:42
  • @ChaiT.Rex I know global variables aren't needed, but right now I just need the code to run. I'll fix the structure later. If it's possible could you have a look at the logic again? Thanks! – Avantika Dasgupta Oct 17 '18 at 00:44
  • Show us the cases it fails – Mars Oct 17 '18 at 00:47
  • @Mars it's mentioned in the post body - above the code, in Edit 2 – Avantika Dasgupta Oct 17 '18 at 00:48
  • Sorry, saw that one. I meant show the cases where it chooses the wrong move, not the one where it fails to make a move – Mars Oct 17 '18 at 00:50
  • @Mars it's not really making a wrong move. It just stops playing. Other cases are like this too. – Avantika Dasgupta Oct 17 '18 at 00:55
  • 1
    Your title seems self contradictory – Mad Physicist Oct 17 '18 at 01:00
  • @MadPhysicist can you help me change that? – Avantika Dasgupta Oct 17 '18 at 01:01
  • Avantika, MadPhysicist is making a joke haha – Mars Oct 17 '18 at 01:04
  • 2
    @AvantikaDasgupta: Much better! See how quickly someone came to your aid with those improvements! Even better would be to hard-code some input, so the program would show the problem without us typing moves, but that's for later problems. I rescinded my closure vote, reversed the down-vote, and up-voted the answer. Nice work, everyone! – Prune Oct 17 '18 at 01:11

1 Answers1

2

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 ;)

Mars
  • 2,505
  • 17
  • 26