# dice game (text mode)#

This tutorial shows you how to create a computer game based on the rules of the pen and paper game →Yahtzee, a game about dice throwing. This tutorial will only use Python and no other third party libraries.

The user interface of the computer game that you will build in this tutorial will be a text based user interface. Later tutorials will improve the game by adding a graphical user interface.

## lesson goal#

• problem solving: adapting a physical dice game to a computer game using Python.

• writing functions to code the game rules in Python

• testing those functions

• writing a playable computer game in Python

## game rules#

The pen_and_paper version of Yahtzee and similar dice games is played with 5 dice, a pen and a (paper) score table. The score table (see Figure 1 at right) has 13 lines, each linerepresenting an option that the player can play. Those options are named similar to the figures of the card game →poker and have names like “full house”, “three of a kind” or “Yathzee”.

The game rules are:

• Each player had 5 dice and can throw them 3 times per game round.

• The game has 13 game rounds.

• After the first and the second throw the player can decide to throw some, all or none of his dice again or leave them.

• After the third throw, the player must play one of the 13 possible options. When the dice match with the chosen options, the player gets points for this round. Otherwise they get zero points. In both cases, the selected option is “used up” for the rest of the game.

• The options consist of an upper section (1,2,3,4,5,6) and a lower section (“three of a kind”, “four of a kind”, “full house”, “small straight”, “large straight”, “yahtzee”, “chance”). The scores of each options are:

### upper section#

Aces:

the sum of dice with the number one

Twos:

the sum of dice with the number two

Threes:

the sum of dice with the number three

Fours:

the sum of dice with the number four

Fives:

the sum of dice with the number five

Sixes:

the sum of dice with the number six

Example: the player throws 2 sixes and 3 other numbers. He decide to play the Sixes option. His score is 2 x 6 = 12 points.

### lower section#

three of a kind:

Sum of all dice. 0 points if less than 3 dice show the same number.

four of a kind:

Sum of all dice. 0 points if less than 4 dice show the same number.

full house:

25 points. 0 points if there are no 3 dice with the same number and 2 dice with (another) same number.

small straight:

30 points if 4 dice show the combination 1,2,3,4 or 2,3,4,5 or 3,4,5,6. Otherwise 0 points.

large straight:

40 points if 5 dice show the combination 1,2,3,4,5 or 2,3,4,5,6. Otherwise 0 points.

Yahtzee:

50 points if 5 dice show the same number, otherwise 0 points.

Chance:

sum of all dice

### special rules#

Yathzee bonus:

If the player throws a Yahtzee and has already filled the Yahtzee box with a score of 50, they score a Yahtzee bonus and get an extra 100 points. However, if they throw a Yahtzee and have used the Yahtzee category already with a score of 0, they do not get a Yahtzee bonus.

Joker:

A yahtzee can act as full house, small street or large street if the upper section with the dice number has already been used and the Yahtzee option in the lower section has also been used.

upper section bonus:

If the player scores more than 63 points in the upper section, they get a bonus of 35 points.

The big task of this tutorial is to write a Yahtzee computer game. There is no single perfect way of how to do this. In fact, every programmer may come up with one (or many!) slightly different versions.

A sensible approach is to split a big task into smaller, more managable sub-tasks. It is helpful to write down what the program should do:

• Simulate throwing of five dice

• Let the player keep some dice / throw some dice

• Keep track of game round and throws (3 throws per game round, 13 game rounds)

• Once per game round: Show the player which options they can play and how many points they will get for each option. This includes:

• Recognizing if the player throwed a specific option (full house, large straight etc.)

• Calculating the amount of points the player would get for each option. This includes:

• Apply special rules such a joker rule (yahtzee can act as a joker under certain conditions)

• Ask the player to choose an option and keep track of already played (used) options

• Keep track of points, calculate sum of points at the end of the game. This includes:

• Calculating upper score bonus and yahtzee bonus

If the program works, it needs testing:

• to make sure that the user can not “break” the game by entering silly or wrong commands

• to make sure that the scoring is correct according to the game rules

Let’s solve all of those sub-tasks listed above step by step:

## step one#

simulate throwing of five dice

To simulate the throwing of five dice, it is necessary to import Python’s bulit-in random module and call the random.randint() function. The result of each function call need to be stored in a variable. Here five different variables (d1…d5) are used:

```1import random             # ❶
2d1 = random.randint(1,6)  # ➋
3d2 = random.randint(1,6)
4d3 = random.randint(1,6)
5d4 = random.randint(1,6)
6d5 = random.randint(1,6)
7print(d1,d2,d3,d4,d5)     # ➌
```
example output of dicegame01_01.txt (Your output may be different)#
```5 6 5 1 5
```

Having five different variables for for the five simulated dice is fine, but having a single data structure to store all of those 5 numbers together is more practical.

The datastructure should be iterable (to process all five dice one-by-one) and it should be mutable to allow the replacing of specific numbers. Python’s most popular datastructure, the list fullfills both criteria. The code example below creates such a list holding five random values:

```1import random
2dicelist = []                            # ➊
3for _ in range(5):                       # ➋
4    dicelist.append(random.randint(1,6)) # ➌
5print(dicelist)                          # ➍
```
example output of dicegame01_02.txt (Your output may be different)#
```[6, 2, 2, 1, 5]
```
diff between dicegame01_01.py -> dicegame01_02.py#
```--- /home/horst/code/sphinx_playground/source/dicegame/dicegame01_01.py
+++ /home/horst/code/sphinx_playground/source/dicegame/dicegame01_02.py
@@ -1,7 +1,5 @@
-import random             # ❶
-d1 = random.randint(1,6)  # ➋
-d2 = random.randint(1,6)
-d3 = random.randint(1,6)
-d4 = random.randint(1,6)
-d5 = random.randint(1,6)
-print(d1,d2,d3,d4,d5)     # ➌+import random
+dicelist = []                            # ➊
+for _ in range(5):                       # ➋
+    dicelist.append(random.randint(1,6)) # ➌
+print(dicelist)                          # ➍
```

One sub-task done, four more to go:

• simulate throwing 5 dice

• let the player keep some dice / throw some dice

• keep track of game round and throws

• ask the player to choose an option and keep track of played options

• show the player how many points they will get for each option

• calculate sum of points at the end of the game

## step two#

let the player keep some dice / throw some dice

keep track of game round and throws

The progrm should provide a way for the user to tell the program which of the five dice they want to keep and which dice they want to throw again. In short, a user interface is needed.

