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 tonew_board
cells, as you perform a comparison, not an assignment. Changenew_board[i][j]==-1
tonew_board[i][j]=-1
and similarly in theelse
clause.In
empty_cells
you are not doing anything with the work done oncells
but just returnFalse
. Surely the caller needs thecells
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 returnNone
is not good. Add areturn 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 shouldreturn [-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
orreturn False
based on a boolean condition, you can just return the evaluation of that condition. For instance,game_over
can doreturn 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.