dominoes: a Python library for the game of dominoes, with an accompanying CLI and AI players

Dominoes have been around for hundreds of years, and many variations of the game have been played all over the world. This library is based on a popular variation commonly played in San Juan, Puerto Rico, and surrounding municipalities, such as Guaynabo.

It is played with a double six set of dominoes. The 28 dominoes are shuffled and distributed evenly between the 4 players, who form 2 teams. The players then take turns placing dominoes in a single chain. The first player to play all their dominoes wins the points in the remaining hands for their team. If the game is stuck, the team with the fewest points remaining in its players’ hands wins the points in all the remaining hands. For more details, see Game.

This library provides a Game class to represent a single dominoes game. It is built on top of Domino, Hand, and Board classes. Furthermore, you can string various games together and play up to a target score using the Series class.

Additionally, this package provides a command line interface to a dominoes series. Not only is it a great way to play a quick game, but it is also a comprehensive example of how to use this library’s API.

The command line interface features various artificial intelligence players. For more information on how these work, see Artificial Intelligence Players.

Install

$ pip install dominoes

Usage Example

>>> import dominoes
>>> d = dominoes.Domino(6, 6)
>>> g = dominoes.Game.new(starting_domino=d)
>>> g
Board: [6|6]
Player 0's hand: [2|4][5|5][2|3][1|3][1|6][1|2]
Player 1's hand: [1|1][3|4][0|5][0|6][2|5][1|5][2|6]
Player 2's hand: [0|4][0|3][4|4][3|6][0|2][4|5][1|4]
Player 3's hand: [5|6][3|5][3|3][0|0][0|1][2|2][4|6]
Player 1's turn
>>> g.board
[6|6]
>>> g.hands
[[2|4][5|5][2|3][1|3][1|6][1|2], [1|1][3|4][0|5][0|6][2|5][1|5][2|6], [0|4][0|3][4|4][3|6][0|2][4|5][1|4], [5|6][3|5][3|3][0|0][0|1][2|2][4|6]]
>>> g.turn
1
>>> g.result
>>> g.valid_moves # True is for the left of the board, False is for the right
[([0|6], True), ([2|6], True)]
>>> g.make_move(*g.valid_moves[0])
>>> g.moves
[([6|6], True), ([0|6], True)]
>>> g
Board: [0|6][6|6]
Player 0's hand: [2|4][5|5][2|3][1|3][1|6][1|2]
Player 1's hand: [1|1][3|4][0|5][2|5][1|5][2|6]
Player 2's hand: [0|4][0|3][4|4][3|6][0|2][4|5][1|4]
Player 3's hand: [5|6][3|5][3|3][0|0][0|1][2|2][4|6]
Player 2's turn
>>> g.make_move(*g.valid_moves[0])
...
>>> g.make_move(*g.valid_moves[0])
Result(player=1, won=True, points=-32)
>>> g.result
Result(player=1, won=True, points=-32)
>>> g
Board: [2|6][6|3][3|4][4|1][1|1][1|6][6|4][4|5][5|2][2|4][4|0][0|6][6|6][6|5][5|0][0|3][3|5][5|5][5|1][1|0]
Player 0's hand: [2|3][1|3][1|2]
Player 1's hand:
Player 2's hand: [4|4][0|2]
Player 3's hand: [3|3][0|0][2|2]
Player 1 won and scored 32 points!

Command Line Interface