This leads to a new problem: How can the user tell the computer which of the five dice they want to keep ?

Several solutions are possible:

• Asking the user which elements of `dicelist` they want to keep. Example: The user throws two sixes and three other numbers. The user types the number 6 to indicate that they want to keep all sixes.

Problem: What if they want to keep only one six? A second follow-up question would be necessary to ask how many sixes they want to keep. And of course, testing is necessary to make sure the user only enters numbers that exist in dicelist in the desired quantity.

• Giving each die an index and asking the user to type in the indexes of the dice they want to keep. Python automatically gives an index to each item of a list (starting with zero!). Example output:

```index       0  1  2  3  4
dicelist: [ 5, 6, 6, 3, 1]
```

Problem: Both the index and results of the dice throws are numbers (0-4 and 1-6), making the user experience a bit confusing. The fact that Python starts indexing with 0 and not with 1 makes the whole process a bit counter-intuitive.

• Giving each die a letter (a,b,c,d,e) and asking the user to type the letter(s) of the dice they want to keep. The index of the desired dicelist item is then calculated from the entered letter. Example output:

```letters:    a  b  c  d  e
dicelist:  [5, 6, 6, 3, 1]
```

This tutorial will continue with the third variant (giving each position in dicelist a letter).

A simple prototype for the user interface could look like this:

``` 1import random
2dicelist = [5,1,4,4,2]                # ➊
3print("old dicelist:")
4print(" a  b  c  d  e")               # ➋
5print(dicelist)
6text = "Type letter(s) of dice you want to keep and press ENTER "
7command = input(text)                 # ➌
8if not "a" in command:                # ➍
9    dicelist[0] = random.randint(1,6) # ➎
10if not "b" in command:
11    dicelist[1] = random.randint(1,6)
12if not "c" in command:
13    dicelist[2] = random.randint(1,6)
14if not "d" in command:
15    dicelist[3] = random.randint(1,6)
16if not "e" in command:
17    dicelist[4] = random.randint(1,6)
18print("new dicelist:", dicelist)
```
output of dicegame01_03.py#
```old dicelist:
a  b  c  d  e
[5, 1, 4, 4, 2]
Type letter(s) of dice you want to keep and press ENTER cd
new dicelist: [4, 5, 4, 4, 6]
```
diff between dicegame01_02.py -> dicegame01_03.py#
```--- /home/horst/code/sphinx_playground/source/dicegame/dicegame01_02.py
+++ /home/horst/code/sphinx_playground/source/dicegame/dicegame01_03.py
@@ -1,5 +1,18 @@
import random
-dicelist = []                            # ➊
-for _ in range(5):                       # ➋
-    dicelist.append(random.randint(1,6)) # ➌
-print(dicelist)                          # ➍+dicelist = [5,1,4,4,2]                # ➊
+print("old dicelist:")
+print(" a  b  c  d  e")               # ➋
+print(dicelist)
+text = "Type letter(s) of dice you want to keep and press ENTER "
+command = input(text)                 # ➌
+if not "a" in command:                # ➍
+    dicelist[0] = random.randint(1,6) # ➎
+if not "b" in command:
+    dicelist[1] = random.randint(1,6)
+if not "c" in command:
+    dicelist[2] = random.randint(1,6)
+if not "d" in command:
+    dicelist[3] = random.randint(1,6)
+if not "e" in command:
+    dicelist[4] = random.randint(1,6)
+print("new dicelist:", dicelist)
```

The beauty of this user interface is that it does not matter if the user enter letters (or numbers, punctuation etc) or if they enter the same letter several times … it still works.

By merging dicegame01_02.py together with dicegame01_03.py, putting everything inside of a nested loop (13 game rounds, 3 throws per game) and making the user interface a bit more idiot-proof so that it does not matter if the user type letters in lowercase or UPPERCASE we have an already playable version of the dicegame. As an undemanded extra feature, version dicegame01_04.py below shows at the end of the game an “history” (a list of dicelists) of every game round (It shows what the value of dicelist was).

