Skip to content

Instantly share code, notes, and snippets.

@lelandpaul
Last active September 1, 2020 20:50
Show Gist options
  • Save lelandpaul/86afe39346ffc55f520c58a1a1e8ac11 to your computer and use it in GitHub Desktop.
Save lelandpaul/86afe39346ffc55f520c58a1a1e8ac11 to your computer and use it in GitHub Desktop.
A quick command-line implementation of Quarto.
#! /usr/python
# This is a quick implementation of Quarto, playable from the command line.
# For game rules, see: https://en.wikipedia.org/wiki/Quarto_(board_game)
#
# The features of pieces are represented in ASCII as follows:
# +--------------+-----------------+---------------+-------------+
# |Square Round | Dark Light | Solid Hollow | Short Tall |
# | | | | _ |
# | _ _ | _ _ | _ . | _ [*] |
# | [*] (*) | [*] [o] | [*] [*] | [*] [*] |
# +--------------+-----------------+---------------+-------------+
from itertools import product, zip_longest
from random import choice
import os
class AsciiRepr:
"""
Helper class for assembling 2-dimensional ascii chunks.
"""
def __init__(self, list_of_rows):
width = max([len(r) for r in list_of_rows])
self.rows = [r + ' '*(width-len(r)) for r in list_of_rows]
def __repr__(self):
return '\n'.join(self.rows)
def __add__(self, other):
"""
Takes two blocks, aligns them at the bottom.
e.g. x x
x + y = xy
There's a minor bug here:
If the left operand is shorter than the right, they'll overlap.
e.g. y y
x + y = xy
"""
new_rows = []
for row_a, row_b in zip_longest(self.rows[::-1],
other.rows[::-1],
fillvalue=' '):
new_rows.insert(0, row_a + row_b)
return AsciiRepr(new_rows)
def __radd__(self, other):
if other == 0:
return self
else:
return self.__add__(other)
class Piece:
"""
Encodes a single Quarto piece.
"""
def __init__(self, code):
# Used for determining winners
# If we add codes pointwise, then we know we have a winning set
# when the resulting tuple contains either 4 or -4
# (i.e. one feature was either all True or all False)
self.code = tuple(1 if c else -1 for c in code)
self.tall = code[0]
self.square = code[1]
self.dark = code[2]
self.solid = code[3]
def ascii_repr(self, number=None):
cap = ' _ ' if self.solid else ' . '
center = '*' if self.dark else 'o'
if self.square:
unit = ' [' + center + '] '
else:
unit = ' (' + center + ') '
if self.tall:
rows = ['', cap, unit, unit, '']
else:
rows = ['', '', cap, unit, '']
if number is not None:
rows.insert(-1, ' ' + str(hex(number))[2:])
return AsciiRepr(rows)
def __repr__(self):
return str(self.ascii_repr())
def __add__(self, other):
# Used for determining winners: Adds tuples pointwise
try:
return tuple(x+y for x, y in zip(self.code, other.code))
except AttributeError:
# Sometimes other will be a Piece; other times it will be a tuple.
return tuple(x+y for x, y in zip(self.code, other))
def __radd__(self, other):
# Used for determining winners.
# This is necessary because sum() always starts accumulating from 0.
if other == 0:
return self
else:
return self.__add__(other)
class Board:
"""
Encodes a 4x4 board and lets us check for winning configurations.
"""
def __init__(self):
self.board = [[None for i in range(4)] for i in range(4)]
def __repr__(self):
horizontal_rule = '+' + ('-'*5 + '+')*4
rep = [horizontal_rule]
for i, row in enumerate(self.board):
row_rep = AsciiRepr(['|']*5)
for j, spot in enumerate(row):
if spot:
row_rep += spot.ascii_repr()
else:
number = str(hex(i*4 + j))[2:]
row_rep += AsciiRepr([' ' + number + ' '])
row_rep += AsciiRepr(['|']*5)
rep.append(row_rep)
rep.append(horizontal_rule)
return '\n'.join([str(r) for r in rep])
def open_positions(self):
# Used by the "AI" player (which just picks a random open position)
return [(i, j) for i, j in product(range(4), repeat=2)
if self.board[i][j] is None]
@static
def check_collection(collection):
# Given a collection of pieces,
# say whether they constitute a Quarto (4-in-a-row)
if None in collection:
return False
if len(collection) < 4:
return False
row_sum = sum(collection)
if 4 in row_sum or -4 in row_sum:
return True
return False
def has_winner(self):
# Check the board for winning configurations.
# First, check the diagonals
if Board.check_win([self.board[i][i] for i in range(4)]):
return True
if Board.check_win([self.board[i][3-i] for i in range(4)]):
return True
# Check rows:
for row in self.board:
if Board.check_win(row):
return True
# Check cols:
for j in range(4):
if Board.check_win([self.board[i][j] for i in range(4)]):
return True
return False
def __getitem__(self, i):
return self.board[i]
class Game:
"""
Encodes the entire game state.
"""
def __init__(self):
self.board = Board() # The board
self.stock = [Piece(code) for code in product(
[True, False], repeat=4)] # Uplayed pieces
self.on_deck = None # Piece selected for play
self.players_turn = True # The player will choose a piece first
def __repr__(self):
# Clear the screen
os.system('cls' if os.name == 'nt' else 'clear')
# Print the board
rep = str(self.board) + '\n\n\n'
# If there's a piece selected, note that
if self.on_deck:
rep += 'On Deck:\n' + str(self.on_deck) + '\n'
# Print the remaining stock
rep += str(sum([p.ascii_repr(number=i)
for i, p in enumerate(self.stock)]))
return rep
def game_loop():
# Initialize a game
g = Game()
while not g.board.has_winner():
# Swapping the turn here means that, if we exit the loop,
# g.players_turn will reflect who *placed* a piece last (and thus won)
g.players_turn = not g.players_turn
print(g)
if g.players_turn:
while True: # Loop to make sure the player enters a valid input
selection = input('Place piece > ')
try:
selection = int(selection, base=16)
except ValueError:
# The player entered something that couldn't be interpreted
print('Not a valid position.')
continue
if selection > 15:
# The player entered too large of a value
print('Not a valid position.')
continue
col = selection % 4
row = (selection - col) // 4
if g.board[row][col] is None:
# It was an empty position, so move on
break
else:
# Not empty — make the player choose again
print('Position already taken.')
g.board[row][col] = g.on_deck
g.on_deck = None
else:
while True: # Loop to make sure the player enters a valid input
selection = input('Choose piece > ')
selection = int(selection, base=16)
except ValueError:
# The player entered something that couldn't be interpreted
print('Not a valid position.')
continue
if selection > len(g.stock)-1:
# The player tried to select a piece that wasn't there.
print('Not a valid piece.')
continue
break
g.on_deck = g.stock.pop(selection) # Put the piece on deck
# COMPUTER'S ACTIONS:
# It would be nice to have actual AI,
# but for now just place the piece randomly
i, j = choice(g.board.open_positions())
g.board[i][j] = g.on_deck
# Randomly pick a new piece
piece = choice(range(len(g.stock)))
g.on_deck = g.stock.pop(piece)
print(g) # Print the board one last time
if g.players_turn:
print('You win!')
else:
print('You lose!')
if __name__ == '__main__':
game_loop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment