Skip to content

Instantly share code, notes, and snippets.

@Per48edjes
Last active March 20, 2022 08:47
Show Gist options
  • Save Per48edjes/c8bfe06e29cc7f06b543b16e6ebd164b to your computer and use it in GitHub Desktop.
Save Per48edjes/c8bfe06e29cc7f06b543b16e6ebd164b to your computer and use it in GitHub Desktop.
Tic-Tac-Toe game for Recurse Center code interview
"""
Recurse Center Programming Task: Tic-Tac-Toe
Author: Ravi Dayabhai
"""
from __future__ import annotations
import copy
import math
import os
import sys
from typing import Set, Tuple
class Cell:
def __init__(self, state: str = None):
self._state = state
@property
def state(self):
return self._state
@state.setter
def state(self, new_state: str):
self._state = new_state
def __str__(self):
return self.state if self.state else " "
class Board:
def __init__(self, board_size: int):
self._board_size = board_size
self._grid = [[Cell() for _ in range(board_size)] for _ in range(board_size)]
@property
def board_size(self):
return self._board_size
@property
def grid(self):
return self._grid
@property
def available_spaces(self) -> Set[Tuple[int, int]]:
return {
(row, col)
for row, row_cells in enumerate(self.grid)
for col, _ in enumerate(row_cells)
if not self.grid[row][col].state
}
@property
def winner(self) -> [int, None]:
rows_container = [0] * self.board_size
columns_container = [0] * self.board_size
diagonal_container = [0] * self.board_size
opposite_diagonal_container = [0] * self.board_size
# Populate containers
for row, row_cells in enumerate(self.grid):
for col, cell in enumerate(row_cells):
# + corresponds to 'X'/Player 1; - corresponds to 'O'/Player 2
player_increment = (
1 if cell.state == "X" else -1 if cell.state == "O" else 0
)
rows_container[row] += player_increment
columns_container[col] += player_increment
if row == col:
diagonal_container[row] += player_increment
if row + col + 1 == self.board_size:
opposite_diagonal_container[row] += player_increment
container_totals = {
*rows_container,
*columns_container,
sum(diagonal_container),
sum(opposite_diagonal_container),
}
# Player 1 winning condition
if self.board_size in container_totals:
return 1
# Player 2 winning condition
elif -self.board_size in container_totals:
return 2
# Game has no winner yet or cat's game
else:
return None
@property
def is_terminal_state(self):
return True if self.winner or not self.available_spaces else False
@property
def utility(self):
return 1 if self.winner == 1 else -1 if self.winner == 2 else 0
def update_grid(self, new_state: str, row: int, col: int) -> None:
self.grid[row][col].state = new_state
def __str__(self) -> str:
horizontal_divider = "\n" + ((self.board_size - 1) * "---+") + "---\n"
return horizontal_divider.join(
" " + " | ".join(str(cell) for cell in row) for row in self.grid
)
@staticmethod
def next_board(board: Board, new_state, row: int, col: int) -> Board:
new_board = copy.deepcopy(board)
new_board.update_grid(new_state, row, col)
return new_board
class Player:
def __init__(self, number: int, marker: str, board: Board):
self.number = number
self.marker = marker
def select_space(self, board: Board) -> Tuple[int]:
while True:
try:
row, col = tuple(
map(
int,
input(
f"Player {self.number} Choose location (zero-indexed from top left space) by entering desired '<row> <column>': "
).split(),
)
)
if board.grid[row][col].state:
raise ValueError
return row, col
except ValueError:
print(
"Space already taken or invalid coordinate; try a different space."
)
except IndexError:
print("One or more indices not on board; try a different space.")
def make_move(self, board: Board, row: int, col: int) -> None:
board.update_grid(self.marker, row, col)
class AI(Player):
def select_space(self, board: Board) -> Tuple[int]:
if self.number == 1:
max_utility = -math.inf
for space in board.available_spaces:
space_utility = AI.minimizer_utility(
Board.next_board(board, self.marker, *space)
)
if space_utility > max_utility:
max_utility = space_utility
row, col = space
else:
min_utility = math.inf
for space in board.available_spaces:
space_utility = AI.maximizer_utility(
Board.next_board(board, self.marker, *space)
)
if space_utility < min_utility:
min_utility = space_utility
row, col = space
return row, col
@staticmethod
def minimizer_utility(
board: Board, alpha: int = -math.inf, beta: int = math.inf
) -> int:
if board.is_terminal_state:
return board.utility
else:
utility = math.inf
for space in board.available_spaces:
space_utility = AI.maximizer_utility(
Board.next_board(board, "O", *space), alpha, beta
)
utility = min(utility, space_utility)
if space_utility <= alpha:
return space_utility
beta = min(beta, space_utility)
return utility
@staticmethod
def maximizer_utility(
board: Board, alpha: int = -math.inf, beta: int = math.inf
) -> int:
if board.is_terminal_state:
return board.utility
else:
utility = -math.inf
for space in board.available_spaces:
space_utility = AI.minimizer_utility(
Board.next_board(board, "X", *space), alpha, beta
)
utility = max(utility, space_utility)
if space_utility >= beta:
return space_utility
alpha = max(alpha, space_utility)
return utility
class Game:
def __init__(self):
self.board = self.choose_board()
self.player_1, self.player_2 = self.choose_players()
def choose_board(self) -> Board:
while True:
try:
board_size = int(
input(
"Choose the game's board size, n (i.e., n x n grid; n = 3 by default): "
)
or 3
)
if board_size >= 1:
return Board(board_size)
else:
raise ValueError
except ValueError:
print(
"Board must by at least 1x1 and can only accept postive integers."
)
def choose_players(self) -> Tuple[Player, AI]:
while True:
try:
is_single_player_str = input("Play against computer? (y/n) ")
if is_single_player_str not in ("y", "n", "Y", "N"):
raise ValueError("Choose 'y' (yes) or 'n' (no).")
is_single_player = True if is_single_player_str in ("Y", "y") else False
break
except ValueError:
continue
if is_single_player:
while True:
try:
human_player_num = int(
input("Which player would you like to play as? (1 or 2) ")
)
if human_player_num not in (1, 2):
raise ValueError
break
except ValueError:
print("Choose either 1 or 2.")
return (
(Player(1, "X", self.board), AI(2, "O", self.board))
if human_player_num == 1
else (AI(1, "X", self.board), Player(2, "O", self.board))
)
else:
return Player(1, "X", self.board), Player(2, "O", self.board)
def play(self) -> None:
os.system("clear")
print(self.board)
# Game loop
while True:
# Each round alternates players
for player in (self.player_1, self.player_2):
# Update state
row, col = player.select_space(self.board)
player.make_move(self.board, row, col)
# Display refresh
os.system("clear")
print(self.board)
# Endgame
if self.board.winner:
print(f"Player {self.board.winner} won!")
sys.exit()
if not self.board.available_spaces:
print("Cat's game!")
sys.exit()
def main() -> None:
g = Game()
g.play()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment