3
\$\begingroup\$

This is a basic implementation of the game of Quarto, inspired by this question to write my own:

From Wikipedia:

Quarto is a board game for two players invented by Swiss mathematician Blaise Müller in 1991.

It is played on a 4×4 board. There are 16 unique pieces, each of which is either:

  • tall or short;
  • red or blue (or a different pair of colors, e.g. light- or dark-stained wood);
  • square or circular; and
  • hollow-top or solid-top.

Players take turns choosing a piece which the other player must then place on the board. A player wins by placing a piece on the board which forms a horizontal, vertical, or diagonal row of four pieces, all of which have a common attribute (all short, all circular, etc.). A variant rule included in many editions gives a second way to win by placing four matching pieces in a 2x2 square. [variant rule is not implemented by me]

Quarto is distinctive in that there is only one set of common pieces, rather than a set for one player and a different set for the other. It is therefore an impartial game.

A piece is represented as a tuple four binary digits, for example (1, 0, 1, 1). Each number represents a different attribute by virtue of its position, so the game is won if in at least one row, column or diagonal, an item at the given position is equal for all items.

This version is "barebones" because the names of the attributes are not taken into account because I considered them superfluous to the logic of the game.

import doctest
import itertools as it

def all_equal(xs):
    """
    >>> all_equal([1, 1, 1])
    True
    >>> all_equal("foobar")
    False
    """
    return all(x == xs[0] for x in xs)

def at_least_one_element_all_in_common_at_same_index(xss):
    """
    >>> at_least_one_element_all_in_common_at_same_index([ (1, 3), (5, 3), (0, 3, 9) ])
    True
    >>> at_least_one_element_all_in_common_at_same_index(["foobar", "zzz", "yyy"])
    False
    """
    return any(all_equal(items) for items in zip(*xss))

def is_won_rows(board):
    """
    >> is_won_rows([[(1, 0), (4, 9)],
    ...             [(3, 0), (3, 8)]]) # 3 at first position in second row
    True
    """
    return any(not None in row and at_least_one_element_all_in_common_at_same_index(row) for row in board)

def columns(xss):
    """
    >>> list(columns([[1,2,3],
    ...               [4,5,6],
    ...               [7,8,9]]))
    [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
    """
    return zip(* xss)

def diagonal(xss):
    """
    >>> diagonal([[1,2,3],
    ...          [4,5,6],
    ...          [7,8,9]])
    [1, 5, 9]
    """
    return [xss[i][i] for i in range(len(xss))]

def diagonals(xss):
    """
    >>> diagonals([[1,2,3],
    ...           [4,5,6],
    ...           [7,8,9]])
    [[1, 5, 9], [3, 5, 7]]
    """

    return [diagonal(xss), diagonal([row[::-1] for row in xss])]

def is_won(board):
    """
    >>> is_won([ [(0, 1, 0, 0), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 0, 1)], # 1 at second place at first row
    ...          [(0, 0, 0, 0), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 0, 1)] ])
    True
    """
    return is_won_rows(board) or is_won_rows(columns(board)) or is_won_rows(diagonals(board))

def ask_option(prompt, choices):
    print(prompt)
    return choices[int(input('\n'.join(str(i)+" "+str(o) for i,o in enumerate(choices))+"\n"))]

def ask_coordinates(prompt):
    x, y = input(prompt).split(',')
    return int(x)-1, int(y)-1 # 1 index to 0 index

def print_board(board):
    """
    >>> print_board([[(0, 1, 0, 0), (0, 0, 0, 0), (0, 1, 1, 0), (1, 1, 0, 0)],
    ...              [(0, 1, 0, 0), (0, 1, 0, 0), (0, 1, 0, 0), (0, 1, 0, 1)],
    ...              [(0, 1, 0, 1), (0, 1, 0, 0), (0, 1, 0, 1), (0, 1, 0, 0)],
    ...              [(0, 1, 0, 0), (1, 1, 0, 0), (0, 1, 0, 0), (0, 1, 0, 1)]]) # Fake board, in reality no repetitions are allowed
    Y X 1    2    3    4    
    1   0100 0000 0110 1100
    2   0100 0100 0100 0101
    3   0101 0100 0101 0100
    4   0100 1100 0100 0101
    """
    print("Y X " + ''.join(n + "    " for n in "1234"))
    for y, row in enumerate(board, start=1):
        print(str(y) +"   "+ " ".join("...." if piece is None else ''.join(map(str, piece)) for piece in row))

def quarto_game():
    board = [ [None for _ in range(4)] for _ in range(4)]
    pieces = [(height, color, shape, top) for height in (0, 1) for color in (0, 1) for shape in (0, 1) for top in (0, 1)]
    for player in it.cycle((1, 2)):
        print_board(board)
        piece = ask_option(
            "Player {}, please select a piece for your opponent to place: ".format(1 if player == 2 else 2),
            choices=pieces)
        pieces.remove(piece)
        x, y = ask_coordinates("Player {}, make your move [x, y] format: ".format(player))
        board[y][x] = piece
        if is_won(board):
            print("Player {} has won!".format(player))
            print_board(board)
            break

if __name__ == "__main__":
    doctest.testmod()
    quarto_game()
\$\endgroup\$
1
\$\begingroup\$

Some naming considerations.

is_

is_won_rows(board) is a weird name just omit the is prefix to become won_rows. Similarly: is_won(board) -> won(board). The term primitive is often used. Furthermore the statement:

    if is_won(board):
        print("Player {} has won!".format(player))
        print_board(board)
        break

Reads awkwardly. I kind of like the idea of maybe even using finished.

    if finished(board):
        ...

Too long

at_least_one_element_all_in_common_at_same_index is way to long. It is hard to read and interrupts the flow of statements that call that function. shared_index may be a better alternative. You probably wont get a name quite as descriptive as what you came up with but I would just write it in the docstring.

XSS

xss sounds like you're talking about an XSS exploit or something. Why do you use that as a variable name? Why not board or mat? (Same goes for diagonal etc.)

Some hardcoded stuff.

1 if player == 2 else 2

I don't like the inline if. I would abstract this into some sort of negate/other dictionary so you can do other[player].

Game representation

Although Python rarely bit twiddles like some other languages. When I see the tuple of 1s and 0s I immediately think this probably could be naturally expressed as a binary number. You gain very little to no abstraction barriers representing the stuff as a binary tuple. I would advise rewriting as a binary number.

This should simplify the creation of pieces, and the printing.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.