Skip to content

Instantly share code, notes, and snippets.

@404Wolf
Last active April 1, 2023 03:57
Show Gist options
  • Save 404Wolf/08c6e781a24c797af8739cee2e3339b9 to your computer and use it in GitHub Desktop.
Save 404Wolf/08c6e781a24c797af8739cee2e3339b9 to your computer and use it in GitHub Desktop.
A simple tictactoe game implementation in Python
from string import ascii_uppercase
from typing import Tuple, Literal
class Board:
"""
A tictactoe board.
A container to hold the current state of the game and provide useful methods like
checking whether a player has won.
Attributes:
state: List of three lists of three letters "O" or "X." This is the actual
current state of the board.
Methods:
winner: Obtain the current winner, or None if nobody has yet won.
parse_index: Convert a letter-number index (i.e. A2 or B3) into a row and
column index (i.e. (0, 1) or (1, 2).
deparse_index: Convert a row and column index (i.e. (0, 1) or (1, 2) into a
letter-number index (i.e. A2 or B3).
"""
def __init__(self) -> None:
"""Initialize the tictactoe board."""
self.state = [None] * 3, [None] * 3, [None] * 3
def __setitem__(self, index: str, value: Literal["O", "X", None]) -> None:
"""
Set/clear an item on the tictactoe board with a row-number index.
Args:
index: A letter-number syntax index on the tictactoe board. For instance,
"A2."
value: Either X, O, or None.
"""
column, row = self.parse_index(index)
self.state[row][column] = value
def __getitem__(self, index: str) -> Literal["O", "X", None]:
"""
Fetch an item on the tictactoe board.
Args:
index: A letter-number syntax index on the tictactoe board. For instance,
"A2."
"""
column, row = self.parse_index(index)
return self.state[row][column]
def __str__(self) -> str:
"""Obtain string representation of the current tictactoe board."""
# Create a line to separate rows
line = "\n-------------\n"
# Create an empty string to hold the output, and iterate over the rows to build
# the output
output = ""
for row_index, row in enumerate(self.state):
# Print row/column index in blank columns
for column_index, column in enumerate(row):
if column: # If the column isn't None then print its value
output += f"{column} | "
else: # If the column is None then print the column/row index as string
output += f"{self.deparse_index(row_index, column_index)} | "
# Trim off the whitespace and add a blank line
output = output[:-2]
output += line
# Remove the trailing line and return the output
output = output[: -len(line)]
return output
def full(self):
"""Whether all spaces on the board are full."""
# Check to see if any item in any row is not None
for row in self.state:
for spot in row:
# If even a single spot is not None the board is not full
if spot is None:
return False
# Since we made it all the way through we can conclude that the board is full.
return True
def winner(self) -> Literal["X", "O", "XO", None]:
"""Obtain the current winner, or None if nobody has yet won."""
test_winner: Literal["X", "O"]
# Iterate over both players to see if either has won
for test_winner in ("O", "X"):
# 1) Check to see if there are any winning columns
for column in self.state:
if column == [test_winner] * 3:
return test_winner
# 2) Check to see if there are any winning rows
if column[0] == column[1] == column[2] == test_winner:
return test_winner
# 3) Check to see if there are any winning diagonals
if self["A1"] == self["B2"] == self["C3"] == test_winner:
return test_winner
if self["C1"] == self["B2"] == self["A3"] == test_winner:
return test_winner
# Now see if the board is full, in which case both X and O have won.
# Otherwise, return None to indicate that nobody has won yet.
if self.full():
return "XO"
else:
return None
@staticmethod
def deparse_index(row, column) -> str:
"""
Deparse a row-column index into a letter-number index.
Args:
row: The row of the index. This is index-0.
column: The column of the index. This is index-0.
Returns:
The letter-number index. For instance, A2.
"""
return ascii_uppercase[column] + str(row + 1)
@staticmethod
def parse_index(index) -> Tuple[int, int]:
"""
Parse a letter-column number-row index into a tuple of two integers.
Args:
index: The index in letter-column form. For instance, A2. The number (row)
should be index-1 and the letter should range from A-Z.
Returns:
Integer representing the column and row, respectively. Both are returned
in index-0 form.
"""
return ascii_uppercase.find(index[0]), int(index[1]) - 1
from typing import Literal
from board import Board
from re import fullmatch
def main():
# Create an interface for the tictactoe game
interface = Board()
print("Welcome to TicTacToe!")
print("On your turn, enter the letter/number coord you'd like to mark at.")
print('"A1" represents the top left corner, and "C3" the bottom left.')
# Display the current game board.
print(f"\nCurrent board:\n{interface}\n")
# Whose turn it is. This alternates.
current_turn: Literal["O", "X"] = "X"
# Continue the game
while True:
# Check if they have won. If they haven't, allow for the next user to take
# their turn.
if winner := interface.winner():
if winner == "XO":
print("Whoops! It's a tie!")
else:
print(f"Congratulations! {winner} has won!")
print("Ending game...")
break # End game
else:
print(f"Enter the location for {current_turn} to mark off.")
# By default, they have not chosen a spot to place their mark.
spot = None
while spot is None:
# Change spot to be the location that they enter.
spot = input(f">>> ")
# Ensure that the spot that they entered is a capital letter between
# A and C followed by a number between 1 and 3.
if fullmatch(r"[A-C][1-3]", spot):
if interface[spot] is None:
# If they entered a valid move, change the interface,
# and print the new board.
interface[spot] = current_turn
print(f"\n{interface}")
# Swap the current turn to "X" if it is "O" or "O" if it is "X"
match current_turn:
case "O":
current_turn = "X"
case "X":
current_turn = "O"
else:
print("You can't overwrite someone else's move!")
print("Please choose a different spot.")
else:
print("Invalid input! Enter a valid letter/number combination.")
spot = None
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment