Files
tic-tac-toe/main.py
2025-10-05 21:23:50 +00:00

187 lines
6.4 KiB
Python
Executable File

#!/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)