``` 1import random
2
3history = []
4
5for game_round in range(1,14):                    # ➊
6    dicelist = []
7    for _ in range(5):
8        dicelist.append(random.randint(1,6))
9    print(f"-------- round {game_round} of 13--------------")  # ➋
10    for throw in (1,2,3):                         # ➌
11        text= "throw: {} of 3".format(throw)      # ➍
12        print(text)
13        print(" a  b  c  d  e")
14        print(dicelist)
15        if throw < 3:
16            text = "Type letter(s) of dice to keep and press ENTER "
17            command = input(text).lower()              # ➎
18            for i, char in enumerate("abcde") :        # ➏
19                if char not in command:
20                    dicelist[i] = random.randint(1,6)  # ➐
21
22
23    print(f"------- end of round {game_round}------")
24    if game_round == 13:                       # ➑
25        text = "press ENTER to see the history of dice throws"
26    else:
27        text = "press ENTER to start the next game round"
28    input(text)                           # ➒
29    history.append(dicelist.copy())       # ➓
30print("Game Over")
31print("--- history of your dice throws: ---")
32print("game round,  result")
33for game_round, result in enumerate(history, 1):  # ⓫
34    print(f"{game_round:>5} : {result}" )         # ⓬
```
example output of dicegame01_04.py (Your output may be different)#
```-------- round 1 of 13--------------
throw: 1 of 3
a  b  c  d  e
[1, 6, 6, 2, 2]
Type letter(s) of dice to keep and press ENTER bc
throw: 2 of 3
a  b  c  d  e
[3, 6, 6, 3, 6]
Type letter(s) of dice to keep and press ENTER bce
throw: 3 of 3
a  b  c  d  e
[4, 6, 6, 2, 6]
------- end of round 1------
press ENTER to start the next game round
-------- round 2 of 13--------------
throw: 1 of 3
a  b  c  d  e
[3, 3, 5, 4, 3]
Type letter(s) of dice to keep and press ENTER abe
throw: 2 of 3
a  b  c  d  e
[3, 3, 6, 3, 3]
Type letter(s) of dice to keep and press ENTER abde
throw: 3 of 3
a  b  c  d  e
[3, 3, 3, 3, 3]
------- end of round 2------
press ENTER to start the next game round
-------- round 3 of 13--------------

(snipp)

-------- round 13 of 13--------------
throw: 1 of 3
a  b  c  d  e
[6, 6, 5, 2, 2]
Type letter(s) of dice to keep and press ENTER ab
throw: 2 of 3
a  b  c  d  e
[6, 6, 3, 3, 3]
Type letter(s) of dice to keep and press ENTER abcde
throw: 3 of 3
a  b  c  d  e
[6, 6, 3, 3, 3]
------- end of round 13------
press ENTER to see the history of dice throws
Game Over
--- history of your dice throws: ---
game round,  result
1 : [4, 6, 6, 2, 6]
2 : [3, 3, 3, 3, 3]
3 : [2, 1, 2, 1, 2]
4 : [1, 1, 2, 5, 1]
5 : [3, 5, 3, 1, 2]
6 : [3, 6, 3, 4, 1]
7 : [6, 2, 6, 1, 5]
8 : [4, 3, 2, 3, 6]
9 : [6, 1, 5, 5, 4]
10 : [4, 3, 2, 3, 1]
11 : [4, 4, 4, 6, 1]
12 : [2, 6, 3, 4, 5]
13 : [6, 6, 3, 3, 3]
```
diff between dicegame01_03.py -> dicegame01_04.py#
```--- /home/horst/code/sphinx_playground/source/dicegame/dicegame01_03.py
+++ /home/horst/code/sphinx_playground/source/dicegame/dicegame01_04.py
@@ -1,18 +1,34 @@
import random
-dicelist = [5,1,4,4,2]                # ➊
-print("old dicelist:")
-print(" a  b  c  d  e")               # ➋
-print(dicelist)
-text = "Type letter(s) of dice you want to keep and press ENTER "
-command = input(text)                 # ➌
-if not "a" in command:                # ➍
-    dicelist[0] = random.randint(1,6) # ➎
-if not "b" in command:
-    dicelist[1] = random.randint(1,6)
-if not "c" in command:
-    dicelist[2] = random.randint(1,6)
-if not "d" in command:
-    dicelist[3] = random.randint(1,6)
-if not "e" in command:
-    dicelist[4] = random.randint(1,6)
-print("new dicelist:", dicelist)
+
+history = []
+
+for game_round in range(1,14):                    # ➊
+    dicelist = []
+    for _ in range(5):
+        dicelist.append(random.randint(1,6))
+    print(f"-------- round {game_round} of 13--------------")  # ➋
+    for throw in (1,2,3):                         # ➌
+        text= "throw: {} of 3".format(throw)      # ➍
+        print(text)
+        print(" a  b  c  d  e")
+        print(dicelist)
+        if throw < 3:
+            text = "Type letter(s) of dice to keep and press ENTER "
+            command = input(text).lower()              # ➎
+            for i, char in enumerate("abcde") :        # ➏
+                if char not in command:
+                    dicelist[i] = random.randint(1,6)  # ➐
+
+
+    print(f"------- end of round {game_round}------")
+    if game_round == 13:                       # ➑
+        text = "press ENTER to see the history of dice throws"
+    else:
+        text = "press ENTER to start the next game round"
+    input(text)                           # ➒
+    history.append(dicelist.copy())       # ➓
+print("Game Over")
+print("--- history of your dice throws: ---")
+print("game round,  result")
+for game_round, result in enumerate(history, 1):  # ⓫
+    print(f"{game_round:>5} : {result}" )         # ⓬
```

Attention

at line 29 (`#⑪`), What would happen if you do not append a copy of `dicelist` to history but instead just `dicelist`:

```29        history.append(dicelist)   #  ⑪
```

Modify the code as shown above, play a full game of 13 game rounds and look at the output of `history` to find out!

Score calculation and the choosing of an option per game round is still lacking but three sub tasks already are done:

• simulate throwing 5 dice

• let the player keep some dice / throw some dice

• keep track of game round and throws

• ask the player to choose an option - - [ ] show the player how many points they will get for each option

• calculate sum of points at the end of the game

## step three#

ask the player to choose an option and keep track of played options

A data structure is needed to hold the options a player can choose at each game round. One possibility is to use a list:

```all_options = [
# upper section:
"Ones","Twos", "Threes", "Fours", "Fives","Sixes",
# lower section:
"Three Of A Kind", "Four Of A Kind", "Full House",
"Small Straight", "Large Straight", "Yahtzee", "Chance",
]
```

For the task of keeping track of played options:

As always there are several possibilities of how to design code and data structures.

Below are just some possible solutions sketched for the problem of keeping track of played and unplayed options:

using lists

Create a copy of a list and use the `.append()` and `.remove()` methods of list:

```all_options = [
"Ones","Twos", "Threes",  # etc.
"Full House", "Chance",  # etc.
]
unplayed = all_options.copy()
played = [] # empty list
player_choice = "Full House"
played.append(player_choice)
unplayed.remove(player_choice)
print(unplayed)
```
list and set

create a set out of a list and use the `.difference()` methods of sets:

```all_options = [
"Ones","Twos", "Threes",  # etc.
"Full House", "Chance",  # etc.
]
played = []
player_choice = "Full House"
played.append(player_choice)
unplayed = set(all_options).difference(
set(played))
print(unplayed)
```
dictionary and list comprehension

create a dictionary with the option names as keys and a boolean value as values to indicate if the option was played. Update the dictionary when an option is played and calculate the unplayed options by using a list comprehension:

```all_options = [
"Ones","Twos", "Threes",  # etc.
"Full House", "Chance",  # etc.
]
played = {"Ones": False,
"Twos": False,
"Threes": False,
"Full House": False,
"Chance":False,
# etc.
}
player_choice = "Full House"
played[player_choice] = True # update the dictionary
unplayed = [o for o in all_options
if not played[o]]
print(unplayed)
```
Syncronize lists with common index

Have two lists with the same lenght: `all_options` and `played`. The latter consist of boolean values. Use the `.index()` methods of lists to find out the index of the played option and update the `played`. Use a list comprehension to calculate the `unplayed` options:

```all_options = [
"Ones","Twos", "Threes",  # etc.
"Full House", "Chance",  # etc.
]
played = [False for o in all_options]
# [False, False, False, ...]  # etc.
player_choice = "Full House"
# find out the index of this option
i = all_options.index(player_choice)
# i has value of 3 in this example
# update played:
played[i] = True
# list comprehension
unplayed = [o for o in all_options
if not played[all_options.index(o)]
]
print(unplayed)
```

TODO: data class

Thinking a bit ahead, a good question to ask is: what information is connected with the list of options?

• the name of the option

• the maximum possible score for this option

• if this option is still playable (or was already played)

• the archived score for this option - if the option was played at all. (Attention: An option can be played with an score of 0, so a score of 0 does not mean the option was not yet played)

The maximum possible score is fixed for each option and will not change during a game. Therefore, a immutable datastructure like a tuple makes sense:

