Today I’m starting a new blog post series about solving code katas in F# and with the help of my NaturalSpec project. A code kata is a programming exercise which helps to improve your skills through practice and repetition. In this series we want to use the Test Driven Development TDD approach which means in the context of NaturalSpec that we have to write our specs before we implement the algorithm.
Problem Description
“The game of yahtzee is a simple dice game. Each round, each player rolls five six sided dice. The player may choose to reroll some or all of the dice up to three times (including the original roll). The player then places the roll at a category, such as ones, twos, sixes, pair, two pairs etc. If the roll is compatible with the score, the player gets a score for this roll according to the rules. If the roll is not compatible, the player gets a score of zero for this roll.
The kata consists of creating the rules to score a roll in any of a predefined category. Given a roll and a category, the final solution should output the score for this roll placed in this category.”
Category 1 – Ones, Twos, Threes, Fours, Fives, Sixes
“Ones, Twos, Threes, Fours, Fives, Sixes: The player scores the sum of the dice that reads one, two, three, four, five or six, respectively. For example, 1, 1, 2, 4, 4 placed on "fours" gives 8 points.”
After reading this category description we could come up with the following spec:
module Yahtzee.Specs
open NaturalSpec
let placed_on category list =
printMethod category
calcValue category list
[<Scenario>]
let “Given 1,1,2,4,4 placed on "fours" gives 8 points.“ () =
Given (1, 1, 2, 4, 4)
|> When (placed_on Fours)
|> It should equal 8
|> Verify
Since we really want to implement six different categories we should also add some more scenarios like this one:
[<Scenario>]
let “Given 1,1,6,4,6 placed on "sixes" gives 12 points.“ ()=
Given (1, 1, 6, 4, 6)
|> When (placed_on Sixes)
|> It should equal 12
|> Verify
// …
Now we have some specs but they will all fail since we don’t have anything implemented yet. So we have to come up with a model of dice rolls and categories. As the specs suggests we model the dice roll as a tuple of ints and the category as a discriminated union:
module Yahtzee.Model
type Roll = int * int * int * int * int
type Category =
| Ones
| Twos
| Threes
| Fours
| Fives
| Sixes
The tuple is a natural choice for the dice roll, but for easier calculation we add a helper function which converts it into a list. This allows use to use the standard list functions and therefore summing the values becomes trivial:
let toList (roll:Roll) =
let a,b,c,d,e = roll
[a;b;c;d;e]
let sumNumber number =
Seq.filter ((=) number)
>> Seq.sum
let calcValue category roll =
let list = toList roll
match category with
| Ones -> sumNumber 1 list
| Twos -> sumNumber 2 list
| Threes -> sumNumber 3 list
| Fours -> sumNumber 4 list
| Fives -> sumNumber 5 list
| Sixes -> sumNumber 6 list
Now we can run our scenarios with any NUnit runner. I’m using the default NUnit GUI runner here, which gives me a picture like this:
Category 2 – Pair
“Pair: The player scores the sum of the two highest matching dice. For example, 3, 3, 3, 4, 4 placed on "pair" gives 8.”
The kata description gives us some new scenarios. As seen above we should specify them before writing the code.
[<Scenario>]
let “Given 3,3,3,4,4 placed on "pair" gives 8.“ () =
Given (3, 3, 3, 4, 4)
|> When (placed_on Pair)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 5,3,5,4,4 placed on "pair" gives 10.“ () =
Given (5, 3, 5, 4, 4)
|> When (placed_on Pair)
|> It should equal 10
|> Verify
[<Scenario>]
let “Given 1,2,3,4,5 placed on "pair" gives 0.“ () =
Given (1, 2, 3, 4, 5)
|> When (placed_on Pair)
|> It should equal 0
|> Verify
Since we use a new category we now have to extend our model and the calcValue function:
type Category =
| Ones
// …
| Sixes
| Pair
// …
let sumAsPair list number =
let numberCount =
list
|> Seq.filter ((=) number)
|> Seq.length
if numberCount >= 2 then 2 * number else 0
let calcValue category roll =
let list = toList roll
match category with
| Ones -> sumNumber 1 list
// …
| Sixes -> sumNumber 6 list
| Pair ->
[1..6]
|> Seq.map (sumAsPair list)
|> Seq.max
Category 3 – Two pairs
“Two pairs: If there are two pairs of dice with the same number, the player scores the sum of these dice. If not, the player scores 0. For example, 1, 1, 2, 3, 3 placed on "two pairs" gives 8.”
[<Scenario>]
let “Given 1,1,2,3,3 placed on "two pair" gives 8.“ () =
Given (1, 1, 2, 3, 3)
|> When (placed_on TwoPair)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 1,6,6,3,3 placed on "two pair" gives 18.“ () =
Given (1, 6, 6, 3, 3)
|> When (placed_on TwoPair)
|> It should equal 18
|> Verify
[<Scenario>]
let “Given 1,1,2,4,3 placed on "two pair" gives 0.“ () =
Given (1, 1, 2, 4, 3)
|> When (placed_on TwoPair)
|> It should equal 0
|> Verify
Implementing this category is a little bit tricky but with the help of our Pair function and some more standard sequence combinators we can get our spec green:
type Category =
| Ones
// …
| TwoPair
let allPairs =
[for i in 1..6 do
for j in 1..6 -> i,j]
let calcValue category roll =
// …
| TwoPair ->
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsPair list a
let b’ = sumAsPair list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
Category 4 – Three of a kind
“Three of a kind: If there are three dice with the same number, the player scores the sum of these dice. Otherwise, the player scores 0. For example, 3, 3, 3, 4, 5 places on "three of a kind" gives 9.”
[<Scenario>]
let “Given 3,3,3,4,5 placed on "three of a kind" gives 9“()=
Given (3, 3, 3, 4, 5)
|> When (placed_on ThreeOfAKind)
|> It should equal 9
|> Verify
[<Scenario>]
let “Given 3,4,3,4,5 placed on "three of a kind" gives 0“()=
Given (3, 4, 3, 4, 5)
|> When (placed_on ThreeOfAKind)
|> It should equal 0
|> Verify
Now it is time to refactor our code. The sumAsPair function should be extended to a sumAsTuple function:
type Category =
| Ones
// …
| ThreeOfAKind
let sumAsTuple value list number =
let numberCount =
list
|> Seq.filter ((=) number)
|> Seq.length
let takeBestTuple value list =
[1..6]
|> Seq.map (sumAsTuple value list)
|> Seq.max
if numberCount >= value then value * number else 0
let calcValue category roll =
// …
| Pair -> takeBestTuple 2 list
| TwoPair ->
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsTuple 2 list a
let b’ = sumAsTuple 2 list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
| ThreeOfAKind –> takeBestTuple 3 list
Category 5 – Four of a kind
“Four of a kind: If there are four dice with the same number, the player scores the sum of these dice. Otherwise, the player scores 0. For example, 2, 2, 2, 2, 5 places on "four of a kind" gives 8.”
[<Scenario>]
let “Given 2,2,2,2,5 placed on "four of a kind" gives 8“ ()=
Given (2, 2, 2, 2, 5)
|> When (placed_on FourOfAKind)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 2,6,2,2,5 placed on "four of a kind" gives 0“ ()=
Given (2, 6, 2, 2, 5)
|> When (placed_on FourOfAKind)
|> It should equal 0
|> Verify
With the help of the takeBestTuple function this becomes trivial:
type Category =
| Ones
// …
| FourOfAKind
let calcValue category roll =
// …
| FourOfAKind -> takeBestTuple 4 list
Category 6 – Small straight
“Small straight: If the dice read 1,2,3,4,5, the player scores 15 (the sum of all the dice), otherwise 0.”
[<Scenario>]
let “Given 1,2,3,4,5 placed on "Small Straight" gives 15“()=
Given (1,2,3,4,5)
|> When (placed_on SmallStraight)
|> It should equal 15
|> Verify
[<Scenario>]
let “Given 1,2,5,4,3 placed on "Small Straight" gives 15“()=
Given (1,2,5,4,3)
|> When (placed_on SmallStraight)
|> It should equal 15
|> Verify
[<Scenario>]
let “Given 1,2,6,4,3 placed on "Small Straight" gives 0“()=
Given (1,2,6,4,3)
|> When (placed_on SmallStraight)
|> It should equal 0
|> Verify
As in all the above scenarios we don’t assume any specific order in our rolls but for this category it is easier to test if the data is sorted:
type Category =
| Ones
// …
| SmallStraight
let calcValue category roll =
// …
| SmallStraight ->
match list |> List.sort with
| [1;2;3;4;5] -> 15
| _ -> 0
Category 7 – Large straight
“Large straight: If the dice read 2,3,4,5,6, the player scores 20 (the sum of all the dice), otherwise 0.”
[<Scenario>]
let “Given 2,3,4,5,6 placed on "Large Straight" gives 20“()=
Given (2,3,4,5,6)
|> When (placed_on LargeStraight)
|> It should equal 20
|> Verify
[<Scenario>]
let “Given 6,2,5,4,3 placed on "Large Straight" gives 20“()=
Given (6,2,5,4,3)
|> When (placed_on LargeStraight)
|> It should equal 20
|> Verify
[<Scenario>]
let “Given 1,2,6,4,3 placed on "Large Straight" gives 0“()=
Given (1,2,6,4,3)
|> When (placed_on LargeStraight)
|> It should equal 0
|> Verify
Of course the implementation is exactly the same as for the small straight:
type Category =
| Ones
// …
| LargeStraight
let calcValue category roll =
// …
| LargeStraight ->
match list |> List.sort with
| [2;3;4;5;6] -> 20
| _ -> 0
Category 8 – Full house
“Full house: If the dice are two of a kind and three of a kind, the player scores the sum of all the dice. For example, 1,1,2,2,2 placed on "full house" gives 8. 4,4,4,4,4 is not "full house".”
[<Scenario>]
let “Given 1,1,2,2,2 placed on "full house" gives 8.“ () =
Given (1,1,2,2,2)
|> When (placed_on FullHouse)
|> It should equal 8
|> Verify
[<Scenario>]
let “Given 4,4,4,4,4 placed on "full house" gives 0.“ () =
Given (4,4,4,4,4)
|> When (placed_on FullHouse)
|> It should equal 0
|> Verify
[<Scenario>]
let “Given 1,1,2,3,2 placed on "full house" gives 0.“ () =
Given (1,1,2,3,2)
|> When (placed_on FullHouse)
|> It should equal 0
|> Verify
Implementing the FullHouse category is easy if we reuse our solutions to the Two pairs category:
type Category =
| Ones
// …
| FullHouse
let takeBestCombo value1 value2 list =
allPairs
|> Seq.filter (fun (a,b) -> a <> b)
|> Seq.map (fun (a,b) ->
let a’ = sumAsTuple value1 list a
let b’ = sumAsTuple value2 list b
if a’ = 0 || b’ = 0 then 0 else a’ + b’)
|> Seq.max
let calcValue category roll =
// …
| TwoPair -> takeBestCombo 2 2 list
// …
| FullHouse -> takeBestCombo 2 3 list
Category 9 – Yahtzee
“Yahtzee: If all dice are the have the same number, the player scores 50 points, otherwise 0.”
Here we can use NaturalSpec’s ScenarioTemplates in order to specify all Yahtzees:
[<ScenarioTemplate(1)>]
[<ScenarioTemplate(2)>]
[<ScenarioTemplate(3)>]
[<ScenarioTemplate(4)>]
[<ScenarioTemplate(5)>]
[<ScenarioTemplate(6)>]
let “Given n,n,n,n,n placed on "Yahtzee" gives 50.“ n =
Given (n,n,n,n,n)
|> When (placed_on Yahtzee)
|> It should equal 50
|> Verify
[<Scenario>]
let “Given 1,1,1,2,1 placed on "Yahtzee" gives 50.“ () =
Given (1,1,1,2,1)
|> When (placed_on Yahtzee)
|> It should equal 0
|> Verify
The implementation is pretty easy:
type Category =
| Ones
// …
| Yahtzee
let calcValue category roll =
// …
| Yahtzee ->
let a,b,c,d,e = roll
if a = b && a = c && a = d && a = e then 50 else 0
Category 10 – Chance
“Chance: The player gets the sum of all dice, no matter what they read.”
[<Scenario>]
let “Given 1,1,1,2,1 placed on "Chance" gives 6.“ () =
Given (1,1,1,2,1)
|> When (placed_on Chance)
|> It should equal 6
|> Verify
[<Scenario>]
let “Given 1,6,1,2,1 placed on "Chance" gives 11.“ () =
Given (1,6,1,2,1)
|> When (placed_on Chance)
|> It should equal 11
|> Verify
This seems to be the easiest category as we only have to sum the values:
type Category =
| Ones
// …
| Chance
let calcValue category roll =
// …
| Chance -> List.sum list
Conclusion
We used a lot of F#’s sequence combinators, pattern matching and discriminated unions in this kata. I think this shows that F# is very well suited for such a problem and with NaturalSpec we can easily use a TDD/BDD approach.
The complete source code can be found in the NaturalSpec repository.
If you want to know more about a specific part of the kata or NaturalSpec feel free to contact me.
Tags: BDD, Code Quality, F#, Kata, KataYahtzee, NaturalSpec, nunit, TDD