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]
-
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. ↩