```# list of tuples: (option name, max_score)
all_options = [("Ones", 5),
("Twos",10),
("Threes", 15),
# etc.
]
```

But a dictionary is also possible (and a bit more beautiful to look at):

```# key: name of option.
# value: maximum possible score for this option
#           keys:            values:
options = {
# upper section:
"Ones":            5,
"Twos":            10,
"Threes":          15,
"Fours":           20,
"Fives":           25,
"Sixes":           30,
# lower section
"Three Of A Kind": 30,
"Four Of A Kind":  30,
"Full House":      25,
"Small Straight":  30,
"Large Straight":  40,
"Yahtzee":         50,
"Chance":          30,
}
```

## step four#

show the player how many points they will get for each option

calculate sum of points at the end of the game

This is by far the most complex step. To calculate how many points an playable option is worth, two steps are necessary:

1. To check if the conditions for the option are met by the numbers inside the variable `dicelist`.

2. To calculate how many points the option would bring according to the rules of the game, because not all options give a fixed amount of points.

While not strictly necessary, it is a good idea to create a few functions to test if `dicelist` meets the conditions for each option. Those functions will return either True (if the conditions are met) or False.

It is good practice to prefix names of functions that return an boolean value with `is_`: A function to test if `dicelist` is a “Full House” should be named: `is_full_house()`. Such an function name indicates already that the function will return either `True` or `False`. (Another way to indicate what the function returns is using a type hint and/or a docstring).

By tradition (and more specific, by the rules of the Python style guide PEP8), function names should always be written in lowercase. A space in a function name is not allowed but underscores are allowed.

Note

All the functions described below require at least one parameter named `dice`. This is technically not necessary: If `dicelist` is defined in the global scope (at “root level”), then the function can access `dicelist` and does not need to have it passed as a parameter.

Those two examples below both work correctly:

function without parameter
```# function without parameters
def is_yahtzee():
if len(set(dicelist)) == 1:
return True
return False

# dicelist is defined at root level
dicelist = [1,4,4,4,2]
# call function:
result = is_yahtzee()
print(result)
```
function with parameter
```# function with parameter
def is_yahtzee(dice):
if len(set(dice)) == 1:
return True
return False

#  root level
dicelist = [1,4,4,4,2]
# call function with one argument
result = is_yahtzee(dicelist)
print(result)
```

The functions below will all use the variant with a parameter named `dice`. To work correctly, each function call has to pass `dicelist` as argument to the function.

### is_three_of_a_kind()#

This function takes a look at `dicelist` (actually, at the parameter `dice`) and returns `True` if `dice` is a “Three of a kind”, otherwise the function returns `False`. As always, there exist more than one single way of how to code such a function. The variant below uses the list.count() functionality of lists.

Included in the code example below is some (very simple) code for testing if the function works correctly. See doctest and unittest to take a deeper look into testing with Python.

``` 1def is_three_of_a_kind(dice):              # ➊
2    """dice is a list of five integers"""  # ➋
3    for number in dice:                    # ➌
4        if dice.count(number) >= 3:        # ➍
5            return True                    # ➎
6    return False                           # ➏
7
8if __name__ == "__main__":
9    # tests
10    print("testing...")
11    assert is_three_of_a_kind([1,1,2,2,2]) == True # ➐
12    assert is_three_of_a_kind([1,1,2,2,2]) == True
13    assert is_three_of_a_kind([1,1,2,2,2]) == True
14    assert is_three_of_a_kind([1,4,4,4,4]) == True
15    assert is_three_of_a_kind([5,5,5,5,5]) == True
16    assert is_three_of_a_kind([1,2,3,4,5]) == False
17    assert is_three_of_a_kind([2,3,4,5,6]) == False
18    assert is_three_of_a_kind([1,1,2,2,5]) == False
19    assert is_three_of_a_kind([6,1,4,6,1]) == False
20    assert is_three_of_a_kind([6,1,1,1,2]) == True
21    print("all tests passed")    # ➑
```
output of dicegame01_06.txt#
```testing...
all tests passed
```

Attention

What would happen if you add an test that does not work? For example, add this line, run the program and check the output:

```assert is_three_of_a_kind([1,1,1,2,2]==False)
```

### is_four_of_a_kind()#

The list.count() functionality of lists can also be used to detect “Four Of A Kind”.

``` 1def is_four_of_a_kind(dice):               # ➊
2    """dice is a list of five integers"""  # ➋
3    for number in dice:                    # ➌
4        if dice.count(number) >= 4:        # ➍
5            return True                    # ➎
6    return False                           # ➏
7
8if __name__ == "__main__":
9    # tests
10    print("testing...")
11    assert is_four_of_a_kind([1,2,3,4,5]) == False # ➐
12    assert is_four_of_a_kind([1,1,2,2,2]) == False
13    assert is_four_of_a_kind([2,1,2,2,2]) == True
14    assert is_four_of_a_kind([4,4,4,4,4]) == True
15    print("all tests passed")    # ➑
```
output of dicegame01_07.txt#
```testing...
all tests passed
```

### is_full_house()#

A “Full House” is defined as dicelist having 2 equal numbers and 3 (other) equal numbers. In other words, only 2 different numbers exist inside dicelist, one of them occuring three times, the other one occuring 2 times.

As always, there exist several ways of how to code a function to detect if `dicelist` is a “Full House”. The code example below creates a set out of `dicelist` (to be more exact, out of the parameter `dice`) and assign this set to the variable `diceset`. By definition, there can be only unique values (no duplicate values) in a set. If `diceset` has a length of 2 and those numbers exist 2 or 3 times in `dice`, the function returns `True`. Otherwise, the function returns `False`.

``` 1def is_full_house(dice):                      # ➊
2    """dice is a list of five integers"""     # ➋
3    diceset = set(dice)                       # ➌
4    if len(diceset) == 2:                     # ➍
5        for number in diceset:                # ➎
6            if dice.count(number) in (2,3):   # ➏
7                return True                   # ➐
8    return False                              # ➑
9
10if __name__ == "__main__":
11    # tests
12    print("testing...")
13    assert is_full_house([1,2,3,4,5]) == False    # ➒
14    assert is_full_house([1,1,2,2,2]) == True
15    assert is_full_house([6,4,6,4,6]) == True
16    assert is_full_house([4,4,4,4,4]) == False
17    print("all tests passed")                     # ➓
```
output of dicegame01_08.txt#
```testing...
all tests passed
```

### is_small_straight()#