$ dominoes
Welcome! Proceeding will clear all text from this terminal session. If you are OK with this, press enter to continue.
Up to how many points would you like to play: 100
Player settings:
0) Human
1) AI: random
2) AI: omniscient
Select a setting for player 0: 0
Select a setting for player 1: 1
Select a setting for player 2: 0
Select a setting for player 3: 1
Press enter to begin game 0.
Player 3 had the [6|6] and made the first move.
Board:
[6|6]
Player 0 has 7 dominoes in his/her hand.
Player 1 has 7 dominoes in his/her hand.
Player 2 has 7 dominoes in his/her hand.
Player 3 has 6 dominoes in his/her hand.
It is now player 0's turn. Press enter to continue.
Board:
[6|6]
Player 0's hand:
0) [3|6]
1) [4|4]
2) [0|1]
3) [2|6]
4) [1|1]
5) [2|5]
6) [3|3]
Choose which domino you would like to play: 3
Choose what end of the board you would like to play on (l or r): r
Press enter to end player 0's turn.
Board:
[6|6][6|2]
Player 1 (AI: random) chose to play [2|4] on the right end of the board.
Press enter to end player 1's turn.
Game over!
Board: [0|2][2|2][2|5][5|5][5|6][6|0][0|0][0|3][3|6][6|1][1|4][4|4][4|6][6|6][6|2][2|4][4|5][5|3][3|3][3|2][2|1][1|1][1|3][3|4]
Player 0's hand: [0|1]
Player 1's hand: [1|5][0|5]
Player 2's hand: [0|4]
Player 3's hand:
Player 3 won and scored 16 points!
The current state of the series:
Series to 100 points:
Team 0 has 0 points.
Team 1 has 16 points.
Press enter to begin game 1.
Game over!
Board: [5|3][3|3][3|6][6|5][5|5][5|0][0|4][4|3][3|1][1|6][6|2][2|5][5|4][4|6][6|0][0|3][3|2][2|0][0|0][0|1][1|4][4|2][2|2][2|1][1|1]
Player 0's hand: [1|5]
Player 1's hand: [4|4]
Player 2's hand:
Player 3's hand: [6|6]
Player 2 won and scored 26 points!
The current state of the series:
Series to 100 points:
Team 0 has 107 points.
Team 1 has 95 points.
Team 0 wins!
$

Artificial Intelligence Players

Players

Players are Python objects with a __call__ method defined to accept a Game instance as the sole argument. Players return None, and leave the input Game unmodified, except for its valid_moves attribute. This value may be replaced with another tuple containing the same moves, but sorted in decreasing order of preference. Players may be applied one after another for easy composability.

>>> import dominoes
>>> g = dominoes.Game.new()
>>> g.valid_moves
(([0|0], True), ([3|4], True), ([1|3], True), ([2|2], True), ([3|3], True), ([2|3], True), ([5|6], True))
>>> dominoes.players.random(g)
>>> g.valid_moves
(([5|6], True), ([1|3], True), ([3|3], True), ([2|2], True), ([0|0], True), ([2|3], True), ([3|4], True))
def double(game):
    '''
    Prefers to play doubles.

    :param Game game: game to play
    :return: None
    '''
    game.valid_moves = tuple(sorted(game.valid_moves, key=lambda m: m[0].first != m[0].second))
dominoes.players.bota_gorda(game)

Prefers to play dominoes with higher point values.

Parameters:game (Game) – game to play
Returns:None
class dominoes.players.counter(player=<function identity>, name=None)

Prefers moves in the same order as the passed-in player. Keeps a counter of the amount of times that this player gets called. An instance of this class must first be initialized before it can be called in the usual way.

Parameters:
  • player (callable) – player that determines the move preferences of this player. The identity player is the default.
  • name (str) – the name of this player. The default is the name of this class.
Variables:
  • count (int) – the amount of times that this player has been called.
  • __name__ (str) – the name of this player.
dominoes.players.double(game)

Prefers to play doubles.

Parameters:game (Game) – game to play
Returns:None
dominoes.players.identity(game)

Leaves move preferences unchanged.

Parameters:game (Game) – game to play
Returns:None
class dominoes.players.omniscient(start_move=0, player=<function identity>, name=None)

Prefers to play the move that maximizes this player’s final score, assuming that all other players play with the same strategy. This player “cheats” by looking at all hands to make its decision. An instance of this class must first be initialized before it can be called in the usual way.

Parameters:
  • start_move (int) – move number at which to start applying this player. If this player is called before the specified move number, it will have no effect. Moves are 0-indexed. The default is 0.
  • player (callable) – player used to sort moves to be explored in the underlying call to alphabeta search. Ordering better moves first may significantly reduce the amount of moves that need to be explored. The identity player is the default.
  • name (str) – the name of this player. The default is the name of this class.
Variables:

__name__ (str) – the name of this player

class dominoes.players.probabilistic_alphabeta(start_move=0, sample_size=inf, player=<function identity>, name=None)

This player repeatedly assumes the other players’ hands, runs alphabeta search, and prefers moves that are most frequently optimal. It takes into account all known information to determine what hands the other players could possibly have, including its hand, the sizes of the other players’ hands, and the moves played by every player, including the passes. An instance of this class must first be initialized before it can be called in the usual way.

