Search code examples
pythonartificial-intelligencetic-tac-toeminmax

Tic Tac Toe AI not working(min max algorithm)


I am trying to make a tic tac toe ai in python using minimax algorithm. I have read a few blogs and came up with these functions. But for some reason it does not work.

Code:

class RunTimeError(Exception):
    pass

class TicTacToe:
    '''Tic Tac Toe class'''
    
    SYMBOL_X = 'x'
    SYMBOL_O = 'o'
    SYMBOL_EMPTY = '-'
    INDEX_MAP = {
        0: [0, 0], 1: [0, 1], 2: [0, 2],
        3: [1, 0], 4: [1, 1], 5: [1, 2],
        6: [2, 0], 7: [2, 1], 8: [2, 2]
    }

    def __init__(self) -> None:
        '''Initialize the game'''
        self.reset()

    def reset(self) -> None:
        '''Reset the game'''
        self.board = [[self.SYMBOL_EMPTY for _ in range(3)] for _ in range(3)]
        self.current_turn = 'x'
        self.moves_stack = []
        self.winner = None
        self.game_state = 'RUNNING'

    def swap_turn(self) -> None:
        '''Swap the turn'''
        self.current_turn = self.SYMBOL_X if self.current_turn == self.SYMBOL_O else self.SYMBOL_O

    def play_move(self, index: int) -> None:
        '''Play a move on the given index:
            0 1 2
            3 4 5
            6 7 8'''

        if self.game_state != 'RUNNING':
            raise RuntimeError('Game already terminated')
        if not isinstance(index, int):
            raise RunTimeError(f"Expected index type 'int' got {type(index)}")
        try:
            y, x = self.INDEX_MAP[index]
        except KeyError:
            raise RunTimeError(f'Expected index to be from 0-8, got {index}')
        if self.board[y][x] != self.SYMBOL_EMPTY:
            raise RunTimeError('Invalid Move - Box occupied')

        self.board[y][x] = self.current_turn
        self.moves_stack.append(index)
        self.update_game_state()
        self.swap_turn()

    def undo_move(self) -> int:
        '''Reverse the last move and return its index'''
        index = self.moves_stack.pop()
        y, x = self.INDEX_MAP[index]
        self.board[y][x] = self.SYMBOL_EMPTY
        self.winner = None
        self.game_state = 'RUNNING'
        self.swap_turn()
        return index

    def update_game_state(self) -> None:
        '''Check for a winner'''
        win_cases = [
            self.board[0][0] == self.board[0][1] == self.board[0][2] == self.current_turn,
            self.board[1][0] == self.board[1][1] == self.board[1][2] == self.current_turn,
            self.board[2][0] == self.board[2][1] == self.board[2][2] == self.current_turn,
            self.board[0][0] == self.board[1][0] == self.board[2][0] == self.current_turn,
            self.board[0][1] == self.board[1][1] == self.board[2][1] == self.current_turn,
            self.board[0][2] == self.board[1][2] == self.board[2][2] == self.current_turn,
            self.board[0][0] == self.board[1][1] == self.board[2][2] == self.current_turn,
            self.board[0][2] == self.board[1][1] == self.board[2][0] == self.current_turn,
        ]
        if any(win_cases):
            self.winner = self.current_turn
            self.game_state = 'WIN'
        elif self.SYMBOL_EMPTY not in [element for row in self.board for element in row]:
            self.game_state = 'DRAW'

    def generate_possible_moves(self) -> list:
        '''Returns a list with all possible move indexes'''
        moves = []
        for num in range(8):
            if num not in self.moves_stack:
                moves.append(num)
        return moves

    def __str__(self) -> str:
        '''Returns the board as a string'''
        return '\n'.join([
            f' {self.board[0][0]} | {self.board[0][1]} | {self.board[0][2]}',
            '-' * 11,
            f' {self.board[1][0]} | {self.board[1][1]} | {self.board[1][2]}',
            '-' * 11,
            f' {self.board[2][0]} | {self.board[2][1]} | {self.board[2][2]}'
        ])

    def ai_play(self):
        best_score = -100
        best_move = None

        for move in self.generate_possible_moves():
            self.play_move(move)
            score = self.minmax(False, self.current_turn)
            print(score)
            self.undo_move()

            if score > best_score:
                best_score = score
                best_move = move

        self.play_move(best_move)

    def minmax(self, maximizing, symbol):
        if self.game_state == 'DRAW':
            return 0
        elif self.game_state == 'WIN':
            return 1 if self.winner == symbol else -1

        scores = []
        for move in self.generate_possible_moves():
            self.play_move(move)
            scores.append(self.minmax(not maximizing, symbol))
            self.undo_move()

        return max(scores) if maximizing else min(scores)

game1 = TicTacToe()

print(game1)
game1.ai_play()
print(game1)

It raises the following error:

Traceback (most recent call last):
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 139, in <module>
    game1.ai_play()
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 112, in ai_play
    score = self.minmax(False, self.current_turn)
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 131, in minmax
    scores.append(self.minmax(not maximizing, symbol))
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 131, in minmax
    scores.append(self.minmax(not maximizing, symbol))
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 131, in minmax
    scores.append(self.minmax(not maximizing, symbol))
  [Previous line repeated 4 more times]
  File "c:\Users\LENOVO\Desktop\tic tac toe.py", line 134, in minmax
    return max(scores) if maximizing else min(scores)
ValueError: max() arg is an empty sequence

I dont have any idea whats the issue(I have spent a lot of time trying to solve it) Any help would be appreciated!


Solution

  • The max and min functions expect at-least one item in the passed list. When in minmax function there are no more possible moves, generate_possible_moves returns an empty list, so scores is []. max([]) will throw an error because there is nothing to do max from.

    You will have to assign some score for when there is no possible move and return that when len(scores) is 0

    Also, you have to call update_game_state when you undo any move, otherwise the state is incorrect.