A “Small Straight” is defines as dicelist having those numbers: [1,2,3,4] or [2,3,4,5] or [3,4,5,6]. The numbers can be in any order, of course.

Sets offer functionality to test if one set is a superset or a subset of another set. In other words, it is very easy to test if all numbers inside of a small set also exist in a bigger set.

``` 1def is_small_straight(dice):                  # ➊
2    """dice is a list of five integers"""     # ➋
3    diceset = set(dice)                       # ➌
4    if any((diceset.issuperset({1,2,3,4}),    # ➍
5            diceset.issuperset({2,3,4,5}),
6            diceset.issuperset({3,4,5,6}))):
7        return True                           # ➎
8    return False                              # ➏
9
10if __name__ == "__main__":
11    # tests
12    print("testing...")
13    assert is_small_straight([1,2,3,4,1]) == True    # ➐
14    assert is_small_straight([6,5,4,3,2]) == True
15    assert is_small_straight([2,3,4,5,2]) == True
16    assert is_small_straight([3,4,5,6,6]) == True
17    assert is_small_straight([4,4,4,4,4]) == False
18    assert is_small_straight([1,2,2,3,6]) == False
19    print("all tests passed")                     # ➑
```
output of dicegame01_09.txt#
```testing...
all tests passed
```

### is_large_straight()#

A “Large Straight” must contain the numbers [1,2,3,4,5] or [2,3,4,5,6] (in any order). The code is very similar to the code above:

``` 1def is_large_straight(dice):                  # ➊
2    """dice is a list of five integers"""     # ➋
3    diceset = set(dice)                       # ➌
4    if (diceset.issuperset({1,2,3,4,5}) or    # ➍
5        diceset.issuperset({2,3,4,5,6})):
6        return True                           # ➎
7    return False                              # ➏
8
9if __name__ == "__main__":
10    # tests
11    print("testing...")
12    assert is_large_straight([1,2,3,4,5]) == True    # ➐
13    assert is_large_straight([6,5,4,3,2]) == True
14    assert is_large_straight([2,3,4,5,2]) == False
15    assert is_large_straight([3,4,5,6,6]) == False
16    assert is_large_straight([4,4,4,4,4]) == False
17    assert is_large_straight([1,3,4,5,6]) == False
18    print("all tests passed")                     # ➑
```
output of dicegame01_10.txt#
```testing...
all tests passed
```

### is_yahtzee()#

How to test if a dicelist is a “Yathzee”: The most simple variant is to compare all numbers using the `==` operator:

```1def is_yahtzee(dice):
2    """dice is a list of five integers"""
3    return dice[0] == dice[1] == dice[2] == dice[3] == dice[4]
4
```

Another variant is by using a set: If all numbers inside a `dicelist` are the same, the `set` of this `dicelist` has a length of exactly one:

``` 1def is_yahtzee(dice):                      # ➊
2    """dice is a list of five integers"""  # ➋
3    return len(set(dice)) == 1             # ➌
4
5if __name__ == "__main__":
6    # tests
7    print("testing...")
8    assert is_yahtzee([1,2,3,4,5]) == False    # ➍
9    assert is_yahtzee([6,5,6,6,6]) == False
10    assert is_yahtzee([2,2,2,2,2]) == True
11    assert is_yahtzee([2,4,4,2,2]) == False
12    print("all tests passed")                  # ➎
```
output of dicegame01_11.txt#
```testing...
all tests passed
```

### calculate_score()#

Now what is missing is a function to calculate the score for a given dicelist and a given option.

The first six keys of the dictionary `all_options` are the “upper section”: (“Ones”, “Twos”, “Threes” etc.). All of those six options are very easy to calculate: The desired number (1 for “Ones” etc.) is multiplyed by the frequency (how many times `1` occurs inside `dicelist`).

For example, if `dicelist` contains two time the number 6 and the player choose to play “Sixes”, the calculation is `2 x 6 = 12` points.

The score for the options “Three of a kind”, “Four of a kind” and “Chance” is also very easy to calculate: just take the sum of all dice in `dice`. Python provides a `sum()` functions for lists, so the code is simply `score = sum(dice)`.

The scores for “Full House”, “Small Straight”, “Large Straight” and “Yahtzee” are fixed with 25, 30, 40 and 50 points.

Finally, there is the joker rule: If a “Yahtzee” was already played AND another Yahtzee is thrown AND this (new) “Yahtzee” can not be played in the corresponding field of the upper section anymore THEN this Yahtzee acts as a joker for “Full House” / “Small Straight” / “Large Straight”, giving 25 / 30 / 40 points.

The code snippet below calls all the little functions listed above (is_full_house(), is_three_of_a_kind()) and therefore needs to either include those functions or import them:

``` 1# this program can NOT run by itself!  # ➊➋➌
2
3def calculate_score(dice, name_of_option): # ➍
4    """calculate how much points an option would score using dice""" # ➎
5    # assert expression, error_message
6    assert len(dice) == 5, f"{dice} must have five elements" # ➏
7    error_message = f"{dice} must have only integers"
8    assert [type(x) for x in dice] == [int,int,int,int,int], error_message # ➐
9    error_message = f"{name_of_option} must be in {all_options.keys()}"
10    assert name_of_option in all_options, error_message # ➑
11
12    # joker rule?
13    option_list = list(all_options) # ➒
14    joker = False
15    upper_name = option_list[dice[0]-1] # ➓
16    if all((is_yahtzee(dice),           # ⓫
17            "Yahtzee" in history,
18            upper_name in history)):
19        joker = True
20    match name_of_option:               # ⓬
21        # first test the lower section options
22        case "Three Of A Kind":
23            return sum(dice) if is_three_of_a_kind(dice) else 0 # ⓭
24        case "Four Of A Kind":
25            return sum(dice) if is_four_of_a_kind(dice) else 0
26        case  "Full House":
27            return 25 if (is_full_house(dice) or joker) else 0
28        case  "Small Straight":
29            return 30 if (is_small_straight(dice) or joker) else 0
30        case "Large Straight":
31            return 40 if (is_large_straight(dice) or joker) else 0
32        case "Yahtzee":
33            return 50 if is_yahtzee(dice) else 0
34        case "Chance":
35            return sum(dice)
36        case _:  # ⓮
37            # it's an upper section option:  Ones, Tows, ... Sixes
38            number = option_list.index(name_of_option) + 1 # ⓯
39            return dice.count(number) * number             # ⓰
```

Note that the code example below has no output. To see what the function `calculate_score()` returns, it is necessary to write another program, a specific test program.

