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 TestGame(unittest.TestCase): def setUp(self): self.game = Game(Player.X) def test_initial_game(self): game = Game(Player.X) self.assertIs(game.human, Player.X) self.assertIs(game.computer, Player.O) self.assertEqual(game.spaces, [None] * 9) game = Game(Player.O) self.assertIs(game.human, Player.O) self.assertIs(game.computer, Player.X) self.assertEqual(game.spaces, [None] * 9) def test_move(self): self.game.move(Player.X, 0) self.assertIs(self.game.spaces[0], Player.X) def test_invalid_move(self): self.assertRaises(Game.InvalidMove, self.game.move, Player.O, 9) def test_already_occupied(self): self.game.move(Player.X, 0) self.assertRaises(Game.AlreadyOccupied, self.game.move, Player.O, 0) def test_get_player_spaces(self): self.game.move(Player.X, 0) self.assertEqual(self.game.get_player_spaces(Player.X), frozenset([0])) self.game.move(Player.O, 1) self.assertEqual(self.game.get_player_spaces(Player.O), frozenset([1])) self.game.move(Player.X, 2) self.assertEqual(self.game.get_player_spaces(Player.X), frozenset([0, 2])) self.game.move(Player.O, 3) self.assertEqual(self.game.get_player_spaces(Player.O), frozenset([1, 3])) def test_get_empty_spaces(self): self.game.move(Player.X, 0) self.assertEqual(self.game.get_empty_spaces(), frozenset(i for i in range(9) if i not in range(1))) self.game.move(Player.O, 1) self.assertEqual(self.game.get_empty_spaces(), frozenset(i for i in range(9) if i not in range(2))) self.game.move(Player.X, 2) self.assertEqual(self.game.get_empty_spaces(), frozenset(i for i in range(9) if i not in range(3))) self.game.move(Player.O, 3) self.assertEqual(self.game.get_empty_spaces(), frozenset(i for i in range(9) if i not in range(4))) def test_get_winner(self): self.game.move(Player.X, 0) self.assertIsNone(self.game.get_winner()) self.game.move(Player.O, 1) self.assertIsNone(self.game.get_winner()) self.game.move(Player.X, 3) self.assertIsNone(self.game.get_winner()) self.game.move(Player.O, 4) self.assertIsNone(self.game.get_winner()) self.game.move(Player.X, 6) self.assertIs(self.game.get_winner(), Player.X) self.game = Game(Player.X) self.game.move(Player.X, 0) self.game.move(Player.O, 2) self.game.move(Player.X, 3) self.game.move(Player.O, 4) self.game.move(Player.X, 5) self.game.move(Player.O, 6) self.assertIs(self.game.get_winner(), Player.O) def test_get_state(self): # Ensure state is incomplete until human wins. self.assertIs(self.game.get_state(), Game.State.Incomplete) self.game.move(Player.X, 0) self.assertIs(self.game.get_state(), Game.State.Incomplete) self.game.move(Player.O, 1) self.assertIs(self.game.get_state(), Game.State.Incomplete) self.game.move(Player.X, 3) self.assertIs(self.game.get_state(), Game.State.Incomplete) self.game.move(Player.O, 4) self.assertIs(self.game.get_state(), Game.State.Incomplete) self.game.move(Player.X, 6) self.assertIs(self.game.get_state(), Game.State.HumanWins) # Reset the game and check for computer wins. self.game = Game(Player.X) self.game.move(Player.X, 0) self.game.move(Player.O, 2) self.game.move(Player.X, 3) self.game.move(Player.O, 4) self.game.move(Player.X, 5) self.game.move(Player.O, 6) self.assertIs(self.game.get_state(), Game.State.ComputerWins) # Reset the game and check for tie. self.game = Game(Player.X) self.game.move(Player.X, 0) self.game.move(Player.O, 1) self.game.move(Player.X, 2) self.game.move(Player.X, 3) self.game.move(Player.O, 4) self.game.move(Player.X, 5) self.game.move(Player.O, 6) self.game.move(Player.X, 7) self.game.move(Player.O, 8) self.assertIs(self.game.get_state(), Game.State.Tie) def test_state_value(self): self.assertLess(Game.State.HumanWins.value, Game.State.Tie.value) self.assertLess(Game.State.Tie.value, Game.State.ComputerWins.value) def test_minimax(self): x = Player.X o = Player.O _ = None # Test out various game states to ensure the computer moves correctly. self.game.spaces = [ x, _, o, x, _, _, _, o, _, ] self.game.computer_move() self.assert_outcome(self.game, [ x, _, o, x, _, _, o, o, _, ]) self.game.spaces = [ x, x, _, _, _, _, _, o, _, ] self.game.computer_move() self.assert_outcome(self.game, [ x, x, o, _, _, _, _, o, _, ]) def test_show(self): self.assertEqual(self.game.show(), "\n".join(( " 0 | 1 | 2 ", "---+---+---", " 3 | 4 | 5 ", "---+---+---", " 6 | 7 | 8 ", ))) self.game.move(Player.X, 0) self.assertEqual(self.game.show(), "\n".join(( " X | 1 | 2 ", "---+---+---", " 3 | 4 | 5 ", "---+---+---", " 6 | 7 | 8 ", ))) self.game.move(Player.O, 4) self.assertEqual(self.game.show(), "\n".join(( " X | 1 | 2 ", "---+---+---", " 3 | O | 5 ", "---+---+---", " 6 | 7 | 8 ", ))) def assert_outcome(self, game, outcome: [Player]): if game.spaces != outcome: actual = Game.format_board(*(x or ' ' for x in game.spaces)) expected = Game.format_board(*(x or ' ' for x in outcome)) raise AssertionError('\nExpected:\n{}\nActual:\n{}' .format(expected, actual))