188 lines
5.7 KiB
Python
188 lines
5.7 KiB
Python
import math
|
||
|
||
# Use these constants to fill in the game board
|
||
X = "X"
|
||
O = "O"
|
||
EMPTY = None
|
||
|
||
|
||
def start_state():
|
||
"""
|
||
Returns starting state of the board.
|
||
"""
|
||
return [[EMPTY, EMPTY, EMPTY],
|
||
[EMPTY, EMPTY, EMPTY],
|
||
[EMPTY, EMPTY, EMPTY]]
|
||
|
||
|
||
def player(board):
|
||
"""
|
||
Returns which player (either X or O) who has the next turn on a board.
|
||
|
||
In the initial game state, X gets the first move. Subsequently, the player alternates with each additional move.
|
||
Any return value is acceptable if a terminal board is provided as input (i.e., the game is already over).
|
||
"""
|
||
count_X = sum(row.count(X) for row in board)
|
||
count_O = sum(row.count(O) for row in board)
|
||
|
||
if count_X > count_O:
|
||
return O
|
||
else:
|
||
return X
|
||
|
||
|
||
def actions(board):
|
||
"""
|
||
Returns the set of all possible actions (i, j) available on the board.
|
||
|
||
The actions function should return a set of all the possible actions that can be taken on a given board.
|
||
Each action should be represented as a tuple (i, j) where i corresponds to the row of the move (0, 1, or 2)
|
||
and j corresponds to the column of the move (also 0, 1, or 2).
|
||
|
||
Possible moves are any cells on the board that do not already have an X or an O in them.
|
||
|
||
Any return value is acceptable if a terminal board is provided as input.
|
||
"""
|
||
actions = set()
|
||
for i in range(3):
|
||
for j in range(3):
|
||
if board[i][j] == EMPTY:
|
||
actions.add((i, j))
|
||
return actions
|
||
|
||
|
||
def succ(board, action):
|
||
"""
|
||
Returns the board that results from making move (i, j) on the board, without modifying the original board.
|
||
|
||
If `action` is not a valid action for the board, you should raise an exception.
|
||
|
||
The returned board state should be the board that would result from taking the original input board, and letting
|
||
the player whose turn it is make their move at the cell indicated by the input action.
|
||
|
||
Importantly, the original board should be left unmodified. This means that simply updating a cell in `board` itself
|
||
is not a correct implementation of this function. You’ll likely want to make a deep copy of the board first before
|
||
making any changes.
|
||
"""
|
||
if action not in actions(board):
|
||
raise ValueError("Invalid action")
|
||
|
||
new_board = [row[:] for row in board]
|
||
new_board[action[0]][action[1]] = player(board)
|
||
|
||
return new_board
|
||
|
||
|
||
def winner(board):
|
||
"""
|
||
Returns the winner of the game, if there is one.
|
||
|
||
- If the X player has won the game, the function should return X.
|
||
- If the O player has won the game, the function should return O.
|
||
- If there is no winner of the game (either because the game is in progress, or because it ended in a tie), the
|
||
function should return None.
|
||
|
||
You may assume that there will be at most one winner (that is, no board will ever have both players with
|
||
three-in-a-row, since that would be an invalid board state).
|
||
"""
|
||
# Check rows
|
||
for row in board:
|
||
if row[0] == row[1] == row[2] and row[0] is not None:
|
||
return row[0]
|
||
|
||
# Check columns
|
||
for col in range(3):
|
||
if board[0][col] == board[1][col] == board[2][col] and board[0][col] is not None:
|
||
return board[0][col]
|
||
|
||
# Check diagonals
|
||
if board[0][0] == board[1][1] == board[2][2] and board[0][0] is not None:
|
||
return board[0][0]
|
||
if board[0][2] == board[1][1] == board[2][0] and board[0][2] is not None:
|
||
return board[0][2]
|
||
|
||
return None
|
||
|
||
|
||
def terminal(board):
|
||
"""
|
||
Returns True if game is over, False otherwise.
|
||
|
||
If the game is over, either because someone has won the game or because all cells have been filled without anyone
|
||
winning, the function should return True.
|
||
|
||
Otherwise, the function should return False if the game is still in progress.
|
||
"""
|
||
if winner(board) is not None or all(all(cell is not EMPTY for cell in row) for row in board):
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
def utility(board):
|
||
"""
|
||
Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
|
||
|
||
You may assume utility will only be called on a board if terminal(board) is True.
|
||
"""
|
||
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.
|
||
|
||
The move returned should be the optimal action (i, j) that is one of the allowable actions on the board.
|
||
|
||
If multiple moves are equally optimal, any of those moves is acceptable.
|
||
|
||
If the board is a terminal board, the minimax function should return None.
|
||
"""
|
||
if terminal(board):
|
||
return None
|
||
|
||
if player(board) == X:
|
||
best_score = float('-inf')
|
||
best_action = None
|
||
for action in actions(board):
|
||
score = min_value(succ(board, action))
|
||
if score > best_score:
|
||
best_score = score
|
||
best_action = action
|
||
return best_action
|
||
else:
|
||
best_score = float('inf')
|
||
best_action = None
|
||
for action in actions(board):
|
||
score = max_value(succ(board, action))
|
||
if score < best_score:
|
||
best_score = score
|
||
best_action = action
|
||
return best_action
|
||
|
||
"""
|
||
Helper function lol
|
||
"""
|
||
def max_value(board):
|
||
if terminal(board):
|
||
return utility(board)
|
||
v = float('-inf')
|
||
for action in actions(board):
|
||
v = max(v, min_value(succ(board, action)))
|
||
return v
|
||
|
||
"""
|
||
Another helper function lol
|
||
"""
|
||
def min_value(board):
|
||
if terminal(board):
|
||
return utility(board)
|
||
v = float('inf')
|
||
for action in actions(board):
|
||
v = min(v, max_value(succ(board, action)))
|
||
return v |