Home > Software design >  NoneType is not subscriptable for Minimax tic-tac-toe game
NoneType is not subscriptable for Minimax tic-tac-toe game

Time:01-15

I have a tic tac toe game using a minimax algorithm for the computer "player." The Tkinter portion works, and the actual person player works correctly. However, when the computer is supposed to play, it gives the error: "NoneType is not subscriptable." I'm not sure why. Am I missing an input for one of my variables? Thank you in advance.

Here is my code:

from tkinter import *
import customtkinter
import random
import minimax

customtkinter.set_appearance_mode("Dark")
#creating CTk window for app
root = customtkinter.CTk()

#setting window width and height
root.geometry('500x300')


#Creating label
label = customtkinter.CTkLabel(master=root,
                               text="Tic Tac Toe",
                               width=120,
                               height=50,
                               font=("normal", 20),
                               corner_radius=8)
label.place(relx=0.25, rely=0.8, anchor=CENTER)

#Handling clicks
DEPTH=8
def clickbutton(r, c):
    buttons[r][c]["text"]="X"
    board[r][c]="X"
    buttons[r][c]['state']=DISABLED

    label = customtkinter.CTkLabel(master=root,
                               text=checkwin(board),
                               width=120,
                               height=25,
                               corner_radius=8)
    label.place(relx=0.25, rely=0.9, anchor=CENTER)


    computerplay()
    DEPTH=DEPTH-1
    

#Button matrix
buttons = [
     [0,0,0],
     [0,0,0],
     [0,0,0]]
 
#Matrix identifying whether buttons are active or inactive
board=[[0,0,0],[0,0,0],[0,0,0]]
 
for i in range(3):
    for j in range(3):                                 
        buttons[i][j] = Button(height = 3, width = 6, font = ("Normal", 20),
                        command = lambda r = i, c = j : clickbutton(r,c))
        buttons[i][j].grid(row = i, column = j)



def computerplay():
    bestmove=minimax.minimax(board, DEPTH, 1)
    buttons[bestmove[0]][bestmove[1]]['text']="O"
    buttons[bestmove[0]][bestmove[1]]['state']=DISABLED
    board[bestmove[0]][bestmove[1]]="O"

def checkwin(b):
    score=minimax.evaluate(b)
    if score==10:
        return 'Computer won!'
    elif score==-10:
        return 'You won!'
    else:
        return 'Player vs. Computer'
    

root.mainloop()

My minimax code:

import math

def change_board(board):
    #changes board into -1, 0, and 1s instead of X, O, and 0 (zero).
    new_board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

    for i in range(3):
        for j in range(3):
            if board[i][j]=='X':
                new_board[i][j]==-1
            elif board[i][j]=='O':
                new_board[i][j]==1

    return new_board

def empty_cells(board):
    cells=[]
    for i in range(3):
        for j in range(3):
            if board[i][j]==0:
                cells.append([i, j])
                
    return False

def game_over(board):
    #check for wins using evaluate
    if evaluate(board)==10 or evaluate(board)==-10:
        return True
    
    #check for full board
    if (not empty_cells(board)):
        return True

    return False
    

def evaluate(board):
    #check score
    if board[0][0]==board[1][1] and board[1][1]==board[2][2]:
        if board[0][0]==-1:
            return -10
        elif board[0][0]==1:
            return 10
    
    if board[0][2]==board[1][1] and board[1][1]==board[2][0]:
        if board[0][2]==-1:
            return -10
        elif board[0][2]==1:
            return 10
    
    for row in range(3):
        if board[row][0]==board[row][1] and board[row][1]==board[row][2]:
            if board[row][0]==-1:
                return -10
            elif board[row][0]==1:
                return 10

    for col in range(3):
        if board[0][col]==board[1][col] and board[1][col]==board[2][col]:
            if board[0][col]==-1:
                return -10
            elif board[0][col]==1:
                return 10
    



def minimax(board, depth, player):
    if player==1:
        #1 is computer. -1 is human player.
        best=[-1, -1, -math.inf]
    else:
        best=[-1, -1, math.inf]

    if depth==0 or game_over(board):
        score=evaluate(board)
        return score

    #checking scores of valid moves 
    for cell in empty_cells(board):
        x, y = cell[0], cell[1]
        board[x][y] = player
        score = minimax(board, depth - 1, -player)
        board[x][y] = 0
        score[0], score[1] = x, y

        if player == 1:
            if score[2] > best[2]:
                best = score
        else:
            if score[2] < best[2]:
                best = score

    return best

CodePudding user response:

There are these issues with your minimax code:

  • In change_board you are not assigning anything to new_board cells, as you perform a comparison, not an assignment. Change new_board[i][j]==-1 to new_board[i][j]=-1 and similarly in the else clause.

  • In empty_cells you are not doing anything with the work done on cells but just return False. Surely the caller needs the cells list, so you should return it.

  • In evaluate you should also return a numerical value when no winning line was detected: the caller needs a number to compare it with the best score so far, so leaving it to just return None is not good. Add a return 0 at the bottom of the function.

  • In minimax you should always return the same structure, i.e. a list with three members. This is not done in the base case where you just return a number. To fix that, that base case should return [-1, -1, score]. Related to this, you should consider using a different name for when the score is a number (like here), or when the score is that list, like you do elsewhere in the code. This is confusing.

With those fixes it will work.

Still, there are some things you could improve, including:

  • Where you return True or return False based on a boolean condition, you can just return the evaluation of that condition. For instance, game_over can do return abs(evaluate(board))==10 or not empty_cells(board)

  • You can chain comparison operators, so like board[0][0]==board[1][1]==board[2][2]

  • Avoid code repetition in game_over: put the information of where the winning lines are in a data structure and then loop over it to only have the three-cell comparisons coded once.

  • Use tuples instead of lists when there is no need to mutate it, like for x/y coordinates.

  • With math.inf you're using a float where otherwise the score would be an integer. It is better practice to avoid that type mix. In this case you can use the value 11 instead.

Here is how it could look:

def change_board(board):
    return [
        [(cell == "O") - (cell == "X") for cell in row]
        for row in board
    ]

def empty_cells(board):
    return [(i, j) for i in range(3) for j in range(3) if board[i][j] == 0]

def game_over(board):
    return abs(evaluate(board)) == 10 or not empty_cells(board)
    
lines = (
    (0, 0, 1, 1, 2, 2),
    (0, 2, 1, 1, 2, 0),
    (0, 0, 0, 1, 0, 2),
    (1, 0, 1, 1, 1, 2),
    (2, 0, 2, 1, 2, 2),
    (0, 0, 1, 0, 2, 0),
    (0, 1, 1, 1, 2, 1),
    (0, 2, 1, 2, 2, 2),
)

def evaluate(board):
    def iswin(line):
        arow, acol, brow, bcol, crow, ccol = line
        if board[arow][acol] == board[brow][bcol] == board[crow][ccol]:
            return board[arow][acol]*10

    return next((win for win in map(iswin, lines) if win), 0)


def minimax(board, depth, player):
    best = (-1, -1, -11*player)

    if depth <= 0 or game_over(board):
        return (-1, -1, evaluate(board))  # must return same structure

    for x, y in empty_cells(board):
        board[x][y] = player
        score = minimax(board, depth - 1, -player)[2]  # only get the score
        board[x][y] = 0
        if (player == 1) == (score > best[2]):
            best = (x, y, score)  # only inject x, y when move is best

    return best

More optimisation is possible when you use a bit representation of the board instead of a 2D matrix.

  • Related