0

I have the following variables X, Y, EMPTY, board, action and function result(board, action) in my tictactoe.py file. board is a 2D array and action is a 2-tuple. I pass this into my function and try to reference an index in it but some how my board and/or action is being interpreted as an integer and I cant figure out why that is. When I add breakpoints to the function and inspect the running of the file it works well but when I run the program as a whole, I get the following traceback:

    board = ttt.result(board, move)
  File "c:\Users\ACER\Desktop\Projects\artificial intelligence with python\tictactoe\tictactoe\tictactoe.py", line 66, in result
    cell = new_board[action[0]][action[1]]
TypeError: 'int' object is not subscriptable

The code to the function is as follows:

X="X"
O="O"
EMPTY= None
initial_board = [[EMPTY,EMPTY,EMPTY],[EMPTY,EMPTY,EMPTY],[EMPTY,EMPTY,EMPTY]]

def result(board, action):
    """
    Returns the board that results from making move (i, j) on the board.
    """
    memo = {}
    new_board = copy.deepcopy(board, memo)
    #The line below raises the exception
    cell = new_board[action[0]][action[1]]
    move = player(board)

    if cell is not EMPTY:
        raise Exception("That move is not allowed")
    else:
        # cell = move
        new_board[action[0]][action[1]] = move
        return new_board

I am convinced the values of board and action I am passing to the function are indeed the correct types. Where am I getting it wrong here? Let me know if I should add more detail to my question. Thank you in advance for the help!

EDIT

The function is being called in a separate file runner.py that implements the GUI for the game, I am not sure what exactly that file is doing but from what I've checked, my function is being called by the following lines of code:

 # Check for AI move
        if user != player and not game_over:
            if ai_turn:
                time.sleep(0.5)
                move = ttt.minimax(board)
                board = ttt.result(board, move)
                ai_turn = False
            else:
                ai_turn = True

        # Check for a user move
        click, _, _ = pygame.mouse.get_pressed()
        if click == 1 and user == player and not game_over:
            mouse = pygame.mouse.get_pos()
            for i in range(3):
                for j in range(3):
                    if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)):
                        board = ttt.result(board, (i, j))

I hope that helps

FURTHER EDITS Below is the code for the functions that allow the AI to look for the optimal move:

def max_value(board):
    """
    Given the AI is "X"(Maximizer), returns the value of the utility if the 
    terminal state has been reached,
    otherwise return the value of the utility that will result 
    in optimal play.
    """
    if terminal(board):
        return utility(board)

    else:
        v = -(math.inf)
        for action in actions(board):
            v = max(v, min_value(result(board, action)))
        return v


def min_value(board):
    """
    Given the AI is "O"(Minimizer), returns the value of the utility if the 
    terminal state has been reached,
    otherwise return the value of the utility that will result 
    in optimal play.
    """
    if terminal(board):
        return utility(board)
    else:
        v = math.inf
        for action in actions(board):
            v = min(v, max_value(result(board, action)))
        return v


def minimax(board):
    """
    Returns the optimal action for the current player on the board.
    """

    if player(board) == X:
        return max_value(board)
    elif player(board) == O:
        return min_value(board)