### testing calculate_score()#

This is done here in `dicegame01_13.py`. It includes the function `calculate_score()` but without comments (for comments explaining the function, see `dicegame01_12.py` above). It also includes the functions `is_three_of_a_kind()` etc. from the programs `dicegame01_06.py` etc. (see far above) by making use of python’s import command. Finally, the dictionaries `history` and `all_options` are included (and manipulated!) to test a lot of different dice combinations (using python’s assert command).

```  1from dicegame01_06 import is_three_of_a_kind # ➊
2from dicegame01_07 import is_four_of_a_kind
3from dicegame01_08 import is_full_house
4from dicegame01_09 import is_small_straight
5from dicegame01_10 import is_large_straight
6from dicegame01_11 import is_yahtzee
7
8def calculate_score(dice, name_of_option): # ➋
9    """calculate how much points an option would score using dice"""
10    # assert expression, error_message
11    assert len(dice) == 5, f"{dice} must have five elements"
12    error_message = f"{dice} must have only integers"
13    assert [type(x) for x in dice] == [int,int,int,int,int], error_message
14    error_message = f"{name_of_option} must be in {all_options.keys()}"
15    assert name_of_option in all_options, error_message
16
17    # joker rule?
18    option_list = list(all_options) #
19    joker = False
20    upper_name = option_list[dice[0]-1] #
21    if all((is_yahtzee(dice),           #
22            "Yahtzee" in history,
23            upper_name in history)):
24        joker = True
25    match name_of_option:               #
26        # first test the lower section options
27        case "Three Of A Kind":
28            return sum(dice) if is_three_of_a_kind(dice) else 0 # ⓭
29        case "Four Of A Kind":
30            return sum(dice) if is_four_of_a_kind(dice) else 0
31        case  "Full House":
32            return 25 if (is_full_house(dice) or joker) else 0
33        case  "Small Straight":
34            return 30 if (is_small_straight(dice) or joker) else 0
35        case "Large Straight":
36            return 40 if (is_large_straight(dice) or joker) else 0
37        case "Yahtzee":
38            return 50 if is_yahtzee(dice) else 0
39        case "Chance":
40            return sum(dice)
41        case _:
42            # it's an upper section option:  Ones, Tows, ... Sixes
43            number = option_list.index(name_of_option) + 1 # ⓯
44            return dice.count(number) * number             # ⓰
45
46
47history = {}            # ➌
48all_options = {         # ➍
49                                       # upper section:
50           "Ones":            5,
51           "Twos":            10,
52           "Threes":          15,
53           "Fours":           20,
54           "Fives":           25,
55           "Sixes":           30,
56                                        # lower section
57           "Three Of A Kind": 30,
58           "Four Of A Kind":  30,
59           "Full House":      25,
60           "Small Straight":  30,
61           "Large Straight":  40,
62           "Yahtzee":         50,
63           "Chance":          30,
64           }
65
66
67print("testing...")  # ➎
68assert calculate_score([1,1,1,1,3], "Ones") == 4
69assert calculate_score([1,2,2,2,3], "Twos") == 6
70assert calculate_score([1,1,1,2,3], "Threes") == 3
71assert calculate_score([1,1,1,2,3], "Fours") == 0
72assert calculate_score([1,1,5,2,3], "Fives") == 5
73assert calculate_score([6,6,6,6,6], "Sixes") == 30
74assert calculate_score([1,1,1,2,3], "Three Of A Kind") == 8
75assert calculate_score([1,1,2,2,3], "Three Of A Kind") == 0
76assert calculate_score([1,1,1,1,1], "Three Of A Kind") == 5
77assert calculate_score([2,2,2,2,3], "Four Of A Kind") == 11
78assert calculate_score([2,2,2,3,3], "Four Of A Kind") == 0
79assert calculate_score([2,2,2,2,2], "Four Of A Kind") == 10
80assert calculate_score([2,2,2,3,3], "Full House") == 25
81assert calculate_score([2,2,2,2,3], "Full House") == 0
82assert calculate_score([2,2,2,2,2], "Full House") == 0
83assert calculate_score([1,2,3,4,5], "Small Straight") == 30
84assert calculate_score([2,3,4,5,6], "Small Straight") == 30
85assert calculate_score([1,3,4,5,6], "Small Straight") == 30
86assert calculate_score([5,4,3,2,2], "Small Straight") == 30
87assert calculate_score([5,5,6,3,4], "Small Straight") == 30
88assert calculate_score([1,3,3,4,6], "Small Straight") == 0
89assert calculate_score([2,2,3,3,3], "Small Straight") == 0
90assert calculate_score([2,2,2,2,2], "Small Straight") == 0
91assert calculate_score([1,2,3,4,5], "Large Straight") == 40
92assert calculate_score([2,3,4,5,6], "Large Straight") == 40
93assert calculate_score([5,6,3,4,2], "Large Straight") == 40
94assert calculate_score([1,2,3,4,1], "Large Straight") == 0
95assert calculate_score([1,2,3,6,5], "Large Straight") == 0
96assert calculate_score([1,1,1,1,1], "Large Straight") == 0
97assert calculate_score([1,2,3,1,5], "Chance") == 12
98assert calculate_score([1,1,1,1,1], "Yahtzee") == 50
99assert calculate_score([1,1,1,1,2], "Yahtzee") == 0
100# joker rule: put "Ones" and "Yahtzee" in history
101history = {"Ones":None,  # the values are not important now
102        "Yahtzee":None,
103        }
104#print(history) # ➏
105assert calculate_score([1,1,1,1,1], "Small Straight") == 30 # ➍
106assert calculate_score([1,1,1,1,1], "Large Straight") == 40
107assert calculate_score([1,1,1,1,1], "Full House") == 25
108print("all tests passed")
```
output of dicegame01_13.txt#
```testing...
all tests passed
```

Nearly all sub-tasks are now done:

• simulate throwing 5 dice

• let the player keep some dice / throw some dice

• keep track of game round and throws

• ask the player to choose an option and keep track of played options

• show the player how many points they will get for each option

• calculate sum of points at the end of the game

## step five#

calculate the sum of points at the end of the game

Two specific rules need to be checked before calculating the final score of a Yahtzee game:

upper section bonus:

If the sum of the upper section options (“Ones”, “Twos”,…”Sixes”) is equal or larger than 63. If yes, an “Upper section bonus” of 35 points is added to the final score.

Yahtzee bonus:

