30007 Knucklebones

What is Knucklebones?

Knucklebones is a dice game that to my knowledge originates1 in the game Cult of the Lamb.

How to Play

It is a fairly simple two player game where each player has a three by three grid to place dice into.

Taking turns, each player rolls a single six sided die and places it in a column. If your die face matches any of the dice in your opponents column, they are removed.

Points are totaled by counting the number of pips a player has in their grid, with the added bonus that same face dice in a column have a multiplicative bonus. If you have two of the same face in a column you score double for that face in that column. If you have three, you score triple for that face.

The game ends when a player's grid is completely full. Whoever has the most points when this happens is declared the winner.

You can play a digital recreation of the game at https://knucklebones.io/.

Analyzing Knucklebones

I'm interested in analyzing Knucklebones and answering some questions about it.

1) Is there a first player advantage? 2) Is there an optimal strategy? What is it? 3) How many turns is a standard game? How many if one player is trying to draw it out? If both players?

In the quest to answer these questions, it looks like I'll need to simulate the game myself.

Simulating Knucklebones

I'll be using python.

Let's model the game board as a triple nested list.

python class Knucklebones:   def __init__():     board = [[[0,0,0], [0,0,0], [0,0,0]], [[0,0,0], [0,0,0], [0,0,0]]]

board[0] is then the first player's board, and board[1] is the second player's board.

We'll need some way to measure the scores of each player. Let's break this problem up by first finding the score for a column, then we can sum up the three column to get the score for a player.

The counter container is probably the easiest way to do this. We'll pass it our column and then quickly get each pip in a column and how many times it occurs. Knowing that the keys of the container will be the face of a die in the column, and the value will be the number of times that face is in the column key*val will be the total pip value of a specific face, and the multiples bonus can be calculated from there as (key*val)*val.

Then we can add a score function to our class. We'll return the scores as a tuple.

```python def score(self):     def SumCol(col):         counter_col = Counter(col)         total = 0         for key, val in counter_col.items():             total += (keyval)val         return total

def SumBoard(board):         total = 0         for i in range(3):             total += SumCol(board[i])         return total

player1score=SumBoard(self.board[0])     player2score=SumBoard(self.board[1])     return (player1score,player2score) ```

Now we can try to get the board to print to the screen.

python def PrintBoard(self):     player1score, player2score = self.score()     print(f"Score: {player1score} vs {player2score}")     for i in range(3):         print(f"{self.board[0][i]}  {self.board[1][i]}")     return None

Which currently prints out these four lines.

Score: 0 vs 0 [0, 0, 0] [0, 0, 0] [0, 0, 0] [0, 0, 0] [0, 0, 0] [0, 0, 0]

Woops. I think this is now horizontal instead of vertical like the game, making our choice of calling these lists columns a bit odd. Let's say I did this on purpose for space saving. While we're at it the spacing for the scores to align with each board was also on purpose and not a happy accident.

Now we need to get this to be a playable game. First we need to be able to roll a die. Simple enough.

python def RollDie(self):     return random.randint(1,6)

A little bit harder will be adding a die to a column. We'll need to find blank spots (represented by zeros) and add the value in, or raise an error when the column is full.

python def AddDie(self, player, col, num):     index = next((index for index,value in enumerate(self.board[player][col]) if value == 0), None)     if index != None:         self.board[player][col][index]=num     else:         raise ValueError(f"Tried to add a die to player {player+1}'s number {col} column, but it is full. Another die cannot be added.")

But adding a die to one player's board isn't enough. We need to remove all die with the same face in the other player's same column. Let's make a way to do that. A list compression that looks through the column and replaces the value with zero should work.

python def RemoveOppositeDice(self, player, col, num):         self.board[player][col] = [x if x != num else 0 for x in self.board[player][col]]

Now we can add the removing to the AddDie Method.

python def AddDie(self, player, col, num):         index = next((index for index,value in enumerate(self.board[player][col]) if value == 0), None)         if index != None:             self.board[player][col][index]=num             self.RemoveOppositeDice(1-player, col, num) # 1-player is the opposite player         else:             raise ValueError(f"Tried to add a die to player {player+1}'s number {col} column, but it is full. Another die cannot be added.")

If would also be helpful for the game to record if a game over is reached. Let's have it record a winner status for each player.

python def __init__(self):         self.board = [[[0,0,0], [0,0,0], [0,0,0]], [[0,0,0], [0,0,0], [0,0,0]]]         self.winner = [False, False]

We'll also add a way to print who won.

python def PrintWinner(self):         if self.winner[0] and self.winner[1]:             print("A tie!")         elif self.winner[0]:             print("Player 1 Wins!")         elif self.winner[1]:             print("Player 2 Wins!")

Now let's add a variable to keep track of if the game is over, and a method to check if the game is over (a player's board has no open spaces in it).

python def __init__(self):         self.board = [[[0,0,0], [0,0,0], [0,0,0]], [[0,0,0], [0,0,0], [0,0,0]]]         self.winner = [False, False]         self.gameover = False def CheckGameOver(self):         for i in range(2):             if not any(0 in sublist for sublist in self.board[i]):                 self.gameover = True

We only need to check if the game is over after a dice has been played, and more specifically after a dice has filled up a column for a player.

python def AddDie(self, player, col, num):         index = next((index for index,value in enumerate(self.board[player][col]) if value == 0), None)         if index != None:             self.board[player][col][index]=num             self.RemoveOppositeDice(1-player, col, num) # 1-player is the opposite player             if 0 not in self.board[player][col]:                 self.CheckGameOver()         else:             raise ValueError(f"Tried to add a die to player {player+1}'s number {col} column, but it is full. Another die cannot be added.")

It's seems we missed a step, recording who won if we realize the game has ended. Let's fix that.

python def CheckGameOver(self):         for i in range(2):             if not any(0 in sublist for sublist in self.board[i]):                 self.gameover = True                 player1score, player2score = self.score()                 self.winner = [player1score >= player2score, player1score <= player2score]


  1. I haven't done too deep a dive, but any forum discussions I find on the matter don't bring up any analogous games. The name of course is probably a reference to Knucklebones, a dexterity game I typically grew up calling Jacks.