Last active
September 1, 2020 20:50
-
-
Save lelandpaul/86afe39346ffc55f520c58a1a1e8ac11 to your computer and use it in GitHub Desktop.
A quick command-line implementation of Quarto.
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
#! /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