Parameters:
  • start_move (int) – move number at which to start applying this player. If this player is called before the specified move number, it will have no effect. Moves are 0-indexed. The default is 0.
  • sample_size (int) – the number of times to assign random possible hands to other players and run alphabeta search before deciding move preferences. By default considers all hands that other players could possibly have.
  • player (callable) – player used to sort moves to be explored in the underlying call to alphabeta search. Ordering better moves first may significantly reduce the amount of moves that need to be explored. The identity player is the default.
  • name (str) – the name of this player. The default is the name of this class.
Variables:

__name__ (str) – the name of this player

dominoes.players.random(game)

Prefers moves randomly.

Parameters:game (Game) – game to play
Returns:None
dominoes.players.reverse(game)

Reverses move preferences.

Parameters:game (Game) – game to play
Returns:None

Search

dominoes.search.alphabeta(game, alpha_beta=(-inf, inf), player=<function identity>)

Runs minimax search with alpha-beta pruning on the provided game.

Parameters:
  • game (Game) – game to search
  • alpha_beta (tuple) – a tuple of two floats that indicate the initial values of alpha and beta, respectively. The default is (-inf, inf).
  • player (callable) – player used to sort moves to be explored. Ordering better moves first may significantly reduce the amount of moves that need to be explored. The identity player is the default.
dominoes.search.make_moves(game, player=<function identity>)

For each of a Game object’s valid moves, yields a tuple containing the move and the Game object obtained by playing the move on the original Game object. The original Game object will be modified.

Parameters:
  • game (Game) – the game to make moves on
  • player (callable) – a player to call on the game before making any moves, to determine the order in which they get made. The identity player is the default.

API Documentation

Board

class dominoes.Board

Python class for objects that represent a domino board. A domino board consists of a series of dominoes placed end to end such that the values on connected ends match.

Variables:board – deque representing the game board
>>> import dominoes
>>> d1 = dominoes.Domino(1, 2)
>>> d2 = dominoes.Domino(1, 3)
>>> b = dominoes.Board()
>>> repr(b)
''
>>> b.add(d1, True)
>>> b
[1|2]
>>> b.add(d2, False)
EndsMismatchException: [1|3] cannot be added to the right of the board - values do not match!
>>> b.add(d2, True)
>>> b
[3|1][1|2]
>>> b.left_end()
3
>>> b.right_end()
2
>>> len(b)
2
add(d, left)

Adds the provided domino to the specifed end of the board.

Parameters:
  • d (Domino) – domino to add
  • left (bool) – end of the board to which to add the domino (True for left, False for right)
Returns:

None

Raises:

EndsMismatchException – if the values do not match

left_end()
Returns:the outward-facing value on the left end of the board
Raises:EmptyBoardException – if the board is empty
right_end()
Returns:the outward-facing value on the right end of the board
Raises:EmptyBoardException – if the board is empty

Domino

class dominoes.Domino

Python class for objects that represent a domino. Each domino is a rectangular tile with a line dividing its face into two square ends. Each end is marked with an integer value, typically ranging from 0 to 6 or 9.

Parameters:
  • first (int) – value on one end
  • second (int) – value on the other end
Variables:
  • first – value on one end
  • second – value on the other end
>>> import dominoes
>>> d = dominoes.Domino(1, 2)
>>> d
[1|2]
>>> d_inv = d.inverted()
>>> d_inv
[2|1]
>>> d == d_inv
True
>>> other_d = dominoes.Domino(1, 3)
>>> d == other_d
False
>>> 2 in d
True
inverted()
Returns:a new Domino, with the same values, but in inverted positions

Game

dominoes.game.next_player(player)

Returns the player that plays after the specified player.

Parameters:player (int) – player for which to calculate the next player. Must be 0, 1, 2, or 3.
Returns:the next player
class dominoes.game.Game(board, hands, moves, turn, valid_moves, starting_player, result)

Python class for objects that represent a dominoes game.

This variation of the dominoes game is played using 28 dominoes, which use values from 0 to 6:

