6
\$\begingroup\$

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: ")) 
\$\endgroup\$

3 Answers 3

2
\$\begingroup\$

UX

When I run the code, I see this:

Y-Coordinate Of Move: 

I understand that it is prompting me for something, but I don't know what to enter. I'm not sure how a "Y-Coordinate" relates to tic-tac-toe.

It would be helpful if the prompt was changed to explicitly tell the user to enter something and what the valid type and range is.

I can't run the code any further than that because I don't have any of the required input files (.mp3, etc.).

DRY

In the basic_ttt_game function, many of the print lines follow a pattern:

  • Starting color code(s)
  • Newline at the end of the message
  • ENDC code

Consider wrapping that up into a helper function which accepts start codes and a unique message string.

Documentation

The PEP 8 style guide recommends adding docstrings for classes and functions.

For example:

def board_repr(self, prefix=" "): """State what this function does""" 
\$\endgroup\$
2
\$\begingroup\$

These definitions seem to be specific to ANSI terminals:

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' 

Given that other kinds of terminal exist, I recommend using the TERM environment variable to select the escape codes that the program should actually use.

\$\endgroup\$
2
\$\begingroup\$

Clearing the Screen

The code is very heavy-weight:

def cls(): os.system('cls' if os.name=='nt' else 'clear') 

It is spawning an entirely new process to clear the screen. It would also be a vector for arbitrary code execution if an executable file named something like “clear” or “cls.bat” was somehow introduced into a directory on the search path.

Confusingly, ANSI escape sequences are already used for coloured output. So, why not just use the sequence for clearing the screen?

def cls(): print('\033[2J', end='') 
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.