#!/usr/bin/env python3 from random import choice from enum import Enum from sys import exit from textwrap import dedent class CellState(Enum): """ Enum to track the state for a cell on the board X is a cell marked by the computer, O is a cell marked by the player, U is a cell that is unmarked """ X = False O = True U = None class TicTacToe: def __init__(self) -> None: self.board: list[list[CellState]] = [ [CellState.U, CellState.U, CellState.U], [CellState.U, CellState.U, CellState.U], [CellState.U, CellState.U, CellState.U], ] self.turn = 0 # Duplicate code in this function because I don't think I'm getting # a mutable reference in Python, and I don't know a better way :pensive: def change_state(self, cell: int, state: CellState) -> None: # Data integreity check cause this is Python rip if cell not in range(1, 10): raise ValueError(f"Cell {cell} is not a number 1-9.") if state.value is None: raise ValueError("Cell cannot be reset to initial state") # Okay cool now actually changing the state # Decrements cell by one to turn into a valid index index: int = cell - 1 # Decides the row using floor divison on the index # (the amount of times index evenly divides into 3 is what list it will be in) row: int = index // 3 # Gets the remanider of the previous operation to decide what row it's in col: int = index % 3 self.board[row][col] = state def get_cell(self, cell: int): if cell not in range(1, 10): raise ValueError(f"Cell {cell} is not a number 1-9.") index: int = cell - 1 # Decides the row using floor divison on the index # (the amount of times index evenly divides into 3 is what list it will be in) row: int = index // 3 # Gets the remanider of the previous operation to decide what row it's in col: int = index % 3 return self.board[row][col] def display_cell(self, cell: int) -> str: sign = self.get_cell(cell).name return sign if not sign == "U" else str(cell) def unmarked_cells(self) -> tuple[int, ...]: unmarked: list[int] = [] for i in range(1, 10): if self.get_cell(i).value is None: unmarked.append(i) else: continue return tuple(unmarked) def main(): tic_tac_toe = TicTacToe() # Game loop while True: # Check to see if the game tied # (I'm too lazy to think about using turn count to do this quicker) if len(tic_tac_toe.unmarked_cells()) == 0: print("What a tie!") break # Incriment turn count tic_tac_toe.turn += 1 # Emulate computer turn draw_move(tic_tac_toe) display_board(tic_tac_toe) # Check if computer won if victory_for(tic_tac_toe, CellState.X): print("Computer won :(") break # Start player turn enter_move(tic_tac_toe) display_board(tic_tac_toe) # Check if player won if victory_for(tic_tac_toe, CellState.O): print("You won!") break def display_board(board: TicTacToe): # The function accepts one parameter containing the board's current status # and prints it out to the console. board_display = f"""\ +-------+-------+-------+ | | | | | {board.display_cell(1)} | {board.display_cell(2)} | {board.display_cell(3)} | | | | | +-------+-------+-------+ | | | | | {board.display_cell(4)} | {board.display_cell(5)} | {board.display_cell(6)} | | | | | +-------+-------+-------+ | | | | | {board.display_cell(7)} | {board.display_cell(8)} | {board.display_cell(9)} | | | | | +-------+-------+-------+ """ print(dedent(board_display), end="") def enter_move(board: TicTacToe) -> None: # The function accepts the board's current status, asks the user about their move, # checks the input, and updates the board according to the user's decision. while True: user_input: str = input("Enter your move: ") cell: int try: cell = int(user_input) except ValueError: print("Please enter a valid value") continue if cell not in board.unmarked_cells(): print("Please choose an unoccupied sqare") continue break board.change_state(cell, CellState.O) def make_list_of_free_fields(board: TicTacToe) -> list[tuple[int, int]]: # The function browses the board and builds a list of all the free squares; # the list consists of tuples, while each tuple is a pair of row and column numbers. free_fields: list[tuple[int, int]] = [] unmarked_cells = board.unmarked_cells() for i in unmarked_cells: cell = i free_fields.append((cell // 3 + 1, cell % 3 + 1)) return free_fields def victory_for(board: TicTacToe, sign: CellState) -> bool: # The function analyzes the board's status in order to check if # the player using 'O's or 'X's has won the game # The is certainly a better way to approach this, but oh well # For example, if any of the spaces in a condition are unmarked, then it should be ignored row1 = board.board[0] row2 = board.board[1] row3 = board.board[2] col1 = [row1[0], row2[0], row3[0]] col2 = [row1[1], row2[1], row3[1]] col3 = [row1[2], row2[2], row3[2]] diag1 = [row1[0], row2[1], row3[2]] diag2 = [row1[2], row2[1], row3[0]] win_conditions = [row1, row2, row3, col1, col2, col3, diag1, diag2] for condition in win_conditions: if all(state == sign for state in condition): return True return False def draw_move(board: TicTacToe): # The function draws the computer's move and updates the board. # Puts an X in the middle of the board on the first turn, otherwise computer_cell: int = 5 if board.turn == 1 else choice(board.unmarked_cells()) board.change_state(computer_cell, CellState.X) if __name__ == "__main__": try: main() exit(0) except KeyboardInterrupt: print("\nQuitting game...") exit(0) except: exit(1)