[0|0][0|1][0|2][0|3][0|4][0|5][0|6]
[1|1][1|2][1|3][1|4][1|5][1|6]
[2|2][2|3][2|4][2|5][2|6]
[3|3][3|4][3|5][3|6]
[4|4][4|5][4|6]
[5|5][5|6]
[6|6]

These dominoes are shuffled, and distributed evenly among 4 players. These players then sit on the edges of a square. Players sitting opposite of each other are on the same team, and the center of the square is the game board. Throughout the game, each player will only be able to see their hand, the game board, and the amount of dominoes left in the hands of the other players. Note that no player can see the values on the dominoes in the hands of the other players.

The 4 players will then take turns placing dominoes from their hands onto the game board. The game board consists of a chain of dominoes placed end to end such that the values on connected ends always match.

Prior to distributing the dominoes, the 4 players will agree on which player will play first, either by designating a specific player or a specific domino that must be played first (often [6|6]). After the game starts, play proceeds clockwise.

If a player is able to place a domino on the board, he/she must. Only if they have no possible moves, can the pass on their turn.

The game ends either when a player runs out of dominoes or when no player can play a domino (in which case we say the game is stuck).

If a player runs out of dominoes, his/her team will earn a number of points computed by adding all the values of all the dominoes remaining in the hands of the 3 other players.

If the game is stuck, each team will add up all the values of all the dominoes remaining in their hands. The team with the lower score wins, and earns a number of points computed by adding both teams’ scores. If both teams have the same score, the game is declared a tie, and neither team earns any points.

Variables:
  • board – the game board
  • hands – a list containing each player’s hand
  • moves – a list of the moves that have been played. Moves are represented by a tuple of Domino and bool. The domino indicates the domino that was played, and the bool indicates on what end of the board the domino was played (True for left, False for right). If the player passed, the move is None.
  • turn – the player whose turn it is
  • valid_moves – a tuple of valid moves for the player whose turn it is. Moves are represented in the same way as in the moves list.
  • starting_player – first player to make a move
  • result – None if the game is in progress; otherwise a Result object indicating the outcome of the game
>>> import dominoes
>>> d = dominoes.Domino(6, 6)
>>> g = dominoes.Game.new(starting_domino=d)
>>> g
Board: [6|6]
Player 0's hand: [2|4][5|5][2|3][1|3][1|6][1|2]
Player 1's hand: [1|1][3|4][0|5][0|6][2|5][1|5][2|6]
Player 2's hand: [0|4][0|3][4|4][3|6][0|2][4|5][1|4]
Player 3's hand: [5|6][3|5][3|3][0|0][0|1][2|2][4|6]
Player 1's turn
>>> g.board
[6|6]
>>> g.hands
[[2|4][5|5][2|3][1|3][1|6][1|2], [1|1][3|4][0|5][0|6][2|5][1|5][2|6], [0|4][0|3][4|4][3|6][0|2][4|5][1|4], [5|6][3|5][3|3][0|0][0|1][2|2][4|6]]
>>> g.turn
1
>>> g.result
>>> g.valid_moves # True is for the left of the board, False is for the right
[([0|6], True), ([2|6], True)]
>>> g.make_move(*g.valid_moves[0])
>>> g.moves
[([6|6], True), ([0|6], True)]
>>> g
Board: [0|6][6|6]
Player 0's hand: [2|4][5|5][2|3][1|3][1|6][1|2]
Player 1's hand: [1|1][3|4][0|5][2|5][1|5][2|6]
Player 2's hand: [0|4][0|3][4|4][3|6][0|2][4|5][1|4]
Player 3's hand: [5|6][3|5][3|3][0|0][0|1][2|2][4|6]
Player 2's turn
>>> g.make_move(*g.valid_moves[0])
...
>>> g.make_move(*g.valid_moves[0])
Result(player=1, won=True, points=-32)
>>> g.result
Result(player=1, won=True, points=-32)
>>> g
Board: [2|6][6|3][3|4][4|1][1|1][1|6][6|4][4|5][5|2][2|4][4|0][0|6][6|6][6|5][5|0][0|3][3|5][5|5][5|1][1|0]
Player 0's hand: [2|3][1|3][1|2]
Player 1's hand:
Player 2's hand: [4|4][0|2]
Player 3's hand: [3|3][0|0][2|2]
Player 1 won and scored 32 points!
all_possible_hands()

