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 (|SingleThrow|) (target: Die) (dice: Die list): int =
dice
|> List.filter (fun die -> die = target)
|> List.length
let private (|FullHouseThrow|_|) (dice: Die list): unit option =
match List.countBy id dice |> List.sortBy snd with
| [(_, 2); (_, 3)] -> Some ()
| _ -> None
let private (|FourOfAKindThrow|_|) (dice: Die list): Die option =
match List.countBy id dice |> List.sortBy snd with
| [(number, 5)] | [_; (number, 4)] -> Some number
| _ -> None
let private (|LittleStraightThrow|_|) (dice: Die list): unit option =
match List.sort dice with
| [Die.One; Die.Two; Die.Three; Die.Four; Die.Five] -> Some ()
| _ -> None
let private (|BigStraightThrow|_|) (dice: Die list): unit option =
match List.sort dice with
| [Die.Two; Die.Three; Die.Four; Die.Five; Die.Six] -> Some ()
| _ -> None
let private (|YachtThrow|_|) (dice: Die list): unit option =
match List.distinct dice with
| [_] -> Some ()
| _ -> None
let score (category: Category) (dice: Die list): int =
match category, dice with
| Ones, SingleThrow Die.One count -> count * 1
| Twos, SingleThrow Die.Two count -> count * 2
| Threes, SingleThrow Die.Three count -> count * 3
| Fours, SingleThrow Die.Four count -> count * 4
| Fives, SingleThrow Die.Five count -> count * 5
| Sixes, SingleThrow Die.Six count -> count * 6
| FullHouse, FullHouseThrow -> List.sumBy dieScore dice
| FourOfAKind, FourOfAKindThrow die -> dieScore die * 4
| LittleStraight, LittleStraightThrow -> 30
| BigStraight, BigStraightThrow -> 30
| Yacht, YachtThrow -> 50
| Choice, _ -> List.sumBy dieScore dice
| _, _ -> 0
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.
Active patterns
Active patterns are used in pattern matching and can be used to categorize input and/or extract data from input.
There are two types of active patterns:
- Regular active patterns: these patterns will match any input
- Partial active patterns: these pattern will match some inputs, but not all
Scoring categories
In this approach, we'll define active patterns for the different categories.
The idea is that if we're try to match category named A
, then we have a corresponding active pattern named AThrow
that will check if the dice match the category.
We'll use a combination of regular and partial active patterns.
Note that the active patterns do not calculate scores, they're just there to help match input data. This better separates responsiblities and opens up the active patterns for usage elsewhere.
These functions will then later on be called in the score
function, like this:
let score (category: Category) (dice: Die list): int =
match category, dice with
| Yacht, YachtThrow -> 50
You can see that we're using regular pattern matching on the category
parameter, which is a discriminated union.
However, we're also pattern matching on the dice
using a custom YachtThrow
(active) pattern.
Let's start defining these active patterns!
Single score
To score a single die, we need to:
- Find the number of dice that match the target die
- Multiply the number of matching dice with the die value
With the above steps, the output is also correct when the target die could not be found (zero times any dice value is zero). Therefore, our active pattern can be a regular, non-partial active pattern as it will always match the input.
Score ones
Let's start by scoring the six die (Die.Six
).
Our active pattern will take the thing we're matching on (the dice) as its sole parameter.
It wil return an int
representing the number of six dice found, as the score
function will require that information to calculate the score:
let private (|SixesThrow|) (dice: Die list): int =
dice
|> List.filter (fun die -> die = Die.Six)
|> List.length
Active patterns functions have their name specified between (|
and |)
.
This name will be used it in pattern matching, so choose the name accordingly.
The implementation is fairly straightforward.
We first filter the dice matching the six dice by using List.filter
.
Then, we count those dice via List.length
, which is subsequently returned.
A different way to read this is: to use the SixesThrow
active pattern, one has to pass it a list of dice and you'll get back their count.
We can now use this pattern in our score
function:
match category, dice with
| Sixes, SixesThrow count -> count * 6
This is saying: if the category is Sixes
and the dice match the SixesThrow
pattern (which they will always do), multiply the count (as returned by the SixesThrow
pattern) by six to determine score.
We could continue defining similar patterns for the other five dice, but we can do something much nicer: parameterizing our active pattern.
Converting to a parameterized active pattern
Active patterns, like regular functions, can have parameters besides the value that is being matched on. The only constraint is that the value to match on must be the last parameter.
To make our SixesThrow
active pattern more generic, let's rename it to SingleThrow
(as in: single dice throw) and add a parameter which is the target die:
let private (|SingleThrow|) (target: Die) (dice: Die list): int =
dice
|> List.filter (fun die -> die = target)
|> List.length
The only thing we then need to change is to replace Die.Six
with our target
parameter in the List.filter
call's lambda.
We can do use this pattern to score the six single dice categories:
match category, dice with
| Ones, SingleThrow Die.One count -> count * 1
| Twos, SingleThrow Die.Two count -> count * 2
| Threes, SingleThrow Die.Three count -> count * 3
| Fours, SingleThrow Die.Four count -> count * 4
| Fives, SingleThrow Die.Five count -> count * 5
| Sixes, SingleThrow Die.Six count -> count * 6
Full house score
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)]
As a full house is the sum of its dice, we don't have to return any value from our active pattern so we'll just return unit
(which is F#'s way of representing the absence of a value).
let private (|FullHouseThrow|_|) (dice: Die list): unit option =
match List.countBy id dice with
| [(_, 2); (_, 3)] | [(_, 3); (_, 2)] -> Some ()
| _ -> None
We have to define the FullHouseThrow
active pattern as a partial active pattern (indicated by the |_|
suffix), as not all dice are a full house.
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 (|FullHouseThrow|_|) (dice: Die list): unit option =
match List.countBy id dice |> List.sortBy snd with
| [(_, 2); (_, 3)] -> Some ()
| _ -> None
Scoring
Let's use this pattern in our score
function, where the score is just summing the dice scores via List.sumBy
and our dieScore
function:
match category, dice with
| FullHouse, FullHouseThrow -> List.sumBy dieScore dice
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)
We can use the same List.countBy
and pattern matching strategy
let private (|FourOfAKindThrow|_|) (dice: Die list): Die option =
match List.countBy id dice with
| [(number, 5)] | [(number, 4); _] | [_; (number, 4)] -> Some number
| _ -> None
As scoring a four of a kind throw requires multipying the die occuring four times, we'll return that die from our function.
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 (|FourOfAKindThrow|_|) (dice: Die list): Die option =
match List.countBy id dice |> List.sortBy snd with
| [(number, 5)] | [_; (number, 4)] -> Some number
| _ -> None
Scoring
For the scoring of a four of kind throw, we'll capture the die in our active pattern, converting it to an int
via dieScore
and then multiply by four:
match category, dice with
| FourOfAKind, FourOfAKindThrow die -> dieScore die * 4
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 (|LittleStraightThrow|_|) (dice: Die list): unit option =
match List.sort dice with
| [Die.One; Die.Two; Die.Three; Die.Four; Die.Five] -> Some ()
| _ -> None
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.
The pattern is defined as a partial active pattern (not all throws are little straights) and returns unit
, as a little straight's score is always the same.
Scoring
Scoring little straights is very straightforward (pun intended):
match category, dice with
| LittleStraight, LittleStraightThrow -> 30
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 (|BigStraightThrow|_|) (dice: Die list): unit option =
match List.sort dice with
| [Die.Two; Die.Three; Die.Four; Die.Five; Die.Six] -> Some ()
| _ -> None
Once again, we need List.sort
to fix the ordering.
Like the little straight pattern, we're using a partial active pattern (not all throws are big straights) and return unit
, as a big straight's score is always the same.
Scoring
We can score big straights as follows:
match category, dice with
| BigStraight, BigStraightThrow -> 30
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 (|YachtThrow|_|) (dice: Die list): unit option =
match List.distinct dice with
| [_] -> Some ()
| _ -> None
Alternatively, we could have counted the number of unique dice and checked if that was equal to one in an if
expression:
let private (|YachtThrow|_|) (dice: Die list): unit option =
if List.distinct dice |> List.length = 1 then Some () else None
Scoring
We can score yachts as follows:
match category, dice with
| Yacht, YachtThrow -> 50
Choice score
Scoring the choice category is simple: we just need to sum all the dice.
We therefore don't need to define an active pattern and can just add the following to the score
function's pattern matching:
| Choice, _ -> List.sumBy dieScore dice
We're matching the dice using the wildcard pattern (_
), which will match any input.
Handling non-matching dice
So far, we've secretly ignored something quite important: what to do if the dice don't match a category's active pattern!
The solution for this is simple though.
As dice that don't match the pattern should be scored as zero, we can just add two wildcards at the end of our score
function and return zero:
| _, _ -> 0
Putting it all together
Now that we support all categories and handle non-matching dice, let's see what the score
function looks like:
let score (category: Category) (dice: Die list): int =
match category, dice with
| Ones, SingleThrow Die.One count -> count * 1
| Twos, SingleThrow Die.Two count -> count * 2
| Threes, SingleThrow Die.Three count -> count * 3
| Fours, SingleThrow Die.Four count -> count * 4
| Fives, SingleThrow Die.Five count -> count * 5
| Sixes, SingleThrow Die.Six count -> count * 6
| FullHouse, FullHouseThrow -> List.sumBy dieScore dice
| FourOfAKind, FourOfAKindThrow die -> dieScore die * 4
| LittleStraight, LittleStraightThrow -> 30
| BigStraight, BigStraightThrow -> 30
| Yacht, YachtThrow -> 50
| Choice, _ -> List.sumBy dieScore dice
| _, _ -> 0
Quite nice!