Yahtzee Scoring Kata in F#
Several years ago I did a Yahtzee kata in Python which was a nice simple problem to help me pick up a few new techniques. I thought I’d give the same thing a try in F# as I’ve not done quite as much F# as I’d like in recent months, and I want to get back into the swing of things ready for this year’s Advent of Code (last year’s videos available here)!
In general, I followed similar approaches to with Python, although it’s easier to work with Lists than Tuples for my dice and test cases in F# since Tuples are not actually enumerable sequences in F#. One distinct advantage F# had was how easy it was to use partial application of functions to build my strategies, but the disadvantage is that unlike Python, there seems to be no easy way to get the name of a function, so I created a simple tuple structure of name and strategy function to make my test output readable.
First of all I needed a few helper functions.
highestRepeated was perhaps the most fiddly to create in F#. This function looks for any dice that are repeated at least
minRepeats times and if there’s more than one, it needs to tell us which the highest value is. So if you rolled
[2;2;2;5;5] then it should return 2 if
minRepeats is 3 and 5 if
minRepeats is 2.
Here’s what I came up with, although I feel there must be a way to simplify it a bit. I made several versions, but all of them were of similar complexity. The fact that
List.max needs at least one element to work doesn’t help
let highestRepeated dice minRepeats = let repeats = dice |> List.countBy id |> List.filter (fun (_,n) -> n >= minRepeats) |> List.map fst match repeats with |  -> 0 | _ -> List.max repeats
ofAKind function uses
highestRepeated to implement the 2/3/4 of a kind scoring strategies, which we can partially partially apply this function to generate (we’ll see that in a minute).
let ofAKind n dice = n * highestRepeated dice n
Next up is
sumOfSingle which you sum all the dice with the specified value. I’m finally getting used to using operators such as equals
(=) as functions in F#.
let sumOfSingle selected dice = dice |> Seq.filter ((=) selected) |> Seq.sum
I also made a helper function to score high and low straights, which is nice and easy since lists can be directly compared for equality so if I pass
target as the list
[1;2;3;4;5] it can compare that directly to the sorted list of dice.
let straight target score dice = if List.sort dice = target then score else 0
And finally I needed to test for Yahtzee itself – all five dice the same value. This was another one that I think could perhaps be made a little more succinct, but here’s what I came up with:
let yahtzee dice = if Seq.length dice = 5 && Seq.length (Seq.distinct dice) = 1 then 50 else 0
Now we have all the pieces to build our list of scoring strategies, which are tuples of a name and a function. Notice how we can partially apply
ofAKind to cut down on declaring additional functions like I did in Python. It means that the F# solution is around a dozen lines shorter than the Python one.
The final piece was the suite of Unit Tests, which were copied over from my Python solution, although F# type inference flagged up that I had a few untested functions, so I added some more test cases:
let testCases = [ ([1;2;3;4;5], 1, Ones) ([1;2;3;4;5], 2, Twos) ([3;2;3;4;3], 9, Threes) ([3;2;3;4;3], 4, Fours) ([5;5;5;4;3], 15, Fives) ([3;2;3;4;3], 0, Sixes) ([1;2;3;4;5], 0, Pair) // no pairs found ([1;5;3;4;5], 10, Pair) // one pair found ([2;2;6;6;4], 12, Pair) // picks highest ([2;3;1;3;3], 6, Pair) // only counts two ([2;2;6;6;6], 18, ThreeOfAKind) ([2;2;4;6;6], 0, ThreeOfAKind) // no threes found ([5;5;5;5;5], 15, ThreeOfAKind) // only counts three ([6;2;6;6;6], 24, FourOfAKind) ([2;6;4;6;6], 0, FourOfAKind) // no fours found ([5;5;5;5;5], 20, FourOfAKind) // only counts four ([1;2;5;4;3], 15, SmallStraight) ([1;2;5;1;3], 0, SmallStraight) ([6;2;5;4;3], 20, LargeStraight) ([1;2;5;1;3], 0, LargeStraight) ([5;5;5;5;5], 50, Yahtzee) ([1;5;5;5;5], 0, Yahtzee) ([1;2;3;4;5], 15, Chance) ]
Now one thing I really need to do is get to grips with one of the F# unit testing frameworks. For this code, which I developed in LinqPad, I just created my own simple test case runner, but I’d be interested to hear recommendations for what I should be using.
let runTest (dice, expected, (name, strategy)) = let score = strategy dice let message = sprintf "testing with %s on %A" name dice (expected = score), message let runAllTests = let results = testCases |> List.map runTest results |> List.iter (fun (s,m) -> printf "%s %s" (if s then "PASS" else "FAIL") m) printfn "ran %d test cases" (List.length testCases) runAllTests
The full source code is available as a GitHub Gist and as always, I welcome any feedback in the comments on how I can improve my code.