I've been working on a tic-tac-toe backend for an interactive commercial-use display and was wondering if I could get some feedback on it.
#tttcore.py ~ Manages game state, checks win condition, etc. from enum import Enum from typing import Optional, Any, Iterable from copy import deepcopy as copy class OutOfBoundsError(IndexError): pass class SpaceOccupied(IndexError): pass class GameEnd(Exception): pass class XWin(GameEnd): pass class OWin(GameEnd): pass class CatsGame(GameEnd): pass class TTTPlayer(Enum): X = 0 O = 1 class TicTacToeCore: def __init__(self, width: int = 3, *, board = None): # Note: width arg is ignored when a board is supplied if board: self.width = len(board) self.board = board else: self.width = width self.board = [[None for _ in range(self.width)] for _ in range(self.width)] def move(self, x: int, y: int, player: TTTPlayer): # Note: Coords are 0-indexed if not isinstance(x, int): raise TypeError(f"x-coord must be int, got {x.__class__}") if not isinstance(y, int): raise TypeError(f"y-coord must be int, got {y.__class__}") if not isinstance(player, TTTPlayer): raise TypeError(f"player must be TTTPlayer, got {player.__class__}") if x < 0 or x >= self.width: raise OutOfBoundsError(f"x-coord must be between 0 and {self.width-1}, inclusive, got {x} instead") if y < 0 or y >= self.width: raise OutOfBoundsError(f"y-coord must be between 0 and {self.width-1}, inclusive, got {y} instead") if self.board[x][y]: raise SpaceOccupied(f"You know you're supposed to catch this exception, right? How we got here: Space ({x}, {y}) is occupied by {self.board[x][y]}.") self.board[x][y] = player self._check_win(player) return True def _check_win(self, player: TTTPlayer): def check(row: Iterable[Optional[TTTPlayer]]): if all(space == player for space in row): if player == TTTPlayer.X: raise XWin() else: raise OWin() for row in self.board: check(row) for col in zip(*self.board): check(col) for diag in ([self.board[x][x] for x in range(self.width)], [self.board[x][self.width-x-1] for x in range(self.width)]): check(diag) if not any(None in row for row in self.board): raise CatsGame() def board_repr(self, prefix=" "): mapping = {None: "-", TTTPlayer.X: "X", TTTPlayer.O: "O"} board_copy = copy(self.board) for i, row in enumerate(board_copy): row.insert(0, str(i)) board_copy.insert(0, [" ", *map(str, range(self.width))]) return prefix + ("\n" + prefix).join((" ".join(mapping.get(player, player) for player in row)) for row in board_copy) def __repr__(self): return f"TicTacToeCore(width={self.width!r}, board={self.board!r})" if __name__ == "__main__": import ttt_terminal #basictttgame.py ~ an extensible frontend demo for the backend from tttcore import * from pygame import mixer as audio import os ASSETS_FOLDER = "assets" AUDIO_FOLDER = os.path.join(ASSETS_FOLDER, "audio") SFX_FOLDER = os.path.join(AUDIO_FOLDER, "sfx") def cls(): os.system('cls' if os.name=='nt' else 'clear') class Colors: """Credit: https://stackoverflow.com/a/287944/14212394""" HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def basic_ttt_game(in_, restart): audio.init() audio.music.load(os.path.join(AUDIO_FOLDER, "bg.mp3")) audio.music.set_volume(0.1) button_sfx = audio.Sound(os.path.join(SFX_FOLDER, "button.wav")) error_sfx = audio.Sound(os.path.join(SFX_FOLDER, "error.wav")) audio.music.play(-1) while True: player = TTTPlayer.X game = TicTacToeCore() try: while True: cls() print(Colors.BOLD + "\n\n\n\tTicTacToe in the Terminal" + Colors.ENDC) print("\n\tBoard:") print(game.board_repr("\t")) print(f"\n\t{player.name} is up!") while True: try: x, y = map(int, in_()) game.move(y, x, player) button_sfx.play() break except ValueError: print(Colors.WARNING + "\tThat's not a number! >:(\n" + Colors.ENDC) error_sfx.play() except OutOfBoundsError: print(Colors.WARNING + "\tThose coordinates don't work! >:(\n" + Colors.ENDC) error_sfx.play() except SpaceOccupied: print(Colors.WARNING + "\tSomeone's already there! >:(\n" + Colors.ENDC) error_sfx.play() player = TTTPlayer.O if player == TTTPlayer.X else TTTPlayer.X except GameEnd as e: cls() button_sfx.play() print(Colors.BOLD + "\n\n\n\tTicTacToe in the Terminal" + Colors.ENDC) print("\n\tBoard:") print(game.board_repr("\t")) if type(e) == XWin: print(Colors.BOLD + Colors.OKGREEN + "\n\tX Wins!\n" + Colors.ENDC) elif type(e) == OWin: print(Colors.BOLD + Colors.OKGREEN + "\n\tO Wins!\n" + Colors.ENDC) else: print(Colors.BOLD + Colors.FAIL + "\n\tCat's Game!\n" + Colors.ENDC) print("\tHit any button to restart!") restart() #ttt_terminal.py ~ a simple terminal-based test of the backend using the above script from basictttgame import * basic_ttt_game(lambda: input("\tX-Coordinate Of Move: "), input("\tY-Coordinate Of Move: "))