Homework 1: Roll your dice!
Yahtzee is a popular dice game that was conceived by Milton Bradley in the early 1940s. In the game, the player rolls five dice and chooses a score for the combination of die faces according to several scoring categories. Once one category has been chosen, it cannot be longer be used in future rolls. The scoring categories have varying point values, which can be fixed values or can depend on the values of die faces. In a multiplayer setting, the winner player is the one that scores the most points.
The scoring of a Yahtzee game is recorded in cards that are distributed to each one of the players. The scoring card is divided into two sections. In the so-called upper section, there are six boxes, which correspond to the six different values on a die. Correspondingly, the score in each of these boxes is determined by adding the total number of dice matching the box. In the so-called lower section, the scoring system is inspired in poker hands each of which leads to different points.
In this homework, you will implement functions to score a combination of dice according to different scoring categories by following the Test-Driven Development approach.
General information:
- The homework can be completed in pairs (2-person team).
- Submission deadline: Friday 09.10.20 at 23:59
- The code with the solution must be pushed into a private repository (only one repository per pair/solution). You can use GitHub, Bitbucket, or GitLab, as you prefer.
- Please add all the teachers to your repo -- the emails are posted in the home page of the course website.
- NB: If you use Bitbucket, make sure you didn’t reach the user limit. In case you have reached the limit, you can upgrade your plan by using your UT email account.
- The homework is composed of 6 parts. Parts 1 to 5 are concerned with the implementation of the basic scoring of a yahtzee game. Part 6 requires you writing a set of test cases to do a sanity check of the whole implementation.
- The maximum points of each part are indicated in the title of each one of them.
- There are many versions of this game around. In this homework, you must provide a solution based on the requirements described in this narrative. Solutions based on variations of the game that have different rules to the described in this document will not be considered.
Setting up
This could have been called Part 0, because it would not be graded. The intention with this part is to illustrate the process we are expecting you to follow while solving this problem. That said, you cannot skip this part because we are going to set up the elixir project you will use later.
Henceforth, we will start by creating the Elixir project and setting up your development environment. To that end, in a terminal window, execute the following command:
mix new yahtzee cd yahtzee
The above creates a skeleton for our application, with the basic files including configuration, test and sample elixir modules as discussed during the laboratory sessions. Open the project in a code editor (e.g. visual studio code) and explore the set of files.
Before you continue, we ask you to create a private code repository on Bitbucket or GitHub, if you haven’t done it yet. It is important for you to keep committing your code to allow us to track your progress as well as the evolution of your adoption of the functional programming style. We ask you to commit your code every time you add a new test, and when you complete the changes in your code to pass the newly added test. We will penalize your submission if you do not commit your code as requested!
For convenience, I propose you to change the name of the file test/yahtzee_test.exs
to test/yahtzee_upper_section_test.exs
. Open that file and change the content with the following snippet:
defmodule YahtzeeUpperSectionTest do use ExUnit.Case test "works with 1 one" do assert %{Ones: 1} = Yahtzee.score_upper([1,2,3,4,5]) end end
The above corresponds to a case where the player rolls the dice and gets the unlikely combination of die faces: “1,2,3,4,5”. Let us try running the test, by entering the command mix test
in the terminal window. Of course, we know the test must fail, as expected because we are following the TDD approach.
Please commit your code now! You can use the commit message to indicate the part you are solving and if the associated test case pass or fail. In this case, commit -m "[Part 0 - Fail] first test added"
At this point we are specifying a set of initial expectations: our application must have a module Yahtzee
which in turn must implement a function score_upper/1
that receives a list of 5 integers and returns a map that matches %{Ones: 1}
. Within the skeleton application, there is already a module Yahtzee (see file lib/yahtzee.ex) which includes a function implementing the traditional hello world function. Well, we can safely replace the code of lib/yahtzee.ex with the following snippet to get our first test passing:
defmodule Yahtzee do def score_upper(_dice) do %{Ones: 1} end end
The code above is the simplest code we need to get the test passing. Please check my claim by running the command mix test
.
It is here that you have to commit your code! In this case, commit -m "[Part 0 - Pass] first test added"
You will agree with me that, using the above approach, we would need to enumerate a large number of test cases with different dice combinations. Instead, I would propose you an approach that generates test cases randomly. To that end, I will ask you to replace fully the content of file test/yahtzee_upper_section_test.exs
with the following snippet.
defmodule YahtzeeUpperSectionTest do use ExUnit.Case doctest Yahtzee def generate(die_face, occurrences) do Enum.to_list(1..6) |> List.delete(die_face) |> Enum.shuffle |> Enum.take(5 - occurrences) |> Enum.concat(List.duplicate(die_face, occurrences)) |> Enum.shuffle end test "works with 1 one" do assert %{Ones: 1} = Yahtzee.score_upper([1,2,3,4,5]) end test "works with any combination of dice, containing 1-5 ones" do Enum.map(1..5, fn n -> assert %{Ones: ^n} = Yahtzee.score_upper(generate(1, n)) end) end end
You can see that the code above includes a function generate/2
that receives two parameters corresponding to the die face (i.e. a number between 1-6 corresponding to the face in the die) and the number of occurrences of that face in the combination of dice to be generated. As part of the assignment, I would ask you to check each one of the functions used within geneate/2
. To understand the purpose of such function, run the command iex.bat -S mix test
in your terminal. Then execute the function call YahtzeeUpperSectionTest.generate(1,1)
several times to see what happens (the function will return a list with combinations of die faces, containing exactly one occurrence of ‘1’). Try now YahtzeeUpperSectionTest.generate(1,3)
and see what happens.
Do not forget to commit your code at this point!
Let us now try to get the second test passing. There are multiple approaches to accomplish this. Here, I will use an approach that is simple, maybe suboptimal, but easy to understand. Replace the content of file lib/yahtzee.ex
with the following snippet.
defmodule Yahtzee do def score_upper(dice) do %{Ones: length(Enum.filter(dice, fn e -> e == 1 end))} end end
The fragment Enum.filter(dice, fn e -> e == 1 end)
traverses the list, keeping all the occurrences of ‘1’. Then the function Kernel.length/1
computes the size of the list. With that information now it is possible to build the map that is returned by our function. Try out, you will see that this code gets the test passing.
Commit your code at this moment. I will no longer repeat that you must commit your code, but do not forget this is a requirement of mine!
Here, we finish the warm-up. You will be responsible for the rest of the steps.
Part 1 (0.6 points)
Let us now complete the scoring of the upper section of the game. To that end, copy the following test in the file test/yahtzee_upper_section_test.exs
. Please note that you are expected to add the new test case while keeping the other tests.
test "works with twos" do Enum.map(1..5, fn n -> assert %{Twos: ^n} = Yahtzee.score_upper(generate(2, n)) end) end
The structure of the above test is such that we are adding cases where the die face ‘2’ appears one or multiple times in a die combination. It is also important to note that while this test is matching the scoring of face ‘2’, your solution should also continue to score properly the die face ‘1’.
After you get the above test passing, you would need to generalize your solution to work all the other die faces. Please copy the following test case into your file test/yahtzee_upper_section.exs
and use it to guide you in completing the scoring.
test "works on upper section, with all the other cases" do Enum.map(1..5, fn n -> assert %{Threes: ^n} = Yahtzee.score_upper(generate(3, n)) end) Enum.map(1..5, fn n -> assert %{Fours: ^n} = Yahtzee.score_upper(generate(4, n)) end) Enum.map(1..5, fn n -> assert %{Fives: ^n} = Yahtzee.score_upper(generate(5, n)) end) Enum.map(1..5, fn n -> assert %{Sixes: ^n} = Yahtzee.score_upper(generate(6, n)) end) end
Part 2 (0.4 points)
Let us proceed, now implementing the scoring of the lower section. In this case, I propose you to add one case at a time. Therefore, we will start with the “Three of a kind” combination, which corresponds to the case where three of the dice show the same face and the other two show different faces.
For instance, the following combination of dices:
[2][3][4][4][4]
would be recognized as a “Three of a kind”, because it includes three “four”s. In this case, the score will be the sum of all the die faces, that is 17 points.
With this in mind, let us now turn our attention to the test. Copy the following snippet into a new file called test/yahtzee_lower_section_test.exs
.
defmodule YahtzeeLowerSectionTest do use ExUnit.Case def generate(dice_face, occurrences) do Enum.to_list(1..6) |> List.delete(dice_face) |> Enum.shuffle |> Enum.take(5 - occurrences) |> Enum.concat(List.duplicate(dice_face, occurrences)) |> Enum.shuffle end test "Identify 'Three of a kind' with ones" do dices = generate(1, 3) sum = Enum.sum(dices) assert %{"Three of a kind": ^sum} = Yahtzee.score_lower(dices) end test "Identify 'Three of a kind' with all the others" do Enum.map(2..6, fn (dice_face) -> dices = generate(dice_face, 3) sum = Enum.sum(dices) assert %{"Three of a kind": ^sum} = Yahtzee.score_lower(dices) end) end end
Sure enough, you can start by commenting out the second test to focus on a single test case at a time. Comment out the second test once you are done with the first one. To speed up your work, you can consider using the following command:
mix test test/yahtzee_lower_section_test.exs
instead of the traditional mix test
. The above command executes only the tests in the new file and ignores the test in the file test/yahtzee_upper_section_test.exs
.
Part 3 (0.6 points)
Let us now focus on another die combination, referred to as “Four of a kind”. One example of this combination is shown below.
[4][5][5][5][5]
As you can see, a “Four of a kind” consists of a die combination that includes four dice with the same face. In the example above, there are four dice showing the face five. In this case, the score is also the sum of all the faces in the combination. Therefore, the score for the example above is 24.
Copy the following test case into the file test/yahtzee_lower_section_test.exs
. Do not remove the previous test cases.
test "Identify 'Four of a kind' with every face" do Enum.map(1..6, fn (dice_face) -> dices = generate(dice_face, 4) sum = Enum.sum(dices) assert %{"Four of a kind": ^sum} = Yahtzee.score_lower(dices) end) end
In this part of the homework, we will also add the scoring of the die combination called “Full house”, which corresponds to the case where we have three dice with the same face and the other two also match. One example of this is shown below:
[2][2][5][5][5]
The score for a “Full house” is always 25 points, without taking into account the values displayed in the dice combination. Copy the following test in the file test/yahtzee_lower_section_test.exs
to guide you in the implementation of the scoring of a “Full house”.
test "Identify 'Full house' with every face" do Enum.map(1..6, fn _ -> [x,y] = Enum.shuffle(1..6) |> Enum.take(2) assert %{"Full house": 25} = Yahtzee.score_lower([x,x,x,y,y] |> Enum.shuffle) end) end
Part 4 (1.2 points)
A Straight is a sequence of consecutive die faces. A small straight consists of 4 consecutive faces, and a large straight 5 consecutive faces.
For example, the following combinations are small straights:
[2][3][2][5][4] [1][3][4][5][6]
Small straights score 30 points.
On the other hand, large straights score 40 points. One example of this is shown below:
[2][3][4][5][6]
For this part, you must write your own tests. Please remember we are following the TDD approach, so you should commit your tests first and later the solution passing tests.
Note that small straights are only counted if no large straight exists.
Part 5 (0.6 points)
We are almost done. Actually, we have only two dice combinations left. The first one is referred to as “Yahtzee”. A “Yahtzee” is simply a dice combination where all die show the same face. Just in case, you will find an example of a “Yahtzee” below.
[2][2][2][2][2]
A “Yahtzee” is scored with 50 points. Let us then copy the test case below in the file test/yahtzee_lower_section_test.exs (do not remove anything in there).
test "Identify 'Yahtzee'" do Enum.map(1..6, fn n -> assert %{Yahtzee: 50} = Yahtzee.score_lower(List.duplicate(n,5)) end) end
Finally, we have to consider the “Chance”. The latter corresponds to the case where the dice combination cannot be classified as any of the previous categories. For instance, the following dice combination:
[1][1][2][2][4]
has to be recognized as a “Chance”. Well, there are two pairs of dice matching. However, we will just ignore that fact and, for the lower section, the above combination will be scored with the sum of all the dice faces. In the example, that corresponds to a score of 10 points. The test case for a “Chance” follows. You just have to copy the snippet below in the file test/yahtzee_lower_section_test.exs
as before.
test "Identify any other combination" do Enum.map(1..6, fn _ -> [x,y,z] = Enum.shuffle(1..6) |> Enum.take(3) seq = Enum.shuffle([x,x,y,y,z]) sum = Enum.sum(seq) assert %{Chance: ^sum} = Yahtzee.score_lower(seq) end) end
Part 6 (0.6 points)
You will notice that some of the dice configurations can be scored with different categories. For instance, the dice configuration:
[2][2][5][5][5]
can be scored in the categories: Twos, Fives, Three of a kind, Full house, etc. Each one of these categories should raise its corresponding score and, importantly, all the other categories should be scored with zero. None of the previous test cases checks this situation.
In this part, you must write at least 3 test cases that verify the categories and scoring of different dice configurations. The given example can be used as input of one of the test cases.
We expect you to write test cases that verify different sets of categories. For example, the dice configurations [2][2][5][5][5]
and [2][5][2][5][5]
correspond to the same set of categories; thus, two test cases using these inputs will be considered as equivalent test cases.
Submission:
- Remember to use the commit messages conveniently. When making commits, please indicate the part you are solving and if the associated test case pass or fail. For example,
commit -m "[Part 1 - Fail] first test added"
- Once you complete the homework, submit (via Moodle) the link to the bitbucket repository.
Grading:
You can get a maximum of 4 points for this homework. Team members receive equal grades.
Each part of the solution is graded individually. The maximum points you can get for each part are described above. The grade of each part is determined based on:
- If you implemented one part (scoring rule) at a time
- If the commits related to the solution of each part show evidence of TDD, as it was described in Section “setting up”.
- If the written tests cover the cases described in the requirement specification (narrative)
- If the provided implementation fulfils the requirements described by the narrative.