Matatena

Matatena (or Knucklebones in english) is a game played in the game Cult of the lamb. Here we provied a Python implementation of the game with the objective to be able to use it as a gym environment for reinforcement learning.

Basic rules

Knucklebones is a simple game in which a dice is rolled and one of the two players must place it in their corresponding board. Each player can only choose the column in which the dice is placed, and if any dice is already placed in that column, the following is put behind. If there are more than one dice with the same number, their score is added and then multiplied by the number of times it appears. For example, if a column has two 6s, their values add up to 24, but if a column has a 6 and a 5, the total score would be 11.

The basic board is a 3x3 grid.

Definition of the board

Each game board (one for each player) can be represented, for example, with a np.array. But we may probably benefit from having a “global” object that controls both of the players to keep track of both boards and the score in a more simple manner.


source

Game

 Game (n_players=2, board_size=3)

Class that controls the whole game. It keeps track of both boards.

Type Default Details
n_players int 2 Number of players.
board_size int 3 Size of the board. It is a squared grid of board_sizexboard_size
matatena = Game()
assert not matatena.is_done()
matatena
Player 1 *
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
matatena = Game()
matatena.boards[1] = np.ones_like(matatena.boards[1])
assert matatena.is_done()
matatena
Player 1 *
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2
[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
matatena = Game()
matatena.add_dice(player=0, column=0, dice=6)
print(matatena)
matatena.add_dice(player=0, column=0, dice=6)
print(matatena)
Player 1 *
[[6. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 1 *
[[6. 0. 0.]
 [6. 0. 0.]
 [0. 0. 0.]]
Player 2
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
matatena = Game()
matatena.add_dice(player=0, column=0, dice=6)
matatena.add_dice(player=0, column=0, dice=6)
matatena.add_dice(player=0, column=0, dice=6)
# matatena.add_dice(player=0, column=0, dice=6)
False
matatena = Game()
matatena.current_player = 0
matatena.boards[1] = np.ones_like(matatena.boards[1])
print(matatena)
matatena.add_dice(player=0, column=0, dice=1)
assert not (matatena.boards[1][:,0] == 1).any()
matatena
Player 1 *
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2
[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
Player 1 *
[[1. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2
[[0. 1. 1.]
 [0. 1. 1.]
 [0. 1. 1.]]

Calculating the score


source

Game.score

 Game.score (player)

Returns the calculated score for a player. If there are numbers repeated in a column, their values must be added and multiplied by the number of repetitions. Otherwise, they are added. If there are repreated and non-repeated in the same column, the repeated are summed and multiplied by the number of repetitions and then the result is added to the non-repeated.

Details
player Number of the player we want to calculate the score.
matatena = Game()
matatena.boards[0] = np.array([[1,0,0],
                               [1,2,3],
                               [4,2,5]])
assert matatena.score(0) == 8+8+8

Now that we know how to calculate the score, we can patch the __repr__ method of our class to show the score of each player as well:

matatena = Game()
assert not matatena.is_done()
matatena
Player 1 (0.0) *
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
Player 2 (0.0)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
matatena = Game()
matatena
Player 1 (0.0) | Player 2 (0.0) *
[[0. 0. 0.]    | [[0. 0. 0.]     
 [0. 0. 0.]    |  [0. 0. 0.]     
 [0. 0. 0.]]   |  [0. 0. 0.]]    

Full turn

A full turn in a Matatena game must follow the following steps:

  • Check the current player
  • Draw a random dice
  • Place the dice in a column
    • Check if the column is not full -> The dice can be placed (If it can’t be placed the game is ended)
  • Change the current player to the next one in the list
  • Repeat

source

Game.play_turn

 Game.play_turn ()

Plays a full turn.

matatena = Game()
print(matatena.current_player)
matatena.play_turn()
# The player is asked for an input and the rolled dice is placed
# in the chosen column.
print(matatena.current_player)
matatena
0
Dice to place: 6
Player 1 (0.0) * | Player 2 (0.0)
[[0. 0. 0.]      | [[0. 0. 0.]   
 [0. 0. 0.]      |  [0. 0. 0.]   
 [0. 0. 0.]]     |  [0. 0. 0.]]  
0
Player 1 (6.0) * | Player 2 (0.0)
[[6. 0. 0.]      | [[0. 0. 0.]   
 [0. 0. 0.]      |  [0. 0. 0.]   
 [0. 0. 0.]]     |  [0. 0. 0.]]