class ShellProgram: """This is the main class that manages the terminal around the game logic. To run this program, instantiate this class and call the run method. >>> ShellProgram().run() """ title = 'Tic Tac Toe' def __init__(self, *args): """Initialize core instance variables. Note that args are parsed in the parse_args() method. """ self.args = args self.game = None """:type: Game""" self.state = None """:type: Game.State""" self._quit = False @staticmethod def parse_args(args: tuple): """Handle arguments passed to __init__(). --colorized - Use ANSI terminal colors for player moves. """ if '--colorized' in args: Game.colorized = True def run(self): """Begin running the shell and initialize the game.""" self.display_title() player = self.choose_player() self.game = Game(player) self.parse_args(self.args) self.game_loop() def game_loop(self): """Handle the main game loop by iteratively checking the state. Terminate once self._quit == False.""" while not self._quit: self.display_board() self.human_move() self.handle_state() if self.state is not Game.State.Incomplete: self.display_board() self.display_final_state_message() if not self.choose_play_again(): self.quit() else: self.game.reset_board() def handle_state(self): """Call into the game and request a new state. Updates self.state.""" self.state = self.game.handle_state() def get_final_state_message(self) -> str: """Based on the end-game state, present different messages.""" if self.state is Game.State.ComputerWins: return 'The computer wins. Surprised?' elif self.state is Game.State.HumanWins: return 'Somehow you won, which is apparently impossible.' elif self.state is Game.State.Tie: return "It's a tie. How exciting." else: return "Ummmm, I'm not quite sure what happened here." def quit(self): """Wrapper to set self._quit to True which will, in turn, terminate the game_loop(). """ self._quit = True # IO methods. @classmethod def display_title(cls): """Print the main title of the game defined at the top of the class.""" print(cls.title) def display_final_state_message(self): """Retrieve the final state message and print it.""" print(self.get_final_state_message()) @classmethod def choose_play_again(cls): """Prompt the user to play again.""" while True: choice = cls.input('Would you like to play again? (y/n) ').lower() if choice == 'y': return True elif choice == 'n': return False else: print("I'm sorry, I didn't understand. Please type y or n.") @classmethod def choose_player(cls) -> Player: """Prompt the user to choose a player.""" while True: choice = cls.input('Choose your player (X/O): ') try: return Player(choice.upper()) except ValueError: print('Invalid choice, please try again.') def human_move(self): """Handle the move made by the user. If invalid, display a message to the user and prompt for a new move. """ while True: try: move = int(self.prompt_for_move()) self.game.move(self.game.human, move) return except ValueError: self.display_error('Invalid move, please try again.') except (Game.InvalidMove, Game.AlreadyOccupied) as e: self.display_error(e) def prompt_for_move(self) -> str: """Prompt the user for a move.""" return self.input('Where would you like to move? ') def display_board(self): """Print the current game board.""" print(self.game.show()) # IO wrappers. @classmethod def prompt_for_quit(cls): """Wrapper to handle when the user sends Ctrl+C to an input.""" while True: try: # Put input message on next line. print() choice = input('Would you like to quit? (y/n) ').lower() if choice == 'y': cls.exit() elif choice == 'n': return else: print('Invalid choice. Please try again.') except KeyboardInterrupt: # Just bail if the user is persistent. cls.exit() @staticmethod def display_error(message: object): """Wrapper to print errors to the user.""" print(message) @classmethod def input(cls, prompt: str) -> str: """Wrapper to get input from user and handle Ctrl+C and Ctrl+D.""" while True: try: return input(prompt) except KeyboardInterrupt: # Ctrl+C - Confirm if we should exit the program. cls.prompt_for_quit() except EOFError: # Ctrl+D - Exit the program immediately. cls.exit() @staticmethod def exit(return_code: int=1): """Wrapper to force-exit the program.""" # Ensure that user prompt ends up on next line. print() exit(return_code)
class GUIProgram: """This is the main class that manages tkinter around the game logic. To run the program, instantiate this class and call the run method. >>> GUIProgram().run() """ standard_button_dimensions = { 'width': 2, 'height': 1, } def __init__(self, *args): """Initialize core instance variables. Note that args is not currently used. It is simply available to remain consistent with the shell.ShellProgram class. """ self.args = args self.app = Tk() self.game = None """:type: Game""" self.state = None """:type: Game.State""" # noinspection PyAttributeOutsideInit def run(self): """Begin running the GUI and initialize the game.""" self.choose_player() self.window = Window(self, master=self.app) self.app.title('Tic Tac Toe') self.app.resizable(width=False, height=False) self.bring_to_front() self.app.mainloop() self.app.quit() def choose_player(self): """Hides the main app temporarily so the user can pick a player.""" self.app.withdraw() ChoosePlayerDialog(self) def handle_player_choice(self, player: Player): """This is a callback used by the ChoosePlayerDialog class once the user has chosen a player. Once executed, initializes the game and shows the main app to the user. """ self.game = Game(player) self.app.deiconify() def handle_state(self): """Handle the game logic after the user has placed a move.""" self.state = self.game.handle_state() if self.state in (Game.State.ComputerWins, Game.State.HumanWins): self.colorize_winner() else: self.window.update() def colorize_winner(self): """Highlight the buttons used to win the game.""" player, play = self.game.get_winner_and_play() if player and play: for position in play: button = self.window.move_buttons[position] button.configure(highlightbackground='yellow') button.configure(background='yellow') self.window.update() def human_move(self, position): """Callback used by the Window class to handle moves.""" self.game.move(self.game.human, position) @staticmethod def bring_to_front(): """Unfortunately, OS X seems to place tkinter behind the terminal. Of all the methods out there, it seems like the best way to handle this is to make an OS call. """ if sys.platform == 'darwin': apple_script = ('tell app "Finder" to set frontmost of process ' '"Python" to true') os.system("/usr/bin/osascript -e '{}'".format(apple_script))