module Yacht
type Category =
| Ones
| Twos
| Threes
| Fours
| Fives
| Sixes
| FullHouse
| FourOfAKind
| LittleStraight
| BigStraight
| Choice
| Yacht
type Die =
| One
| Two
| Three
| Four
| Five
| Six
let private dieScore (die: Die): int =
match die with
| One -> 1
| Two -> 2
| Three -> 3
| Four -> 4
| Five -> 5
| Six -> 6
let private singleScore (target: Die) (dice: Die list): int =
dice
|> List.filter (fun die -> die = target)
|> List.sumBy dieScore
let private fullHouseScore (dice: Die list): int =
match List.countBy id dice |> List.sortBy snd with
| [(_, 2); (_, 3)] -> List.sumBy dieScore dice
| _ -> 0
let private fourOfAKindScore (dice: Die list): int =
match List.countBy id dice |> List.sortBy snd with
| [(number, 5)] | [_; (number, 4)] -> dieScore number * 4
| _ -> 0
let private littleStraightScore (dice: Die list): int =
match List.sort dice with
| [Die.One; Die.Two; Die.Three; Die.Four; Die.Five] -> 30
| _ -> 0
let private bigStraightScore (dice: Die list): int =
match List.sort dice with
| [Die.Two; Die.Three; Die.Four; Die.Five; Die.Six] -> 30
| _ -> 0
let private yachtScore (dice: Die list): int =
match List.distinct dice with
| [_] -> 50
| _ -> 0
let private choiceScore (dice: Die list): int = List.sumBy dieScore dice
let score (category: Category) (dice: Die list): int =
match category with
| Ones -> singleScore Die.One dice
| Twos -> singleScore Die.Two dice
| Threes -> singleScore Die.Three dice
| Fours -> singleScore Die.Four dice
| Fives -> singleScore Die.Five dice
| Sixes -> singleScore Die.Six dice
| FullHouse -> fullHouseScore dice
| FourOfAKind -> fourOfAKindScore dice
| LittleStraight -> littleStraightScore dice
| BigStraight -> bigStraightScore dice
| Yacht -> yachtScore dice
| Choice -> choiceScore dice
This approach combines a number of functions from the List
module with some pattern matching to score the dice.
Scoring dice
A Die
is defined by a discriminated union.
We need some way to convert its individual values to scores (e.g. Three
should equal 3
).
One way to do this is by converting the discriminated union to an enum type:
type Die =
| One = 1
| Two = 2
| Three = 3
| Four = 4
| Five = 5
| Six = 6
While this may look appealing, it is actually not recommend. As explained in this discriminated union vs enum types article, it is possible to construct an enum value that doesn't match any of the predefined values. For that reason, we'll stick with the discriminated union.
We'll support converting dice to scores via a function that uses some basic pattern matching:
let private dieScore (die: Die): int =
match die with
| One -> 1
| Two -> 2
| Three -> 3
| Four -> 4
| Five -> 5
| Six -> 6
Another option would have been to add a member to the discriminated union:
type Die =
| One
| Two
| Three
| Four
| Five
| Six
member this.Score: int =
match this with
| One -> 1
| Two -> 2
| Three -> 3
| Four -> 4
| Five -> 5
| Six -> 6
We've chosen not to do this, as members are more awkward to use in higher-order functions, which we rely on a lot in this approach.
Scoring categories
In our approach, we'll have a separate function to score each category.
These functions will then later on be called in the score
function, but first, let's go through the scoring functions one by one.
Single score
To score a single dice, we need to:
- Find the dice that match the target die
- Sum the matching dice
We do this by first using List.filter
to filter the dice matching the target die (which is supplied as a parameter).
Then, we sum those dice via List.sumBy
, passing the dieScore
function to convert the Die
values to int
values (allowing them to be "summed"):
let private singleScore (target: Die) (dice: Die list): int =
dice
|> List.filter (fun die -> die = target)
|> List.sumBy dieScore
Full house score
let private fullHouseScore (dice: Die list): int =
match List.countBy id dice with
| [(_, 2); (_, 3)] | [(_, 3); (_, 2)] -> List.sumBy dieScore dice
| _ -> 0
A four of a kind score contains one dice at least four times.
We can use List.countBy
to return a list of pairs where the first value is the unique value and the second value is the number times it occurred in the list.
Then we pattern match the result of the List.countBy
call with the two possible full house patterns:
- The dice contain two numbers, and the first number occurs twice and the second number thrice times:
[(_, 2); (_, 3)]
- The dice contain two numbers, and the first number occurs thrice and the second number twice times:
[(_, 3); (_, 2)]
let private fullHouseScore (dice: Die list): int =
match List.countBy id dice with
| [(_, 2); (_, 3)] | [(_, 3); (_, 2)] -> List.sumBy dieScore dice
| _ -> 0
Simplifying
We can simplify things a bit by sorting the results, ordering by the second value (the count) using List.sortBy
and snd
(which selects the second value).
This allows us to merge the second and third pattern:
let private fullHouseScore (dice: Die list): int =
match List.countBy id dice |> List.sortBy snd with
| [(_, 2); (_, 3)] -> List.sumBy dieScore dice
| _ -> 0
Four of a kind score
A four of a kind score contains one dice at least four times. We'll use the same strategy we just used for a full house, but this time looking for the following count patterns:
- The dice contain just one number and it occurs five times:
[(number, 5)]
- The dice contain two numbers, and the first number occurs four times:
[(number, 4); _]
- The dice contain two numbers, and the second number occurs four times:
[_; (number, 4)
let private fourOfAKindScore (dice: Die list): int =
match List.countBy id dice with
| [(number, 5)] | [(number, 4); _] | [_; (number, 4)] -> dieScore number * 4
| _ -> 0
Simplifying
Once again, we can simplify things a bit by sorting the results, ordering by the second value (the count) using List.sortBy
and snd
(which selects the second value).
This allows us to merge the second and third pattern:
let private fourOfAKindScore (dice: Die list): int =
match List.countBy id dice |> List.sortBy snd with
| [(number, 5)] | [_; (number, 4)] -> dieScore number * 4
| _ -> 0
Little straight score
A little straight contains the dice with face values 1, 2, 3, 4 and 5. This can be directly translated into pattern matching:
let private littleStraightScore (dice: Die list): int =
match List.sort dice with
| [Die.One; Die.Two; Die.Three; Die.Four; Die.Five] -> 30
| _ -> 0
Note that we do need to call List.sort
first, as the dice aren't necessarily in order and pattern matching is sensitive to the ordering.
Big straight score
A big straight contains the dice with face values 2, 3, 4, 5 and 6. This can be directly translated into pattern matching:
let private bigStraightScore (dice: Die list): int =
match List.sort dice with
| [Die.Two; Die.Three; Die.Four; Die.Five; Die.Six] -> 30
| _ -> 0
Once again, we need List.sort
to fix the ordering.
Yacht score
For the yacht category, we need to determine if all dice have the same face.
We can check this by using List.distinct
to first remove any duplicates, and then use pattern matching to check if there is only one unique die:
let private yachtScore (dice: Die list): int =
match List.distinct dice with
| [_] -> 50
| _ -> 0
Alternatively, we could have counted the number of unique dice and checked if that was equal to one in an if
expression:
let private yachtScore (dice: Die list): int =
if List.distinct dice |> List.length = 1 then 50 else 0
Choice score
Scoring the choice category is simple: we just need to sum all the dice.
We can do this by once again using List.sumBy
and the dieScore
function (but this time we don't apply any filtering):
let private choiceScore (dice: Die list): int = List.sumBy dieScore dice
Putting it all together
Let's put good use of these category scoring functions in our score
function.
This function takes two parameters (the score category and a list of dice) and returns the score as an int
:
let score (category: Category) (dice: Die list): int
Within this function, we'll pattern match on the category
parameter and call the appropriate category scoring function:
match category with
| Ones -> singleScore Die.One dice
| Twos -> singleScore Die.Two dice
| Threes -> singleScore Die.Three dice
| Fours -> singleScore Die.Four dice
| Fives -> singleScore Die.Five dice
| Sixes -> singleScore Die.Six dice
| FullHouse -> fullHouseScore dice
| FourOfAKind -> fourOfAKindScore dice
| LittleStraight -> littleStraightScore dice
| BigStraight -> bigStraightScore dice
| Yacht -> yachtScore dice
| Choice -> choiceScore dice
Nothing interesting really, except for the fact that the categories for individual dice (Ones
.. Sixes
) get passed the the dice to score for as an additional argument.
And that's it! Our implementation now passes all the tests.