Yields all possible hands for all players, given the information known by the player whose turn it is. This information includes the current player’s hand, the sizes of the other players’ hands, and the moves played by every player, including the passes.

Yields:a list of possible Hand objects, corresponding to each player
make_move(d, left)

Plays a domino from the hand of the player whose turn it is onto one end of the game board. If the game does not end, the turn is advanced to the next player who has a valid move.

Making a move is transactional - if the operation fails at any point, the game will return to its state before the operation began.

Parameters:
  • d (Domino) – domino to be played
  • left (bool) – end of the board on which to play the domino (True for left, False for right)
Returns:

a Result object if the game ends; None otherwise

Raises:
  • GameOverException – if the game has already ended
  • NoSuchDominoException – if the domino to be played is not in the hand of the player whose turn it is
  • EndsMismatchException – if the domino cannot be placed on the specified position in the board
missing_values()

Computes the values that must be missing from each player’s hand, based on when they have passed.

Returns:a list of sets, each one containing the values that must be missing from the corresponding player’s hand
classmethod new(starting_domino=None, starting_player=0)
Parameters:
  • starting_domino (Domino) – the domino that should be played to start the game. The player with this domino in their hand will play first.
  • starting_player (int) – the player that should play first. This value is ignored if a starting domino is provided. Players are referred to by their indexes: 0, 1, 2, and 3. 0 and 2 are on one team, and 1 and 3 are on another team.
Returns:

a new game, initialized according to starting_domino and starting_player

Raises:
  • NoSuchDominoException – if starting_domino is invalid
  • NoSuchPlayerException – if starting_player is invalid
random_possible_hands()

Returns random possible hands for all players, given the information known by the player whose turn it is. This information includes the current player’s hand, the sizes of the other players’ hands, and the moves played by every player, including the passes.

Returns:a list of possible Hand objects, corresponding to each player
skinny_board()

Converts the board representation used by this game from a regular Board to a less descriptive but more memory efficient SkinnyBoard.

Returns:None

Hand

dominoes.hand.contains_value(hand, value)

Checks whether a value appears in any of the dominoes in the hand.

Parameters:
  • hand (Hand) – hand in which to look for the value
  • value (int) – value to look for in the hand
Returns:

bool indicating whether the value was found in the hand

class dominoes.hand.Hand(dominoes)

Python class for objects that represent a hand of dominoes.

Parameters:dominoes (Sequence) – sequence of dominoes in the hand
>>> import dominoes
>>> d1 = dominoes.Domino(1, 2)
>>> d2 = dominoes.Domino(1, 3)
>>> h = dominoes.Hand([d1, d2])
>>> h
[1|2][1|3]
>>> d1 in h
True
>>> len(h)
2
>>> for d in h: d
[1|2]
[1|3]
>>> h.play(d1)
>>> h
[1|3]
draw(d, i=None)

Adds a domino to the hand.

Parameters:
  • d (Domino) – domino to add to the hand
  • i (int) – index at which to add the domino; by default adds to the end of the hand
Returns:

None

play(d)

Removes a domino from the hand.

Parameters:d (Domino) – domino to remove from the hand
Returns:the index within the hand of the played domino
Raises:NoSuchDominoException – if the domino is not in the hand

Result

class dominoes.Result

namedtuple to represent the result of a dominoes game.

Variables:
  • player – the last player to make a move
  • won – True if the game ended due to an empty hand; False if the game ended due to being stuck
  • points – the absolute value of this quantity indicates the amount of points earned by the winning team. This quantity is positive if the team consisting of players 0 and 2 won, negative if the team consisting of players 1 and 3 won, and 0 in case of a tie.
>>> import dominoes
>>> dominoes.Result(1, True, -25)
Result(player=1, won=True, points=-25)

Series

class dominoes.Series(target_score=200, starting_domino=None)

Python class for objects that represent a series of dominoes games.

A series of dominoes games is played with 4 players, split into two teams. They will then play a sequence of games and keep a running tally of how many points each team has scored. When a team’s cumulative score surpasses some predetermined threshold (usually 100 or 200), that team wins.