Not to be confused with the Joker rule (where Yahtzee can act as a joker for Full House etc): The Yahtzee bonus rule checks if Yahtzee was already played sucessfully with 50 points. If yes and if another Yahtzee is rolled, a bonus of 100 points is added to the final score. This bonus applies to each (additional) Yahtzee in a game. For example, if a super lucky player manages to roll a Yathzee in the first game round (played as “Yahtzee” with 50 points) and in the same game manages to roll two additional Yathzees, they would score an Yathzee bonus of 200 points.

Here is a function to calculate the sum of the upper section options, the upper section bonus, the sum of the lower section options and the yahtzee bonus. The final score is the sum of those 4 values. The program below includes the function and a few tests:

``` 1def calculate_final_scores(history):   # ➊
2    """returns a tuple of 4 integers:
3           sum_upper_section,
4           upper_section_bonus,
5           sum_lower_section,
6           yahtzee_bonus
7    """                                # ➋
8    sum_upper_section = 0              # ➌
9    sum_lower_section = 0
10    upper_section_bonus = 0
11    yahtzee_bonus = 0
12    allow_yahtzee_bonus = False
13    for name in history:               # ➍
14        scored = history[name][0]      # ➎
15        dice = history[name][1]        # ➏
16        # is upper section?            # ➐
17        if name in ("Ones", "Twos", "Threes",
18                    "Fours", "Fives", "Sixes"):
19            sum_upper_section += scored
20        else:
21            sum_lower_section += scored
22        # is Yahtzee?                  # ➑
23        if len(set(dice)) == 1:
24            # enable Yahtzee bonus ?
25            if scored == 50 and not allow_yahtzee_bonus:
26                allow_yahtzee_bonus = True
27            elif allow_yahtzee_bonus:
28                yahtzee_bonus += 100
29    if sum_upper_section >= 63:        # ➒
30        upper_section_bonus = 35
31    return sum_upper_section, upper_section_bonus, sum_lower_section, yahtzee_bonus
32
33# testing
34
35if __name__ == "__main__":             # ➓
36    print("testing...")
37    # example 1:
38    # get upper section bonus of 35
39    # because sum(upper_section) > 63
40    history = {"Ones":  (5, [1,1,1,1,1]),
41               "Twos":  (10,[2,2,2,2,2]),
42               "Threes":(15,[3,3,3,3,3]),
43               "Fours": (20,[4,4,4,4,4]),
44               "Fives": (15,[5,5,5,1,1]),
45               "Sixes": (0,[1,2,6,6,3]),
46               }
47    assert sum(calculate_final_scores(history)) == 100
48    # test the upper section bonus  directly
49    # it's the second element of the return value
50    # a second element in python has the index 1
51    assert calculate_final_scores(history)[1] == 35
52    # example 2:
53    # get yahtzee bonus of 100:
54    history = {"Ones":   (5, [1,1,1,1,1]),
55               "Yahtzee":(50, [2,2,2,2,2]),
56               "Twos":   (10, [2,2,2,2,2])}
57    assert sum(calculate_final_scores(history)) == 165
58    # example 3
59    # get yahtzee bonus of 200:
60    history = {"Ones":   (5, [1,1,1,1,1]),
61               "Yahtzee":(50, [2,2,2,2,2]),
62               "Twos":   (10, [2,2,2,2,2]),
63               "Threes": (15, [3,3,3,3,3])}
64    assert sum(calculate_final_scores(history)) == 280
65    # example4:
66    # don't get yahtzee bonus because Yahtzee
67    # because Yahtzee was played (used up) with
69    history = {"Ones":(5, [1,1,1,1,1]),
70               "Yahtzee":(0, [3,4,2,2,2]),
71               "Twos":(10, [2,2,2,2,2])}
72    assert sum(calculate_final_scores(history)) == 15
73
74    print("all tests passed")
```
output of dicegame01_14.txt#
```testing...
all tests passed
```

• simulate throwing 5 dice

• let the player keep some dice / throw some dice

• keep track of game round and throws

• ask the player to choose an option and keep track of played options

• show the player how many points they will get for each option

• calculate sum of points at the end of the game

### merging everything together#

The end of this tutorial is reached. The last task is to merge all the “little” functions together into one big python program that can run alone. Note that tests and detailed comments can be found in the code examples above.

