Search code examples
pythonglobal-variablestic-tac-toelocal-variablesminimax

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 JavaScript and thus couldn't solve my problem. I also had a look at this question ( Python minimax for tictactoe ), but it did not have any answers and the implementation was considerably different from mine.

EDIT: the code you will find below is an edit suggested by one of the answers (@water_ghosts).

EDIT #2: I deleted possiblePositions, as the AI should choose a free field and not a place from the possiblePositions (that wouldn't make it that smart while implementing minimax :) )

Now the code doesn't give out any errors at all and functions properly, but one small thing: the AI always chooses the next available field. For example in situations where i am i move away from winning, instead of blocking my win option, it chooses the next free spot.

If you're wondering what that elements dict is doing there: i just wanted to make sure the programm chose the best index...

Here is my code:

class TicTacToe:
    def __init__(self):

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

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

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

        self.score = 0

        self.winner = None

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

        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"

    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
                print("Player Wins :)")
                return True
            elif position.issubset(self.aiPosition):
                self.winner = self.aiSymbol
                print("AI wins :(")
                return True
        if self.board.count(" ") == 0:
            self.winner = "tie"
            print("Guess it's a draw")
            return True

        return False

    def findOptimalPosition(self):

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

        for i in range(9):
            if self.board[i] == " ":
                self.board[i] = self.aiSymbol  # AI quasi made the move here
                if self.minimax(True) > bestScore:
                    bestScore = self.score
                    elements[i] = bestScore
                self.board[i] = " "
        return max(elements, key=lambda k: elements[k])

    def minimax(self, isMaximizing):

        if self.winner is not None:
            return self.scoreBoard[self.winner]

        if isMaximizing:
            bestScore = float("-Infinity")
            for i in range(9):
                if self.board[i] == " ":
                    self.board[i] = self.aiSymbol
                    bestScore = max(self.minimax(False), bestScore)
                    self.board[i] = " "
            return bestScore
        else:
            bestScore = float("Infinity")
            for i in range(9):
                if self.board[i] == " ":
                    self.board[i] = self.playerSymbol
                    bestScore = min(self.minimax(True), bestScore)
                    self.board[i] = " "
            return bestScore

    def play(self):

        self.choice()

        while not self.won():
            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 :)")


tictactoe = TicTacToe()
tictactoe.play()


I come from a java background and am not used to this :( Any help would be highly appreciated

I am open to suggestions and ways to improve my code and fix this problem. Thanks in advance and stay healthy, Kristi


Solution

  • 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 :))