Skip to content

Instantly share code, notes, and snippets.

@savarin
Last active July 20, 2020 17:24
Show Gist options
  • Save savarin/89227f559aa11f021ba75e13c18f8bca to your computer and use it in GitHub Desktop.
Save savarin/89227f559aa11f021ba75e13c18f8bca to your computer and use it in GitHub Desktop.
"""Tic Tac Toe game consisting of (1) minimal logic to hold board state, and (2) wrapper to
improve user interface.
"""
from builtins import input
from builtins import object
from typing import List, Tuple, Optional
PRETTY_BOARD = """
a b c
1 {} | {} | {}
--+---+--
2 {} | {} | {}
--+---+--
3 {} | {} | {}
"""
class Board(object):
def __init__(self):
# type: () -> None
"""Initializes Tic Tac Toe board, represented as a list of strings, as well as a counter to
track number of pieces played.
Minimal logic created to hold the board state.
"""
# state
self.board = [" "] * 9
self.counter = 0
# static
self.pieces = ("x", "o")
self.lines = [
(0, 1, 2), # horizontal wins
(3, 4, 5),
(6, 7, 8),
(0, 3, 6), # vertical wins
(1, 4, 7),
(2, 5, 8),
(0, 4, 8), # diagonal wins
(2, 4, 6),
]
def has_winner(self):
# type: () -> bool
"""Checks if there is a winner."""
for i, j, k in self.lines:
if self.board[i] != " " and self.board[i] == self.board[j] == self.board[k]:
return True
return False
def is_playable(self):
# type: () -> bool
"""Checks if there are still moves to play."""
return self.counter < 9
def place_piece(self, piece, location):
# type: (str, int) -> Tuple[bool, Optional[str]]
"""Sets the piece at the given location on the board, returns a tuple with the first value
True if successful and False otherwise.
"""
if location < 0 or location > 8:
return False, "Valid locations are 0-8"
elif piece not in self.pieces:
return False, "Valid pieces are '{}' and '{}'".format(self.pieces[0], self.pieces[1])
elif not self.board[location] == " ":
return False, "Location already taken"
self.board[location] = piece
self.counter += 1
return True, None
def expose_board(self):
# type: () -> List[str]
"""Returns current state of the board."""
return self.board
def expose_counter(self):
# type: () -> int
"""Returns current state of the counter."""
return self.counter
class Game(object):
def __init__(self):
# type: () -> None
"""Sets up the Tic Tac Toe board and runs logic for a two-player game.
Wrapper around the minimal board - improves the user interface via use of coordinate grid
and conversion from grid to index.
"""
self.board = Board()
self.pieces = self.board.pieces
@staticmethod
def convert_grid(grid):
# type: (str) -> int
"""Converts grid in string representation to index location on the board."""
if len(grid) != 2:
raise IndexError("Require string of length 2")
column = grid[0]
row = grid[1]
if column not in ("a", "b", "c"):
raise ValueError("Columns should be a-c")
elif row not in ("1", "2", "3"):
raise ValueError("Rows should be 1-3")
return "123".index(row) * 3 + "abc".index(column)
def place_piece(self, piece, location):
# type: (str, int) -> None
"""Sets the piece at the given location on the board, and raises an error if piece placement
not successful."""
result, reason = self.board.place_piece(piece, location)
if not result:
raise ValueError(reason)
def show_board(self):
# type: () -> None
"""Prints user-friendly board with player state."""
print(PRETTY_BOARD.format(*self.board.expose_board()))
@staticmethod
def show_error(message):
# type: () -> None
"""Prints error message for player."""
print("ERROR: {}, please try again\n".format(message))
def run(self):
print("Starting new game")
self.show_board()
while True:
if not self.board.is_playable():
print("DRAW: No further moves available!\n")
break
# Each player takes turns based on pieces played
player = self.board.expose_counter() % 2
piece = self.pieces[player]
while True:
grid = input("Player {} to place {} at grid: ".format(player + 1, piece))
# Ensure player (1) inputs a valid grid and (2) piece is successfully placed, then
# break out of inner loop if so.
try:
location = self.convert_grid(grid)
self.place_piece(piece, location)
self.show_board()
break
except IndexError as e:
self.show_error(e)
continue
except ValueError as e:
self.show_error(e)
continue
if self.board.has_winner():
print("VICTORY: Player {} wins!\n".format(player + 1))
break
def test_conversion():
# type: () -> None
"""Checks grid converts to index location as expected."""
assert Game.convert_grid('a1') == 0
assert Game.convert_grid('b2') == 4
assert Game.convert_grid('c3') == 8
if __name__ == "__main__":
game = Game()
game.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment