Skip to content

Instantly share code, notes, and snippets.

@jtmcx
Last active September 4, 2019 18:31
Show Gist options
  • Save jtmcx/8ad3e1b55c1a0a66b2e5962784f029cb to your computer and use it in GitHub Desktop.
Save jtmcx/8ad3e1b55c1a0a66b2e5962784f029cb to your computer and use it in GitHub Desktop.
A simple interactive command-line tic-tac-toe game.
#!/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