I am trying to write a minimax algorithm for tic tac toe in cs50ai, but I just can't seem to get my head around it. I can't seem to understand how to get a value back from my recursion between maxval and minval.
How can my script find a value for the game state and compare it to the infinite value and how can I relate that value to the initial move that the for loop is checking out? Currently my code always raises the exception of an invalid action. This makes me feel like it is running until the board is empty and then trying to perform another action.
I have done so much reading and looking through different codes but I can't figure out how to make this work. Any help and hints are appreciated.
"""
Tic Tac Toe Player
"""
import math
import copy
X = "X"
O = "O"
EMPTY = None
def initial_state():
"""
Returns starting state of the board.
"""
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
"""
Returns player who has the next turn on a board.
"""
if terminal(board):
return None
if board == initial_state():
return X
if checkturn(board) == X:
return X
elif checkturn(board) == O:
return O
#return turn
#raise NotImplementedError
def actions(board):
"""
Returns set of all possible actions (i, j) available on the board.
"""
action_set = set()
coord_x = 0
coord_y = 0
board_size = 3
for row in board:
for item in row:
if item is None:
action_set.add((coord_x, coord_y))
coord_y += 1
if coord_y == board_size:
coord_y = 0
coord_x += 1
return action_set
#raise NotImplementedError
def result(board, action):
"""
Returns the board that results from making move (i, j) on the board.
"""
# If the action is not in the action set raise an exception that the move is invalid
#
# DISCUSS THIS WITH HAMISH
action_set = actions(board)
# Make a copy of the previous boards to reference
parentboard = copy.deepcopy(board)
if parentboard[action[0]][action[1]] is not None:
raise Exception("Invalid action")
#Make a new updated board to return so we don't overwrite the old ones
if action in action_set:
newboard = board
newboard[action[0]][action[1]] = player(board)
return newboard
def winner(board):
"""
Returns the winner of the game, if there is one.
"""
row = [[board[0][0], board[0][1], board[0][2]],
[board[1][0], board[1][1], board[1][2]],
[board[2][0], board[2][1], board[2][2]]]
col = [[board[0][0], board[1][0], board[2][0]],
[board[0][1], board[1][1], board[2][1]],
[board[0][2], board[1][2], board[2][2]]]
diag = [[board[0][0], board[1][1], board[2][2]],
[board[2][0], board[1][1], board[0][2]]]
for item in row:
win = check_xo(item)
if win is not None:
return win
for item in col:
check_xo(item)
if win is not None:
return win
for item in diag:
check_xo(item)
if win is not None:
return win
#raise NotImplementedError
def terminal(board):
"""
Returns True if game is over, False otherwise.
"""
action_set = actions(board)
# Check if the actionset is empty. IF so game is over(True)
if len(action_set) == 0:
return True
# Check if someone has won the game. If so game is over (True).
if winner(board) == X or winner(board) == O:
return True
else:
return False
#raise NotImplementedError
def utility(board):
"""
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
if winner(board) == X:
return 1
elif winner(board) == O:
return -1
elif winner(board) is None:
return 0
#raise NotImplementedError
def minimax(board):
"""
Returns the optimal action for the current player on the board.
"""
#if terminal(board):
# return None
if player(board) == X:
v = -math.inf
for action in actions(board):
v = minval(result(board, action))
if v > value:
return action
if player(board) == O:
v = math.inf
for action in actions(board):
v = maxval(result(board, action))
if v < value:
return action
# My added functions start #
def maxval(board):
if terminal(board):
return utility(board)
value = -math.inf
for action in actions(board):
max(value, minval(result(board, action)))
return value
# Find the move that produces the highest Value
def minval(board):
if terminal(board):
return utility(board)
value = -math.inf
for action in actions(board):
min(value, maxval(result(board, action)))
return value
def checkturn(board):
xCount = 0
oCount = 0
# Count how many Os and Xs are on the board
for i in board:
for j in i:
if j == X:
xCount += 1
if j == O:
oCount += 1
#If O is less than x, then it's O's turn, otherwise it's Xs turn
if oCount < xCount:
return O
else:
return X
def check_xo(item):
if item == ['X', 'X', 'X']:
return X
if item == ['O', 'O', 'O']:
return O
else:
return None
I am tryna do same thing but whenever I run my code I choose my player and put one move then next turn getting error
Related
I'm programming Tic-Tac-Toe using minimax algorithm with alpha-beta pruning after taking Harvard's CS-50.
My logic is working perfectly, except for the return time of minimax (especially on the first call, when only one player played). I was thinking of replacing the extra recursive call of minimax to max/min value. I want to emphasize that minimax is taking relatively slow for my purposes (around 0.5 seconds).
import math
import copy
import time
X = "X"
O = "O"
EMPTY = None
def initial_state():
"""
Returns starting state of the board.
"""
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
"""
Returns player who has the next turn on a board.
"""
if board == initial_state():
return X
X_counter = 0
O_counter = 0
for row in board:
for cell in row:
if cell == X:
X_counter += 1
elif cell == O:
O_counter += 1
return O if X_counter > O_counter else X
def actions(board):
"""
Returns set of all possible actions (i, j) available on the board.
"""
possible_moves = set()
for i in range(3):
for j in range(3):
if board[i][j] is None:
possible_moves.add((i, j))
return possible_moves
def result(board, action):
"""
Returns the board that results from making move (i, j) on the board.
"""
newboard = copy.deepcopy(board)
newboard[action[0]][action[1]] = player(board)
return newboard
def winner(board):
"""
Returns the winner of the game, if there is one.
"""
if board[0][0] == board[0][1] == board[0][2] != None: # 1, 2, 3
return board[0][0]
if board[1][0] == board[1][1] == board[1][2] != None: # 4, 5, 6
return board[1][0]
if board[2][0] == board[2][1] == board[2][2] != None: # 7, 8, 9
return board[2][0]
if board[0][0] == board[1][0] == board[2][0] != None: # 1, 4, 7
return board[0][0]
if board[0][1] == board[1][1] == board[2][1] != None: # 2, 5, 8
return board[0][1]
if board[0][2] == board[1][2] == board[2][2] != None: # 3, 6, 9
return board[0][2]
if board[0][0] == board[1][1] == board[2][2] != None: # 1, 4, 7
return board[0][0]
if board[0][2] == board[1][1] == board[2][0] != None: # 3, 5, 7
return board[0][2]
return None
def terminal(board):
"""
Returns True if game is over, False otherwise.
"""
def check_draw(board):
for row in board:
for cell in row:
if cell is None:
return False
return True
if winner(board) is not None or check_draw(board): # or someone won , or there is a draw
return True
return False
def utility(board):
"""
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
temp = winner(board)
if temp == X:
return 1
elif temp == O:
return -1
return 0
def minimax(board):
"""
Returns the optimal action for the current player on the board.
"""
options = actions(board)
if player(board) == X:
vT = -math.inf
move = set()
for action in options:
v, count = minvalue(result(board,action), -math.inf, math.inf, 0)
if v > vT:
vT = v
move = action
else:
vT = math.inf
move = set()
for action in options:
v, count = maxvalue(result(board,action), -math.inf, math.inf, 0)
if v < vT:
vT = v
move = action
return move
def maxvalue(board,alpha,beta,count):
"""
Calculates the max value of a given board recursively together with minvalue
"""
if terminal(board): return utility(board), count+1
v = -math.inf
posactions = actions(board)
for action in posactions:
vret, count = minvalue(result(board, action),alpha,beta,count)
v = max(v, vret)
alpha = max(alpha, v)
if alpha > beta:
break
return v, count+1
def minvalue(board,alpha,beta,count):
"""
Calculates the min value of a given board recursively together with maxvalue
"""
if terminal(board): return utility(board), count+1
v = math.inf
posactions = actions(board)
for action in posactions:
vret, count = maxvalue(result(board, action),alpha,beta,count)
v = min(v, vret)
beta = min(v, beta)
if alpha > beta:
break
return v, count + 1
Rather than:
def player(board):
"""
Returns player who has the next turn on a board.
"""
if board == initial_state():
return X
X_counter = 0
O_counter = 0
for row in board:
for cell in row:
if cell == X:
X_counter += 1
elif cell == O:
O_counter += 1
return O if X_counter > O_counter else X
What kind of performance do you get with:
def player(board):
"""
Returns player who has the next turn on a board.
"""
counts = collections.Counter(cell for row in board for cell in row if cell)
return O if counts.get(X, 0) > counts.get(O, 0) else X
A question might arise as to why improving the performance of player might make a big difference.
To calculate the first move, I counted the number of times each method was called:
[('winner', 53673), ('player', 30442), ('result', 30441), ('terminal', 30441), ('utility', 23232), ('maxvalue', 16248), ('actions', 14419), ('minvalue', 14193), ('minimax', 1)]
We can see that player() is called very often. The next candidate to look at is clearly winner() as it is called even more frequently than player(). We can observe that winner() is only ever called by terminal() and utility(). Further, we can observe that terminal() and utility() are only ever called together like:
if terminal(board): return utility(board), count+1
So let's see if we can do something about combining them.
How about:
def terminal(board):
"""
Returns True if game is over, False otherwise.
"""
is_winner = winner(board)
if is_winner == X:
return 1
if is_winner == O:
return -1
if not actions(board):
return 0
return None
and we can use it like:
who_wins = terminal(board)
if who_wins is not None: return who_wins, count+1
This takes the heat off of winner():
[('player', 30442), ('result', 30441), ('terminal', 30441), ('winner', 30441), ('maxvalue', 16248), ('actions', 14419), ('minvalue', 14193), ('minimax', 1)]
This takes an addition 25% off the already reduced time.
On my system, the original code takes (on average) 2.5 seconds for a first move. With these simple updates, the same recommended move is generated in 0.001 seconds on average.
I'm doing the cs50 AI tic-tac-toe project. I have completed the entire code base but it seems to me that the agent only does moves in the first row of the grid. Also, if the first row of the grid is filled up, the game is shown as 'tied'. This is my code:
"""
Tic Tac Toe Player
"""
import math
import copy
X = "X"
O = "O"
EMPTY = None
def initial_state():
"""
Returns starting state of the board.
"""
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
"""
Returns player who has the next turn on a board.
"""
#Initialize number of X's and O's in the board
sum_X = 0
sum_O = 0
#Count the number of variables each time
for i in board:
sum_X+=i.count(X)
sum_O+=i.count(O)
# If X>O, then the player has to be O, but if X<O(which won't really happen ever) or X=O, naturally the player has to be X
if sum_X>sum_O:
return O
else:
return X
def actions(board):
"""
Returns set of all possible actions (i, j) available on the board.
"""
#Initialize a dictionary to track all the empty spots on the board
i=0
j=0
possible_actions=set()
while i<3:
while j<3:
if board[i][j] == EMPTY:
possible_actions.add((i,j))
j+=1
i+=1
return possible_actions
def result(board, action):
"""
Returns the board that results from making move (i, j) on the board.
"""
#Generate deep copy
board_deepcopy = copy.deepcopy(board)
try:
if board_deepcopy[action[0]][action[1]]:
raise IndexError
else:
board_deepcopy[action[0]][action[1]] = player(board_deepcopy)
return board_deepcopy
except IndexError:
print('Spot occupied already')
def winner(board):
"""
Returns the winner of the game, if there is one.
"""
#Horizontal check
for i in board:
if i.count(X)==3:
return X
elif i.count(O)==3:
return O
#Vertical check
j=0
while j<3:
i=0
vert_check=[]
while i<3:
vert_check.append(board[i][j])
i+=1
if vert_check.count(X)==3:
return X
elif vert_check.count(O)==3:
return O
j+=1
#Diagonal check
i=0
diag_check_1=[]
diag_check_2=[]
while i<3:
diag_check_1.append(board[i][i])#top left to bottom right
diag_check_2.append(board[i][2-i])#top right to bottom left
i+=1
if diag_check_1.count(X)==3 or diag_check_2.count(X)==3:
return X
elif diag_check_1.count(O)==3 or diag_check_2.count(O)==3:
return O
return None
def terminal(board):
"""
Returns True if game is over, False otherwise.
"""
#Game either ends if a winner is declared or if there is a tie i.e. there are no more actions left
if winner(board):
return True
elif not actions(board):
return True
else:
return False
def utility(board):
"""
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
if terminal(board):
if winner(board) == X:
return 1
elif winner(board) == O:
return -1
else:
return 0
def minimax(board):
"""
Returns the optimal action for the current player on the board.
"""
current_player = player(board)
if current_player == X:
v = -math.inf
for action in actions(board):
k = minimize(result(board, action))
if k > v:
v = k
best_move = action
else:
v = math.inf
for action in actions(board):
k = maximize(result(board, action))
if k < v:
v = k
best_move = action
return best_move
def maximize(board):
if terminal(board):
return utility(board)
v = -math.inf
for action in actions(board):
v=max(v,minimize(result(board, action)))
return v
def minimize(board):
if terminal(board):
return utility(board)
v = math.inf
for action in actions(board):
v=min(v,maximize(result(board, action)))
return v
The moves are also not optimal since it moves only in the first row. The possible_moves set contains all the possible moves in all the rows so that isn't really an issue.
I'm trying to create an AI program that plays tic-tac-toe against the user using PyGame. I get two different errors depending on if I click X or O. When I click X, I see the game window pop up but I cannot actually click on any of the squares. When I click O, the game window reads "Computer is thinking..." and then crashes with the following error message:
File "C:\Users\schwa\Code\cs50\pset1\tictactoe\tictactoe\tictactoe.py", line 169, in min_value
v = min(v, max_value(result(board, action)))
File "C:\Users\schwa\Code\cs50\pset1\tictactoe\tictactoe\tictactoe.py", line 158, in max_value
v = max(v, min_value(result(board, action)))
File "C:\Users\schwa\Code\cs50\pset1\tictactoe\tictactoe\tictactoe.py", line 169, in min_value
v = min(v, max_value(result(board, action)))
File "C:\Users\schwa\Code\cs50\pset1\tictactoe\tictactoe\tictactoe.py", line 158, in max_value
v = max(v, min_value(result(board, action)))
File "C:\Users\schwa\Code\cs50\pset1\tictactoe\tictactoe\tictactoe.py", line 53, in result
new_board = copy.deepcopy(board)
File "C:\Users\schwa\lib\copy.py", line 146, in deepcopy
y = copier(x, memo)
File "C:\Users\schwa\lib\copy.py", line 205, in _deepcopy_list
append(deepcopy(a, memo))
File "C:\Users\schwa\lib\copy.py", line 146, in deepcopy
y = copier(x, memo)
File "C:\Users\schwa\lib\copy.py", line 205, in _deepcopy_list
append(deepcopy(a, memo))
File "C:\Users\schwa\lib\copy.py", line 137, in deepcopy
d = id(x)
RecursionError: maximum recursion depth exceeded while calling a Python object
I scrolled and there seems to be an endless cycle alternating between min_value and max_value being called.
Here is my code:
"""
Tic Tac Toe Player
"""
import copy
import math
X = "X"
O = "O"
EMPTY = None
def initial_state():
"""
Returns starting state of the board.
"""
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
x_count = 0
o_count = 0
for rows in board:
for columns in rows:
if columns == X:
x_count += 1
elif columns == O:
o_count += 1
if x_count <= o_count:
return X
else:
return O
def actions(board):
"""
Returns set of all possible actions (i, j) available on the board.
"""
possible_actions = set()
for i in range(3):
for j in range(3):
if board[i][j] == EMPTY:
possible_actions.add((i, j))
return possible_actions
def result(board, action):
"""
Returns the board that results from making move (i, j) on the board.
"""
new_board = copy.deepcopy(board)
if new_board[action[0]][action[1]] != EMPTY:
return Exception
else:
new_board[action[0]][action[1]] == player(board)
return new_board
# if action in actions(board):
# (i, j) = action
# current_player = player(board)
# new_board = copy.deepcopy(board)
# new_board[i][j] = current_player
# return new_board
# else:
# raise Exception
def winner(board):
"""
Returns the winner of the game, if there is one.
"""
for i in range(3):
if board[i][0] == board[i][1] == board[i][2] == X:
return X
elif board[i][0] == board[i][1] == board[i][2] == O:
return O
for i in range(3):
if board[0][i] == board[1][i] == board[2][i] == X:
return X
elif board[0][i] == board[1][i] == board[2][i] == O:
return O
if board[0][0] == board[1][1] == board[2][2] == X:
return X
if board[0][0] == board[1][1] == board[2][2] == O:
return O
if board[0][2] == board[1][1] == board[2][0] == X:
return X
if board[0][2] == board[1][1] == board[2][0] == O:
return O
return None
def terminal(board):
"""
Returns True if game is over, False otherwise.
"""
if winner(board) == X or winner(board) == O:
return True
else:
for i in range(3):
for j in range(3):
if board[i][j] == EMPTY:
return False
return True
def utility(board):
"""
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
if winner(board) == X:
return 1
elif winner(board) == O:
return -1
else:
return 0
def minimax(board):
"""
Returns the optimal action for the current player on the board.
"""
if terminal(board):
return None
if player(board) == X:
score = -math.inf
best = None
for action in actions(board):
current = min_value(result(board, action))
if current > score:
score = current
best = action
return best
elif player(board) == O:
score = math.inf
best = None
for action in actions(board):
current = max_value(result(board, action))
if current < score:
score = current
best = action
return best
def max_value(board):
if terminal(board):
return utility(board)
v = -math.inf
for action in actions(board):
v = max(v, min_value(result(board, action)))
return v
def min_value(board):
if terminal(board):
return utility(board)
v = math.inf
for action in actions(board):
v = min(v, max_value(result(board, action)))
return v
The rest of functions like actions(board) or result(board, action), are good and working effectively, basedes my Minimax() method works perfectly when I choose "O" as player, but when choose "X" AI Minimax System turns too much weird:
*sorry for the basic english but im chilean (from LA)
Here is my code:
def minimax(board):
"""
Returns the optimal action for the current player on the board.
"""
# Defining AI
current_player = player(board)
# Randomizing first move
if board == initial_state():
a = random.randint(0, 2)
b = random.randint(0, 2)
return [a, b]
# if the game is ended none action is valid
if terminal(board):
return None
# Storing all possible actions
Actions = actions(board)
# If one action returns a terminal board then realize that
for action in Actions:
if terminal(result(board, action)):
return action
# Randomizing first move
if board == initial_state():
a = random.randint(0, 2)
b = random.randint(0, 2)
return [a, b]
# X maximize the utility
if current_player == X:
v = -math.inf
for action in Actions:
k = min_value(result(board, action))
if k > v:
v = k
move = action
# O minimize the utility
if current_player == O:
v = math.inf
for action in Actions:
k = max_value(result(board, action))
if k < v:
v = k
move= action
return move
def max_value(board):
if terminal(board):
return utility(board)
v = -math.inf
for action in actions(board):
v = max(v, min_value(result(board, action)))
return v
def min_value(board):
if terminal(board):
return utility(board)
v = math.inf
for action in actions(board):
v = min(v, max_value(result(board, action)))
return v
I'm working on the tic tac toe problem set. The whole game is working correctly, however I'm having some troubles kicking off the AI. I've been running the game with the computer doing random moves, and it works fine. I've tried implementing the minimax algorithm, and it seems like it works for a while, until I run into the error TypeError: '>' not supported between instances of 'NoneType' and 'float'Since I'm an absolute beginner in AI, (and python), I'm not really sure how to solve this. Regarding the error, I suppose the math.inf and -math.inf are floats, and the state (board) is is the one being NoneType?
Why am I getting an error? What value am I suppose to get? Why is this happening?
Thanks in advance for looking into this.
I've pasted all the code with comments
import math
import random
import copy
X = "X"
O = "O"
EMPTY = None
def initial_state():
"""
Returns starting state of the board.
"""
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
"""
Returns player who has the next turn on a board.
- takes a state, tells whos turn it is
- assuming x makes a first move on an empty game board, player is x
- if x has made a move, o is the player
- takes a game board, and returns whos turn it is
"""
empty_board = True
count_x = 0
count_o = 0
f_list = []
"""
for cell in board:
empty_board = all(c is None for c in cell)
print(all(c is None for c in cell))
"""
# Check every item in board.
# Flatten list, and put all elements in new list, for later use
# Check if any of the cells contain X or O. If so, the board is not empty,
# game has began, so we switch empty_board to False
for cell in board:
for i in cell:
f_list.append(i)
if i == X or i == O:
empty_board = False
# If board is empty, return X as the first player
# else, figure out who's turn is next, by counting how many X and O's on the game board.
if empty_board == True:
return X
else:
count_x = f_list.count("X")
count_o = f_list.count("O")
if count_x > count_o:
# print("Computer turn")
return O
else:
# print("Player turn")
return X
def actions(board):
"""
Returns set of all possible actions (i, j) available on the board.
- Figures out which possible moves to do next
"""
available_moves = set()
for i in range(3):
for j in range(3):
if board[i][j] == None:
available_moves.add((i, j))
return available_moves
def result(board, action):
"""
Returns the board that results from making move (i, j) on the board.
If having a state, and taking an action, we need a result
- takes the board, and the move as input, and returns the new board
with the new action
"""
nboard = copy.deepcopy(board)
if nboard[action[0]][action[1]] == None:
nboard[action[0]][action[1]] = player(board)
else:
raise ValueError("Already taken")
return nboard
def winner(board):
"""
Returns the winner of the game, if there is one.
"""
for i in range(3):
if board[i][0] == board[i][1] == board[i][2]:
return board[i][0]
elif board[0][i] == board[1][i] == board[2][i]:
return board[0][i]
elif board[0][0] == board[1][1] == board[2][2]:
return board[0][0]
elif board[0][2] == board[1][1] == board[2][0]:
return board[0][2]
def terminal(board):
"""
Returns True if game is over, False otherwise.
- takes the game board as input. If the game is over, returns true
"""
# Determine if anyone has 3 in a row
for i in range(3):
if board[i][0] == board[i][1] == board[i][2] != None:
return True
elif board[0][i] == board[1][i] == board[2][i] != None:
return True
elif board[0][0] == board[1][1] == board[2][2] != None:
return True
elif board[0][2] == board[1][1] == board[2][0] != None:
return True
l_flatten = []
# Flattens the board, and check if None is in any of the cells.
# If None is in any cells, the game is not finished, return False.
# If None is NOT in any cells, the game board is filled out, and the game is over
for cell in board:
for i in cell:
l_flatten.append(i)
if None not in l_flatten:
return True
return False
def utility(board):
"""
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
for i in range(3):
if board[i][0] == board[i][1] == board[i][2]:
if board[i][0] == X:
return 1
elif board[i][0] == O:
return -1
else:
return 0
elif board[0][i] == board[1][i] == board[2][i]:
if board[0][i] == X:
return 1
elif board[i][0] == O:
return -1
else:
return 0
elif board[0][0] == board[1][1] == board[2][2]:
if board[0][0] == X:
return 1
elif board[0][0] == O:
return -1
else:
return 0
elif board[0][2] == board[1][1] == board[2][0]:
if board[0][2] == X:
return 1
elif board[0][2] == O:
return -1
else:
return 0
def minimax(board):
"""
Returns the optimal action for the current player on the board.
should call action to get every move possible for the current player
"""
moves = actions(board)
playing = player(board)
if playing == X:
return max_value(board)
else:
return min_value(board)
return random.choice(tuple(moves))
def max_value(board):
if terminal(board):
print("TERMINAL MAX")
return utility(board)
v = -math.inf
for action in actions(board):
v = max(v, min_value(result(board, action)))
return v
def min_value(board):
if terminal(board):
print("TERMINAL MIN")
return utility(board)
v = math.inf
for action in actions(board):
v = min(v, max_value(result(board, action)))
return v
Edit: Added traceback
Traceback (most recent call last):
File "runner.py", line 115, in <module>
move = ttt.minimax(board)
File "tictactoe.py", line 198, in minimax
return min_value(board)
File "tictactoe.py", line 218, in min_value
v = min(v, max_value(result(board, action)))
File "tictactoe.py", line 209, in max_value
v = max(v, min_value(result(board, action)))
File "tictactoe.py", line 218, in min_value
v = min(v, max_value(result(board, action)))
File "tictactoe.py", line 209, in max_value
v = max(v, min_value(result(board, action)))
File "tictactoe.py", line 218, in min_value
v = min(v, max_value(result(board, action)))
File "tictactoe.py", line 209, in max_value
v = max(v, min_value(result(board, action)))
File "tictactoe.py", line 218, in min_value
v = min(v, max_value(result(board, action)))
File "tictactoe.py", line 209, in max_value
v = max(v, min_value(result(board, action)))
TypeError: '>' not supported between instances of 'NoneType' and 'float'