0

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

Kristi
  • 27
  • 7

3 Answers3

0

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
faressalem
  • 574
  • 6
  • 20
  • Thanks for your comment! I tried to implement this, but it didn't work (in the old and new Version). Do you have any other suggestions i could try? – Kristi Apr 28 '20 at 12:32
  • @Kristi I tried it now and the first problem I solved you in my answer was solved but still, there are the other problems, I suggest you use a debugger and try going through the code line by line to identify the errors you have. – faressalem Apr 28 '20 at 13:59
  • I tried to follow your advice and came up with the edited code above. Thankfully, there are no more errors, however it doesn't behave properly. Any ideas? – Kristi May 06 '20 at 20:53
0

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.

water_ghosts
  • 716
  • 5
  • 12
  • Hey i tried to implement you advice, however it was rather unhelpful. I keep getting the Error: optimalMove is referenced before assigned, despite it being calles from self. . I have edited the question to show my updated code and hope that helps solving the problem. Thanks :) – Kristi Apr 28 '20 at 12:24
0

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

Kristi
  • 27
  • 7