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
preparation#
warming up:
read the rules of “Yahtzee” or similar games:
Poker Dice: https://en.wikipedia.org/wiki/Poker_Dice
Generala: https://en.wikipedia.org/wiki/Generala
See http://www.yahtzee-rules.com/ for alternative Yahtzee rules
prepare five dice. Each die has six sides, representing numbers from 1 to 6.
play some rounds of “Yahtzee” without computer
Software:
make sure Python is installed on your computer: see https://python.org
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.
divide task into small tasks#
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#
- task:
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) # ➌
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) # ➍
[6, 2, 2, 1, 5]
--- /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#
- task:
let the player keep some dice / throw some dice
- task:
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)
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]
--- /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}" ) # ⓬
-------- 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]
--- /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#
- task:
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:
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)
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)
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)
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#
- task:
show the player how many points they will get for each option
- task:
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:
To check if the conditions for the option are met by the numbers inside the variable
dicelist
.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 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
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") # ➑
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") # ➑
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") # ➓
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") # ➑
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") # ➑
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") # ➎
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")
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#
- last remaining task:
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
68 # zero points already:
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")
testing...
all tests passed
This was the final sub-task!
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()
---(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