player(board) Returns player who has the next turn on a board. In terms of X and O. terminal(board Returns True if game is over, False otherwise. utility(board) returns 1 if X has won the game, -1 if O has won, 0 otherwise. max_value and min_value are alternatively being called as part of the recursion. N.BI have tested the player, terminal and utility function and they are working fine

Tanatswa
  • 21
  • 3
  • 1
    Hi, could you also include the code you use to call the function? Would be much easier to debug. – Otto Hanski Jul 21 '22 at 11:08
  • 1
    "I am convinced the values of board and action I am passing to the function are indeed the correct types.": but the error tells differently. Or possibly, you're passing them in the incorrect order. – 9769953 Jul 21 '22 at 11:15
  • 2
    This is easy to debug in the script itself, without using a debugger and breakpoints. Just print the two variables above the offending line, print their types, print their lengths and the type and length of a single element of each variable. – 9769953 Jul 21 '22 at 11:18
  • What is `minimax()`? As in this line: `move = ttt.minimax(board)`. Which is one line above from where your error traceback starts. `move` may not be a 2-tuple there; or `board` isn't what you think it is in that line. – 9769953 Jul 21 '22 at 11:35
  • minimax is a function that returns the optimal move the AI can play. It does this through a series of recursive calls, and is helped out by two other functions. Let me add my implementation of those functions above. – Tanatswa Jul 21 '22 at 11:39
  • Doesn't `minimax` return the *value* of the optimal action, rather than the optimal action itself? So that when you call `result`, `action` is a single int, and trying to do `action[0]` throws the exception. – slothrop Jul 21 '22 at 12:07
  • @slothrop, Thank you that was an oversight. I'm realizing now my minimax function returns the value of the utility not the optimal action itself. I however am out of ideas as to how I can relate the action to its utility value. Any suggestions? – Tanatswa Jul 21 '22 at 12:10

1 Answers1

0

The problem is that minimax returns the value of the optimal action, rather than the optimal action itself.

This could be fixed by doing something like:

  1. Changing max_value like this:
def max_value(board):
    """
    Return a tuple (maximal_value, maximal_value_action)
    """
    if terminal(board):
        return utility(board), None

    else:
        values_per_action = [(min_value(result(board, a))[0], a) for a in actions(board)]
        return max(values_per_action)
  1. Making the equivalent changes to min_value.

  2. Changing minimax to something like this:

def minimax(board):
    if player(board) == X:
        _val, action = max_value(board)
    elif player(board) == O:
        _val, action = min_value(board)
    return action
slothrop
  • 3,218
  • 1
  • 18
  • 11
  • @slothpro. May I please have further clarification on the else clause of the max_value function. I don't quite understand whats going on there. Also, where is the recursion being applied. This implementation also returns a type error to say "'<' not supported between instances of 'NoneType' and 'str'". How do i fix that? – Tanatswa Jul 21 '22 at 12:32
  • I see what you mean - added the recursion now. Does that also fix the type error? – slothrop Jul 21 '22 at 12:38
  • 1
    The logic in the else clause is: (1) generate a list of tuples like [(recursive_value_of_action1, action1)...], (2) find the maximum member of that list. Because if we don't specify a key, comparing tuples works item-by-item from left to right (https://stackoverflow.com/questions/5292303/how-does-tuple-comparison-work-in-python), the maximum member is the one with the maximum recursive_value_of_actionN. – slothrop Jul 21 '22 at 12:41
  • It now returns a TypeError: max_value() takes 1 positional argument but 2 were given. Let me try checking if there is something wrong with the bracketing in the recursion. – Tanatswa Jul 21 '22 at 12:48
  • @slothpro. I see we have eliminated the v = math.inf and v = -math.info from the whole idea. May you kindly clarify why we eliminated that and also what exactly its purpose was in the original code that we no longer need here? – Tanatswa Jul 21 '22 at 12:52
  • 1
    The purpose was presumably to return an infinite value from the recursive calculation if there was no possible move. However, this should never be the case with your code, because you check `terminal(board)`, and if that's True then you never go into the recursion. So `values_per_action` should always contain at least one possible action and always have a valid maximum. (Edit: also, initialising v to +/-inf also made the for-loop a bit cleaner, otherwise something special would have been needed to initialise v on the first pass through the loop) – slothrop Jul 21 '22 at 12:54
  • When I try to alter the parenthesis such that max_value only takes one argument, I get a type error of "'<' not supported between instances of 'int' and 'tuple'". This is being raised by the line: min(values_per_action). – Tanatswa Jul 21 '22 at 12:56
  • Ah, I think I see the problem: see the edit (note the extra [0]) because we just want the first element of the (value, action) tuple there. – slothrop Jul 21 '22 at 12:59
  • The same error still displays. Are you sure the [0] indexing is supposed to go where youve put it. I'm thinking it should actually go on: values_per_action = [(max_value(result(board, a)), a)[1] for a in actions(board)]. And the I've also changed it to [1]. Confirm its the action not the value that we want here? – Tanatswa Jul 21 '22 at 13:07
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246648/discussion-between-slothrop-and-tanatswa). – slothrop Jul 21 '22 at 13:10