Prior to starting the series, the teams agree to a starting domino (usually [6|6]). The player that draws this domino during the first game will play first. The starting player for subsequent games is determined as follows:

  • If a player wins by playing their last domino, that player will start the following game.
  • If a player makes the game stuck, and his/her team wins, that player will start the following game.
  • If a player makes the game stuck, and there is a tie, the player who started that game will start the following game.
  • If a player makes the game stuck, and his/her team loses, then the following player (from the other team) will start the following game.
Parameters:
  • target_score (int) – score up to which the series will be played; defaults to 200
  • starting_domino (Domino) – domino that will determine which player starts the first game; defaults to [6|6]
Variables:
  • games – ordered list of games played in the series
  • scores – list containing the two teams’ scores; team 0 has players 0 and 2, and team 1 has players 1 and 3
  • target_score – score up to which the series will be played
>>> import dominoes
>>> s = dominoes.Series(target_score=50)
>>> s
Series to 50 points:
Team 0 has 0 points.
Team 1 has 0 points.
>>> s.is_over()
False
>>> s.games[0]
Board: [6|6]
Player 0's hand: [0|3][4|4][1|5][0|2][3|4][2|3]
Player 1's hand: [4|6][5|5][2|4][2|5][0|5][3|6][1|4]
Player 2's hand: [3|3][1|3][0|6][5|6][2|2][3|5][2|6]
Player 3's hand: [0|0][0|4][0|1][1|1][4|5][1|6][1|2]
Player 1's turn
>>> s.scores
[0, 0]
>>> s.next_game()
GameInProgressException: Cannot start a new game - the latest one has not finished!
>>> s.games[0].make_move(*s.games[0].valid_moves[0])
...
>>> s.games[0].make_move(*s.games[0].valid_moves[0])
Result(player=3, won=False, points=-24)
>>> s.next_game()
Board:
Player 0's hand: [5|6][3|6][2|2][2|3][4|6][4|4][1|1]
Player 1's hand: [1|5][2|5][0|4][1|3][4|5][0|1][3|4]
Player 2's hand: [6|6][2|4][0|6][3|3][1|2][3|5][0|5]
Player 3's hand: [0|0][0|3][5|5][1|6][1|4][2|6][0|2]
Player 3's turn
>>> s
Series to 50 points:
Team 0 has 0 points.
Team 1 has 24 points.
>>> s.is_over()
False
>>> len(s.games)
2
>>> s.scores
[0, 24]
is_over()
Returns:boolean indicating whether either team has reached the target score, thus ending the series
next_game()

Advances the series to the next game, if possible. Also updates each team’s score with points from the most recently completed game.

Returns:

the next game, if the previous game did not end the series; None otherwise

Raises:
  • SeriesOverException – if the series has already ended
  • GameInProgressException – if the last game has not yet finished

SkinnyBoard

class dominoes.SkinnyBoard(left=None, right=None, length=0)

Python class for objects that represent a domino board. A domino board consists of a series of dominoes placed end to end such that the values on connected ends match. This class reduces the memory required by each instance by remembering only the values at the ends of the board.

Parameters:
  • left (int) – value on the left end of the board
  • right (int) – value on the right end of the board
  • length (int) – amount of dominoes on the board
>>> import dominoes
>>> d1 = dominoes.Domino(1, 2)
>>> d2 = dominoes.Domino(1, 3)
>>> b = dominoes.SkinnyBoard()
>>> repr(b)
''
>>> b.add(d1, True)
>>> b
[1|2]
>>> b.add(d2, False)
EndsMismatchException: [1|3] cannot be added to the right of the board - values do not match!
>>> b.add(d2, True)
>>> b
[3|?][?|2]
>>> b.left_end()
3
>>> b.right_end()
2
>>> len(b)
2
add(d, left)

Adds the provided domino to the specifed end of the board.

Parameters:
  • d (Domino) – domino to add
  • left (bool) – end of the board to which to add the domino (True for left, False for right)
Returns:

None

Raises:

EndsMismatchException – if the values do not match

classmethod from_board(board)
Parameters:board (Board) – board to represent
Returns:SkinnyBoard to represent the given Board
left_end()
Returns:the outward-facing value on the left end of the board
Raises:EmptyBoardException – if the board is empty
right_end()
Returns:the outward-facing value on the right end of the board
Raises:EmptyBoardException – if the board is empty

Questions, Comments, Ideas?

Feel free to create an issue or a pull request.