Last active
April 1, 2023 03:57
-
-
Save 404Wolf/08c6e781a24c797af8739cee2e3339b9 to your computer and use it in GitHub Desktop.
A simple tictactoe game implementation in Python
This file contains 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
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 |
This file contains 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
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