Skip to content

Instantly share code, notes, and snippets.

@MartinHarding
Last active November 10, 2021 14:17
Show Gist options
  • Save MartinHarding/c8c2c9adf853d4703015fdbed6fa4f0b to your computer and use it in GitHub Desktop.
Save MartinHarding/c8c2c9adf853d4703015fdbed6fa4f0b to your computer and use it in GitHub Desktop.
Tic-tac-toe

Tic-tac-toe

Play Tic-tac-toe on your command line with up to four players on boards from 3x3 to 9x9 in size.

Example:

          _______            __                   __
         /_  __(_)____      / /_____ ______      / /_____  ___
          / / / / ___/_____/ __/ __ `/ ___/_____/ __/ __ \/ _ \
         / / / / /__/_____/ /_/ /_/ / /__/_____/ /_/ /_/ /  __/
        /_/ /_/\___/      \__/\__,_/\___/      \__/\____/\___/

Enter a board size between '3' and '9': 3
Enter a number of human players between '1' and '2': 1

   │  1  │  2  │  3  │
───┼─────┼─────┼─────┤
a  │  O  │     │     │
───┼─────┼─────┼─────┤
b  │     │  O  │     │
───┼─────┼─────┼─────┤
c  │  X  │  X  │     │
───┴─────┴─────┴─────┘
Enter a move for player 'X': c3

Install / Play

git clone https://gist.github.com/MartinHarding/c8c2c9adf853d4703015fdbed6fa4f0b tictactoe
cd tictactoe
python tictactoe.py
class Board:
"""Store and render the state of a game played on a grid using simple tokens.
"""
def __init__(self, rows, columns):
self.rows = rows
self.columns = columns
self.matrix = [[None for c in range(0, columns)]
for r in range(0, rows)]
def set_cell(self, coordinates, data):
"""Set the contents of a cell
Args:
coordinates (tuple): x and y coordinates of the cell being set.
data (str): data to store in the cell (usually a single character).
"""
self.matrix[coordinates[0]][coordinates[1]] = data
def get_available_cells(self):
"""Get a list of tuples representing cell coordinates which have not been set
Returns:
list: coordinates of empty cells on the board
"""
available_cells = []
for row_index, row in enumerate(self.matrix):
for column_index, cell in enumerate(row):
if cell is None:
available_cells.append((row_index, column_index))
return available_cells
def check_for_win(self):
"""Checks the board for a horizontal, vertical, or diagonal unbroken line of characters.
Returns:
bool: Whether or not an unbroken line was detected in any direction.
"""
if self._check_for_horizontal_win():
return True
if self._check_for_vertical_win():
return True
if self._check_for_diagonal_win():
return True
return False
def render(self):
"""Renders a representation of the board (including a coordinate system) to stdout
"""
print('\n\n')
header = [' ']
divider = ['─']
footer = ['─']
for column_index in range(1, self.columns + 1):
header.append(str(column_index))
if column_index < self.columns :
divider.append('─')
footer.append('─')
header.append('')
print(' │ '.join(header))
for row_index, row in enumerate(self.matrix):
print('──┼──'.join(divider + ['───┤']))
row_data = [chr(row_index+97)] + [' ' if c is None else c for c in row]
print(' │ '.join(row_data + ['']))
print('──┴──'.join(divider + ['───┘']))
def _rotated(self):
"""Get a rotated representation of the board matrix (useful for checking for unbroken rows)
Returns:
Matrix: Clockwise rotated board matrix.
"""
return [list(x) for x in zip(*self.matrix)][::-1]
def _check_for_horizontal_win(self):
"""Checks the board for a horizontal unbroken line of characters.
Returns:
bool: Whether or not an unbroken line was detected horizontally.
"""
return self._check_rows_for_win(self.matrix)
def _check_for_vertical_win(self):
"""Checks the board for a vertical unbroken line of characters.
Returns:
bool: Whether or not an unbroken line was detected vertically.
"""
return self._check_rows_for_win(self._rotated())
def _check_for_diagonal_win(self):
"""Checks the board for a diagonally unbroken line of characters.
Returns:
bool: Whether or not an unbroken line was detected diagonally.
"""
diagonals = [[], []]
for row_index, row in enumerate(self.matrix):
diagonals[0].append(row[row_index])
diagonals[1].append(row[len(self.matrix) - row_index - 1])
return self._check_rows_for_win(diagonals)
def _check_rows_for_win(self, rows):
"""Checks each row to see if there is an unbroken line of characters.
Args:
rows (list): Rows to check for an unbroken line of characters.
Returns:
bool: Whether or not an unbroken line was detected in the rows.
"""
for row in rows:
if row[0] is None:
# Since we know an unbroken row must begin with a non-null value, we can just skip this row saving
# previous CPU cycles
continue
if ''.join([c for c in row if c]) == row[0] * self.rows:
return True
import random
from utils import board_coordinate_input, int_between_input, clamp
from board import Board
class TicTacToe:
"""Play a game of Tic-tac-toe on an interactive shell.
"""
def __init__(self):
self._print_title()
self.board = self.setup_board()
self.players = self.setup_players()
self.play()
def play(self):
"""Start the game / main loop.
"""
current_player = self.players[0]
self.board.render()
while True:
self.move(current_player)
self.board.render()
if self.board.check_for_win():
print("'{}' wins the game!".format(current_player['symbol']))
break
if not bool(self.board.get_available_cells()):
# TODO: make this able to tell if there are any moves that would result in a win left
print('Draw')
break
next_player_index = self.players.index(current_player) + 1
if next_player_index > len(self.players) - 1:
current_player = self.players[0]
else:
current_player = self.players[next_player_index]
def setup_board(self):
"""Configure the board.
Returns:
Board: An object representing the play space.
"""
self.board_size = int_between_input(3, 9, 'board size')
return Board(self.board_size, self.board_size)
def setup_players(self):
"""Configure the players.
Returns:
list: A list of dictionaries representing the players in the game.
"""
max_players = clamp(2, 4, self.board_size - 2)
humans = int_between_input(0, max_players, 'number of human players')
min_computers = clamp(0, max_players, 2 - humans)
max_computers = max_players - humans
if max_computers == min_computers:
computers = min_computers
elif max_computers > 0:
computers = int_between_input(min_computers, max_computers, 'number of computer players')
else:
computers = 0
symbols = ['X', 'O', 'Y', 'Z']
players = []
for i in range(0, humans):
players.append({'symbol': symbols[i], 'human': True})
for i in range(0, computers):
players.append({'symbol': symbols[i+humans], 'human': False})
return players
def move(self, player):
"""Make a move for a given player
Args:
player (dict): a dictionary representing the player
"""
if player['human']:
move = board_coordinate_input(board=self.board,
prompt="Enter a move for player '{}': ".format(player['symbol']))
else:
# TODO: Write an actual AI for the computers
move = random.choice(self.board.get_available_cells())
self.board.set_cell(move, player['symbol'])
def _print_title(self):
"""Prints a nice little title screen.
"""
title = """
_______ __ __
/_ __(_)____ / /_____ ______ / /_____ ___
/ / / / ___/_____/ __/ __ `/ ___/_____/ __/ __ \/ _ \\
/ / / / /__/_____/ /_/ /_/ / /__/_____/ /_/ /_/ / __/
/_/ /_/\___/ \__/\__,_/\___/ \__/\____/\___/
"""
print(title)
TicTacToe()
def int_between_input(minimum, maximum, label="number"):
"""Get a number between the minimum and maximum from the user.
Args:
minimum (int): Allowable input minimum; values less than this will parameter will be rejected.
maximum (int): Allowable input maximum; values more than this will parameter will be rejected.
label (str, optional): Name of the thing you are getting to display in messages. Defaults to "number".
Returns:
int: a number between the minimum and maximum input by the user.
"""
while True:
prompt = "Enter a {} between '{}' and '{}': ".format(label, minimum, maximum)
try:
number = int(input(prompt))
except ValueError:
print("{} must be a number".format(label.capitalize()))
continue
if not minimum <= number <= maximum:
print("{} must be between '{}' and '{}'".format(label.capitalize(), minimum, maximum))
continue
return number
def board_coordinate_input(board, prompt='Enter a coordinate in the form <row number><column letter>: '):
"""Get a two-dimensional coordinate on a given board from the user.
Args:
board (Board): the Matrix for which you want to enter a coordinate on.
prompt (str, optional): A prompt for the user. Defaults to 'Enter a coordinate in the form <row><column>: '.
Returns:
tuple: two integers representing the x and y coordinates on the board.
"""
while True:
move = input(prompt)
if len(move) != 2:
print("You must enter a coordinate in the form of <row number><column letter>")
continue
row = ord(move[0].lower()) - 97
column = ord(move[1].lower()) - 49
if not 0 <= row <= board.rows - 1:
print("Row (first character) must be a letter between 'a' and '{}'".format(chr(board.rows + 96)))
continue
if not 0 <= column <= board.columns - 1:
print("Column (second character) must be a number between '1' and '{}'".format(board.columns))
continue
if board.matrix[row][column]:
print("That cell is already taken by '{}'".format(board.matrix[row][column]))
continue
return (row, column)
def clamp(minimum, maximum, value):
"""Clamp a value between two other values
Args:
minimum (int): Lowest number that can be returned.
maximum (int): Highest number that can be returned.
value (int): Value to clamp (will return itself if within the range of minimum and maximum).
"""
return max(minimum, min(maximum, value))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment