Last active
September 4, 2019 18:31
-
-
Save jtmcx/8ad3e1b55c1a0a66b2e5962784f029cb to your computer and use it in GitHub Desktop.
A simple interactive command-line tic-tac-toe game.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from enum import Enum | |
HELP_MESSAGE = """\ | |
To play tic-tac-toe, specify which cell you would like to place your Xs | |
and Os by using any combination of the words 'top', 'bottom', 'center', | |
'right', and 'left'. For example, these are valid commands: | |
> top right | |
> center | |
> bottom center""" | |
WORDS = { | |
frozenset({"top", "left"}): (0, 0), | |
frozenset({"top", "center"}): (0, 1), | |
frozenset({"top", "right"}): (0, 2), | |
frozenset({"center", "left"}): (1, 0), | |
frozenset({"center"}): (1, 1), | |
frozenset({"center", "right"}): (1, 2), | |
frozenset({"bottom", "left"}): (2, 0), | |
frozenset({"bottom", "center"}): (2, 1), | |
frozenset({"bottom", "right"}): (2, 2), | |
} | |
class InvalidMoveError(Exception): | |
"""Exception raised when a player attempts to make an invalid move, | |
i.e., when a player tries to place a tile over an existing one.""" | |
pass | |
class Tile(Enum): | |
"""Enumeration of all possible states of a tic-tac-toe square.""" | |
E = " " # An empty tile. | |
X = "X" # Player 'X' | |
O = "O" # Player 'O' | |
def __str__(self): | |
return self.value | |
class Board: | |
"""A tic-tac-toe board.""" | |
def __init__(self): | |
"""Initialize a new board to nine empty tiles.""" | |
self.tiles = [Tile.E for _ in range(9)] | |
def get(self, row, col): | |
"""Return the tile at the given 'row' and 'col'""" | |
self._validate_coords(row, col) | |
return self.tiles[row * 3 + col] | |
def set(self, row, col, tile): | |
"""Set the tile at the given 'row' and 'col' to the | |
provided value.""" | |
self._validate_coords(row, col) | |
if self.get(row, col) is not Tile.E: | |
raise InvalidMoveError() | |
self.tiles[row * 3 + col] = tile | |
def check(self): | |
"""Check if a player has won the game. Return the tile of the | |
winning player if there is one. Otherwise, return Tile.E. If the | |
game is a stalemate, return None.""" | |
spans = [ | |
set(self.tiles[0:3]), # Top row | |
set(self.tiles[3:6]), # Center row | |
set(self.tiles[6:9]), # Bottom row | |
set(self.tiles[0::3]), # Left column | |
set(self.tiles[1::3]), # Center column | |
set(self.tiles[2::3]), # Right column | |
set(self.tiles[::4]), # First diagonal | |
set(self.tiles[2:7:2]), # Second diagonal | |
] | |
if Tile.E not in self.tiles: | |
return None | |
for s in spans: | |
if len(s) == 1 and Tile.E not in s: | |
return s.pop() | |
return Tile.E | |
def _validate_coords(self, row, col): | |
"""Throw a RuntimeError if either the given 'row' or 'col' | |
are out of bounds.""" | |
if not (0 <= row and row < 3): | |
raise RuntimeError("row out of bounds") | |
if not (0 <= col and col < 3): | |
raise RuntimeError("col out of bounds") | |
def __str__(self): | |
fmt = [ | |
" . | . | . ", | |
"---+---+---", | |
" . | . | . ", | |
"---+---+---", | |
" . | . | . ", | |
] | |
fmt = "\n".join(fmt).replace(".", "{}") | |
return fmt.format(*self.tiles) | |
if __name__ == '__main__': | |
board = Board() | |
turn = Tile.X | |
print(HELP_MESSAGE) | |
print(board) | |
while True: | |
try: | |
# Prompt the user for a command | |
cmd = input("{}> ".format(turn)).strip() | |
except EOFError: | |
break | |
# Print the help message if requested. | |
if cmd == "help": | |
print(HELP_MESSAGE) | |
continue | |
# Parse the coordinates provided | |
coord = WORDS.get(frozenset(cmd.split())) | |
if coord is None: | |
print("I don't understand. Try running 'help'.") | |
continue | |
try: | |
row, col = coord | |
board.set(row, col, turn) | |
except InvalidMoveError: | |
print("Invalid move. That square isn't empty!") | |
continue | |
winner = board.check() | |
if winner is None: | |
print("Cat game :(. Better luck next time.") | |
break | |
if winner != Tile.E: | |
print("The winner is {}!".format(winner)) | |
break | |
# Swap the player | |
turn = Tile.O if turn is Tile.X else Tile.X | |
print(board) | |
print("bye!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment