3

I'm trying to code a tic-tac-toe bot using the minimax algorithm in python. My code seems like it should work to me, but misevaluates positions, searches too many or too few nodes and loses to me every game.

mainboard = ["-", "-", "-", "-", "-", "-", "-", "-", "-"]
nodes = 0

def detectwin(b):
    signs = ["O", "X"]
    for s in signs:
        for i in range(3):
            j = 3 * i
            if ((b[0 + j]==s and b[1 + j]==s and b[2 + j]==s) or
                (b[0 + i]==s and b[1 + i]==s and b[2 + i]==s)):
                if s == "O": return 1
                if s == "X": return -1
        if ((b[0]==s and b[4]==s and b[8]==s) or
            (b[2]==s and b[4]==s and b[6]==s)):
                if s == "O": return 1
                if s == "X": return -1
    return 0

def evaluate(board):
    return detectwin(board)

def fullboard(board):
    return all(cell != "-" for cell in board)

def makemove(board, move, maximizingPlayer):
    if maximizingPlayer:
        board[move] = "O"
        return board
    else:
        board[move] = "X"
        return board

def undomove(board, move):
    board[move] = "-"
    return board

def minimax(board, depth, maximizingPlayer):

    global nodes

    if depth == 0 or fullboard(board) or detectwin(board) != 0:
        nodes += 1
        return evaluate(board)

    if maximizingPlayer:
        maxEval = -1000
        for i in range(9):
            if board[i] == "-":
                board = makemove(board, i , True)
                newEval = minimax(board, depth-1, False)
                maxEval = max(maxEval, newEval)
                board = undomove(board, i)
        return maxEval
    
    else:
        minEval = 1000
        for i in range(9):
            if board[i] == "-":
                board = makemove(board, i , False)
                newEval = minimax(board, depth-1, True)
                minEval = min(minEval, newEval)
                board = undomove(board, i)
        return minEval

def findbestmove(board, maximizingPlayer):

    global nodes
    
    if maximizingPlayer:
        bestmove = -1
        maxEval = -1000
        for i in range(9):
            if board[i] == "-":
                board = makemove(board, i , True)
                nodes = 0
                newEval = minimax(board, 9, False)
                print(f"Eval move {i}: {newEval} ({nodes} nodes)")
                if newEval > maxEval:
                    maxEval = newEval
                    bestmove = i
                board = undomove(board, i)
        return bestmove

def printboard(b):
    signs = ["No", "O", "X"]
    win = signs[detectwin(b)] + " wins"
    print(f'{b[0]} {b[1]} {b[2]}\n{b[3]} {b[4]} {b[5]}\n{b[6]} {b[7]} {b[8]}\n{win}\n')

print("Ready!")
while True:
    move = findbestmove(mainboard, True)
    mainboard = makemove(mainboard, move, True)
    printboard(mainboard)
    yourmove = int(input())
    mainboard = makemove(mainboard, yourmove, False)
    printboard(mainboard)

I've tried evaluating different positions, expecting it to give the correct objective evaluation of the postion by searching every possibility. Instead it searches the wrong number of nodes and evaluates positions incorrectly. Why does it do this?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Alexand
  • 31
  • 2

1 Answers1

7

In the for loop in your detectwin function, (b[0 + j]==s and b[1 + j]==s and b[2 + j]==s) is actually checking rows, because when you increase j by 3 each iteration, you're moving to the next row in the board. However, (b[0 + i]==s and b[1 + i]==s and b[2 + i]==s) is also checking rows instead of columns, just in a different way. This is because when you increase i by 1 each iteration without multiplying it by 3, you're staying within the same row of the 1D board representation.

So the issue is that you're checking the rows twice and the columns not at all.

The current column checking loop:

for i in range(3):
    j = 3 * i
    if ((b[0 + j]==s and b[1 + j]==s and b[2 + j]==s) or
        (b[0 + i]==s and b[1 + i]==s and b[2 + i]==s)):
        if s == "O": return 1
        if s == "X": return -1

which doesn't check the second and third column at all. Fixed:

for i in range(3):
    j = 3 * i
    if ((b[0 + j]==s and b[1 + j]==s and b[2 + j]==s) or
        (b[i]==s and b[i + 3]==s and b[i + 6]==s)):
        if s == "O": return 1
        if s == "X": return -1

By replacing (b[0 + i]==s and b[1 + i]==s and b[2 + i]==s) with (b[i]==s and b[i + 3]==s and b[i + 6]==s) the check will go through the columns of the game board. Hope this helps.

Ro.oT
  • 623
  • 6
  • 15
David Greydanus
  • 2,551
  • 1
  • 23
  • 42
  • 1
    OMG THANK YOU SO MUCH!! You just made my day :D been trying to solve this for so long. Thank you!! Have an awesome day!! – Alexand Jul 06 '23 at 20:59