Last active
July 20, 2020 17:24
-
-
Save savarin/89227f559aa11f021ba75e13c18f8bca to your computer and use it in GitHub Desktop.
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
"""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