Skip to content Skip to sidebar Skip to footer

TicTacToe And Minimax

I am a young programmer that is learning python and struggling to implement an AI (using minimax) to play TicTacToe. I started watching a tutorial online, but the tutorial was on J

Solution 1:

Change this part, your implementation will return optimalMove even if it doesn't go inside the if statement, and optimalMove will not be assigned at that point, so put the return inside.

    if score > sampleScore:
        sampleScore = score
        optimalMove = i
        return optimalMove

Solution 2:

optimalMove = 0 in play() and optimalMove = i in findOptimalField() are declaring two distinct variables, each of which is local to the function declaring it.

If you want multiple functions to have access to the same variable, you can use the global keyword, but that's generally considered a bad practice. It can make it hard to reason about the code (e.g. is var = x creating a new local variable or overwriting the value of a global?) and it doesn't stop you from accidentally using a variable before it's declared.

Since you're coming from a Java background, you can turn this into a class to get behavior more like what you expect, eliminating the need for globals:

class TicTacToe:
    def __init__(self):
        self.board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]

        self.playerSymbol = ""
        self.playerPosition = []

        self.aiSymbol = ""
        self.aiPosition = []

        self.score = 0

        self.playerSymbol = None
        self.aiSymbol = None
        ...

    def drawBoard(self):
        print(self.board[0] + " | " + self.board[1] + " | " + self.board[2])
        ...

    def choice(self):
        answer = input("What do you want to play as? (type x or o) ")

        if answer.upper() == "X":
            self.playerSymbol = "X"
            self.aiSymbol = "O"
        ...

Each method now takes an explicit self argument that refers to the current instance, and you can use this to access any variables that belong to the class instance instead of a particular method. If you don't include self. before a variable, that variable will still be local to the method that declares it. In this case, the drawBoard() method won't be able to access the answer variable defined in choice().

You can create new self. variables in any of the class's methods, but the best practice is to initialize all of them in the __init__ constructor method, using None as a placeholder for variables that don't have a value yet.


Solution 3:

I am posting this as an answer, just in case somebody in the future stumbles upon the same problem :)

the main issue i encountered (besides my bad programming style) is that i forgot to update the contents the lists playerPosition and aiPosition. You can review the rest of the changes in the working code:

class TicTacToe:
    def __init__(self):

        self.board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]

        self.playerSymbol = ""
        self.playerPosition = []

        self.aiSymbol = ""
        self.aiPosition = []

        self.winner = None

        self.scoreBoard = None

        self.turn = 0

        self.optimalMove = int()

    def drawBoard(self):
        print(self.board[0] + " | " + self.board[1] + " | " + self.board[2])
        print("___" + "___" + "___")
        print(self.board[3] + " | " + self.board[4] + " | " + self.board[5])
        print("___" + "___" + "___")
        print(self.board[6] + " | " + self.board[7] + " | " + self.board[8])

    def choice(self):

        answer = input("What do you want to play as? (type x or o) ")

        if answer.upper() == "X":
            self.playerSymbol = "X"
            self.aiSymbol = "O"
        else:
            self.playerSymbol = "O"
            self.aiSymbol = "X"

        self.scoreBoard = {
            self.playerSymbol: -1,
            self.aiSymbol: 1,
            "tie": 0
        }

    def availableMoves(self):

        moves = []
        for i in range(0, len(self.board)):
            if self.board[i] == " ":
                moves.append(i)
        return moves

    def won_print(self):
        self.won()
        if self.winner == self.aiSymbol:
            print("AI wins :(")
            exit(0)
        elif self.winner == self.playerSymbol:
            print("Player Wins :)")
            exit(0)
        elif self.winner == "tie":
            print("Guess it's a draw")
            exit(0)

    def won(self):

        winningPositions = [{0, 1, 2}, {3, 4, 5}, {6, 7, 8},
                            {0, 4, 8}, {2, 4, 6}, {0, 3, 6},
                            {1, 4, 7}, {2, 5, 8}]

        for position in winningPositions:
            if position.issubset(self.playerPosition):
                self.winner = self.playerSymbol
                return True
            elif position.issubset(self.aiPosition):
                self.winner = self.aiSymbol
                return True
        if self.board.count(" ") == 0:
            self.winner = "tie"
            return True

        self.winner = None
        return False

    def set_i_ai(self, i):
        self.aiPosition.append(i)
        self.board[i] = self.aiSymbol

    def set_clear_for_ai(self, i):
        self.aiPosition.remove(i)
        self.board[i] = " "

    def set_i_player(self, i):
        self.playerPosition.append(i)
        self.board[i] = self.playerSymbol

    def set_clear_for_player(self, i):
        self.playerPosition.remove(i)
        self.board[i] = " "

    def findOptimalPosition(self):

        bestScore = float("-Infinity")
        elements = {}  # desperate times call for desperate measures

        for i in self.availableMoves():
            self.set_i_ai(i)
            score = self.minimax(False)
            if score > bestScore:
                bestScore = score
                elements[i] = bestScore
            self.set_clear_for_ai(i)
        if bestScore == 1:
            print("you messed up larry")
        elif bestScore == 0:
            print("hm")
        else:
            print("whoops i made a prog. error")
        return max(elements, key=lambda k: elements[k])

    def minimax(self, isMaximizing):

        if self.won():
            return self.scoreBoard[self.winner]

        if isMaximizing:
            bestScore = float("-Infinity")
            for i in self.availableMoves():
                self.set_i_ai(i)
                bestScore = max(self.minimax(False), bestScore)
                self.set_clear_for_ai(i)
            return bestScore
        else:
            bestScore = float("Infinity")
            for i in self.availableMoves():
                self.set_i_player(i)
                bestScore = min(self.minimax(True), bestScore)
                self.set_clear_for_player(i)
            return bestScore

    def play(self):

        self.choice()

        while not self.won_print():
            if self.turn % 2 == 0:
                pos = int(input("Where would you like to play? (0-8) "))
                self.playerPosition.append(pos)
                self.board[pos] = self.playerSymbol
                self.turn += 1
                self.drawBoard()
            else:
                aiTurn = self.findOptimalPosition()
                self.aiPosition.append(aiTurn)
                self.board[aiTurn] = self.aiSymbol
                self.turn += 1
                print("\n")
                print("\n")
                self.drawBoard()
        else:
            print("Thanks for playing :)")


if __name__ == '__main__':
    tictactoe = TicTacToe()
    tictactoe.play()

But as mentioned, the code may work, but there are MANY problems regarding the logic and structure, so do not straight-forward copy-paste it :))


Post a Comment for "TicTacToe And Minimax"