```  1import random
2history = {}            # ➊
3all_options = {         # ➋
4                                       # upper section:
5           "Ones":            5,
6           "Twos":            10,
7           "Threes":          15,
8           "Fours":           20,
9           "Fives":           25,
10           "Sixes":           30,
11                                        # lower section
12           "Three Of A Kind": 30,
13           "Four Of A Kind":  30,
14           "Full House":      25,
15           "Small Straight":  30,
16           "Large Straight":  40,
17           "Yahtzee":         50,
18           "Chance":          30,
19           }
20
21# functions to test options
22
23def is_three_of_a_kind(dice):              #
24    """dice is a list of five integers"""  #
25    for number in dice:                    #
26        if dice.count(number) >= 3:        #
27            return True                    #
28    return False
29
30def is_four_of_a_kind(dice):               #
31    """dice is a list of five integers"""  #
32    for number in dice:                    #
33        if dice.count(number) >= 4:        #
34            return True                    #
35    return False
36
37def is_full_house(dice):                      #
38    """dice is a list of five integers"""     #
39    diceset = set(dice)                       #
40    if len(diceset) == 2:                     #
41        for number in diceset:                #
42            if dice.count(number) in (2,3):   #
43                return True                   #
44    return False                              #
45
46def is_small_straight(dice):                  #
47    """dice is a list of five integers"""     #
48    diceset = set(dice)                       #
49    if any((diceset.issuperset({1,2,3,4}),    #
50            diceset.issuperset({2,3,4,5}),
51            diceset.issuperset({3,4,5,6}))):
52        return True                           #
53    return False
54
55def is_large_straight(dice):                  #
56    """dice is a list of five integers"""     #
57    diceset = set(dice)                       #
58    if (diceset.issuperset({1,2,3,4,5}) or    #
59        diceset.issuperset({2,3,4,5,6})):
60        return True                           #
61    return False
62
63def is_yahtzee(dice):                      #
64    """dice is a list of five integers"""  #
65    return len(set(dice)) == 1             #
66
67def calculate_score(dice, name_of_option): #
68    """calculate how much points an option would score using dice"""
69    # assert expression, error_message
70    assert len(dice) == 5, f"{dice} must have five elements"
71    error_message = f"{dice} must have only integers"
72    assert [type(x) for x in dice] == [int,int,int,int,int], error_message
73    error_message = f"{name_of_option} must be in {all_options.keys()}"
74    assert name_of_option in all_options, error_message
75
76    # joker rule?
77    option_list = list(all_options) #
78    joker = False
79    upper_name = option_list[dice[0]-1] #
80    if all((is_yahtzee(dice),           #
81            "Yahtzee" in history,
82            upper_name in history)):
83        joker = True
84    match name_of_option:               #
85        # first test the lower section options
86        case "Three Of A Kind":
87            return sum(dice) if is_three_of_a_kind(dice) else 0 #
88        case "Four Of A Kind":
89            return sum(dice) if is_four_of_a_kind(dice) else 0
90        case  "Full House":
91            return 25 if (is_full_house(dice) or joker) else 0
92        case  "Small Straight":
93            return 30 if (is_small_straight(dice) or joker) else 0
94        case "Large Straight":
95            return 40 if (is_large_straight(dice) or joker) else 0
96        case "Yahtzee":
97            return 50 if is_yahtzee(dice) else 0
98        case "Chance":
99            return sum(dice)
100        case _:
101            # it's an upper section option:  Ones, Tows, ... Sixes
102            number = option_list.index(name_of_option) + 1 #
103            return dice.count(number) * number             #
104
105def calculate_final_scores(history):   #
106    """returns a tuple of 4 integers:
107           sum_upper_section,
108           upper_section_bonus,
109           sum_lower_section,
110           yahtzee_bonus
111    """                                #
112    sum_upper_section = 0              #
113    sum_lower_section = 0
114    upper_section_bonus = 0
115    yahtzee_bonus = 0
116    allow_yahtzee_bonus = False
117    for name in history:               #
118        scored = history[name][0]      #
119        dice = history[name][1]        #
120        # is upper section?            #
121        if name in ("Ones", "Twos", "Threes",
122                    "Fours", "Fives", "Sixes"):
123            sum_upper_section += scored
124        else:
125            sum_lower_section += scored
126        # is Yahtzee?                  #
127        if len(set(dice)) == 1:
128            # enable Yahtzee bonus ?
129            if scored == 50 and not allow_yahtzee_bonus:
130                allow_yahtzee_bonus = True
131            elif allow_yahtzee_bonus:
132                yahtzee_bonus += 100
133    if sum_upper_section >= 63:        #
134        upper_section_bonus = 35
135    return sum_upper_section, upper_section_bonus, sum_lower_section, yahtzee_bonus
136
137# main function
138
139def game():
140    for game_round in range(1,14):
141        dicelist = []
142        for _ in range(5):
143            dicelist.append(random.randint(1,6))
144        print(f"-------- round {game_round} of 13--------------")
145        for throw in (1,2,3):
146            text= "throw: {} of 3".format(throw)
147            print(text)
148            print(" a  b  c  d  e")
149            print(dicelist)
150            if throw < 3:
151                text = "Type letter(s) of dice to keep and press ENTER >>>"
152                command = input(text).lower()
153                for i, char in enumerate("abcde") :
154                    if char not in command:
155                        dicelist[i] = random.randint(1,6)
156
157        print(f"------- end of round {game_round}------")
158        # calculate playable options    #
159        unplayed = [name for name in all_options if name not in history]
160        while True:                     #
161            # create menu, starting with number 1 for "Ones"
162            for name in unplayed:
163                print(list(all_options).index(name)+1, name) #
164            command = input("enter number of option to play >>>")
165            try:                         #
166                number = int(command)    #
167            except ValueError:           #
168                print("Please enter only a NUMBER")
169                continue                 #
170            if not (0 < number < 14):    #
171                print("Number out of allowed range, try again")
172                continue                 #
173            #try:
174            option = list(all_options)[number-1] #
175            #except IndexError:
176            #    print("Please enter only a listed number")
177            #    continue
178            if option not in unplayed:   #
179                print(f"You already played {option}, try again")
180                continue                 #
181            print("You play: ", option)
182            break                        #
183        # option should be correct now
184        #points = all_options[option]     #
185        points = calculate_score(dicelist, option)
186        print(f"you get {points} points!")
187        if game_round == 13:
188            text = "press ENTER to see the history of dice throws >>>"
189        else:
190            text = "press ENTER to start the next game round >>>"
191        input(text)
192        history[option] = (points, dicelist.copy())  #
193    print("Game Over")
194    print("--- history of your dice throws: ---")
195    print("game round,  played_option, points, dicelist")
196    sum_upper, upper_bonus, sum_lower, yat_bonus = calculate_final_scores(history)
197    print("option,          score (of max.)    dice")
198    for name in all_options:
199        print("{:<20}   {:>2} (of {:>2})   {:<20}".format(
200            name,
201            history[name][0],
202            all_options[name],
203            str(history[name][1])
204        ))
205        if name == "Sixes":
206            print("-------------------------")
207            print("sum upper section:", sum_upper)
208            print("upper section bonus:", upper_bonus)
209    print("--------------------")
210    print("sum lower section", sum_lower)
211    print("Yahtzee bonus:", yat_bonus)
212    print("==========================")
213    print("final score:",
214           sum_upper+upper_bonus+sum_lower+yat_bonus)
215
216
217
218
219if __name__ == "__main__":
220    game()
```
output of dicegame01_15.txt#
```---(snip)----
Game Over
--- history of your dice throws: ---
game round,  played_option, points, dicelist
option,          score (of max.)    dice
Ones                    1 (of  5)   [6, 4, 1, 4, 4]
Twos                    0 (of 10)   [5, 1, 3, 6, 4]
Threes                  0 (of 15)   [4, 5, 4, 4, 2]
Fours                   0 (of 20)   [6, 6, 5, 1, 2]
Fives                   0 (of 25)   [2, 3, 4, 1, 2]
Sixes                   0 (of 30)   [1, 4, 1, 1, 3]
-------------------------
sum upper section: 1
upper section bonus: 0
Three Of A Kind        21 (of 30)   [1, 5, 5, 5, 5]
Four Of A Kind          8 (of 30)   [1, 1, 1, 4, 1]
Full House              0 (of 25)   [6, 6, 3, 4, 4]
Small Straight          0 (of 30)   [6, 3, 3, 2, 3]
Large Straight          0 (of 40)   [4, 2, 3, 2, 3]
Yahtzee                 0 (of 50)   [6, 4, 3, 5, 1]
Chance                 16 (of 30)   [4, 5, 5, 1, 1]
--------------------
sum lower section 45
Yahtzee bonus: 0
==========================
final score: 46
```