In my post about patterns in Elixir I was having a field day with the pattern matching functionality in Elixir.

At the inaugural meet-up for the Copenhagen Elm group we used the Yatzy scoring exercise as a way to get into the language.

I dreeded it a bit.

I knew that my nifty pattern matching tricks from Elixir would not fly past the Elm type system.

But the meet-up organiser Mads told me that he had had some luck getting a lot of the same structure in the Elm program, so I rolled up my sleeves and started hacking.

I will try to take you through the thought process I went through. I think I have found a nice solution, but I will not try to pass the impression that I arrived at it in one go.

Starting point

You can use the elm-yatzy project as a starting point. It has some unit tests for the Yatzy scoring, so you can do some Test Driven Development while learning Elm!

Datatypes

Since Elm has a nice static type system we need to think a little more explicitly about types.

A roll of the dice will be represented as a List Int.

One could go into defining type aliases, but that is not mandatory.

Starting out

The upper : Int -> Int List -> Int function has to calculate what the rolled dice would be worth for the given slot.

This is easy in Elm:

upper n ds =
  filter n ds
      |> List.sum

Where

filter : Int -> List Int -> List Int
filter n ds =
    List.filter (\x -> x==n) ds

In the beginning I did not have filter as a separate function, but I ended up with another function that had to do the exact same thing, so I refactored my code.

And chance is even easier:

chance : List Int -> Int
chance ds =
  ds |> List.sum

Having dealt with that quite fast I ran into my first wall…

Yatzy is special

In Elixir I was able to write a direct pattern for detecting Yatzy - just [x,x,x,x,x] and one is home free - so such luck in Elm :-(

So I tried to take the head of the list and then see if a list consisting of five heads would equal the rolled dice…

yatzy : List Int -> Int
yatzy ds =
    case List.head ds of
        Maybe x ->
            if List.repeat 5 x == ds
                then 50
                else 0
        _ -> 0

While this is relatively straight-forward it just doesn’t feel elegant.

So I pondered a bit.

"What if I could calculate the count of a given number of eyes?" was my next thought.

That could help - with that in hand I could just iterate over all eyes (1 through 6) to see if the count was 5.

But it still felt a bit clunky.

Then it came to me.

Do a bucket sort and remove all empty buckets.

The code for that helper function looks like this:

counts : List Int -> List (Int, Int)
counts ds =
    List.map (\n -> (n, count n ds)) all_dice
        |> List.filter (\(n,c) -> c>0)

count : Int -> List Int -> Int
count n ds =
    filter n ds |> List.length

It uses the count function to count for a give number of eyes, then maps that function over all dice eyes before filtering out the empty buckets.

And now happiness enters the yatzy function!

yatzy : List Int -> Int
yatzy ds =
    case counts ds of
        [(n, 5)] -> 50
        _  -> 0

Now we’re talking!

If counts ds returns a list with just one element where the count is 5 we have rolled a Yatzy.

It’s right there. Yelling at you what it is doing.

Invigourated by this break-through I charged forward with much inspiration…

Hitting another wall

The straights were just coming right out of my fingers, but four of a kind stopped me in my tracks.

While counts ds gave me a nice list, I still had to go through the list to find out the number of eyes with a count of 4, if any.

That did not sit well with my pattern matching addiction.

After opening a new Pellegrino soda with orange (dangerously addictive - don’t try it) the sugar brought a solution to my brain.

If I sorted the list by count then I would be a lot better off. So I created a little helper funtion.

descendingCounts : List (Int, Int) -> List (Int, Int)
descendingCounts raw_counts =
    List.sortWith count_compare raw_counts

count_compare (n1,c1) (n2,c2) =
    case compare c1 c2 of
        GT -> LT
        LT -> GT
        EQ -> EQ

Now four_of_a_kind becomes nice:

fourOfAKind : List Int -> Int
fourOfAKind ds =
    case counts ds of
        [(n,5)] -> n*4
        (n,4)::_ -> n*4
        _ -> 0

Again, the code tells you what your brain is doing instinctively: if you have 5 or 4 of the same die then you have four of a kind.

Three of a kind went down equally easy and there was much rejoicing.

The final twist

One pair is not the most prominent slot in Yatzy. It’s one of those that you fill out when nothing better is on the table.

But it got its revenge on my counts function.

You would have to search through the list of counts to find the highest possible pair.

That’s not pattern matching. Me dislike.

With shame I must admit that another Pellegrino - this time blood orange - was consumed. But not in vain. I got my final insight.

Just sort the die with equal number of occurances descendingly based on the number of eyes as the second comparison.

That only requires a small tweak to the count_compare function:

count_compare (n1,c1) (n2,c2) =
    case compare c1 c2 of
        GT -> LT
        LT -> GT
        EQ -> compare n2 n1

Now one pair is easy to calculate:

onePair : List Int -> Int
onePair ds =
    case counts ds of
        (n,c)::_ -> if c>=2
                        then n*2
                        else 0
        _ -> 0

If the head of the counts ds list has at least 2 as count we have a pair.

Very nice.

With this in place two pairs and full house is equally intuitive:

twoPair : List Int -> Int
twoPair ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c2==2
                                    then 2*(n1+n2)
                                    else 0
        _ -> 0

fullHouse : List Int -> Int
fullHouse ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c1==3 && c2==2
                                    then 3*n1 + 2*n2
                                    else 0
        _ -> 0

twoPair : List Int -> Int
twoPair ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c2==2
                                    then 2*(n1+n2)
                                    else 0
        _ -> 0

fullHouse : List Int -> Int
fullHouse ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c1==3 && c2==2
                                    then 3*n1 + 2*n2
                                    else 0
        _ -> 0

Notice how I can just pattern match on the start of the list. If it has the right structure and values we are in business, otherwise it is 0 points.

Conclusion

The nifty pattern matching from Elixir might not be possible in Elm, but you can actually leverage pattern matching to write some quite natural code.

The key take away is to find a way to summarise your input data into a form that lends itself to the kind of pattern matching you are already doing in your brain.

As you can see it does not come as a divine intervention - at least not for me. I only arrived at the final counts function after a number of small improvements.

When the code is not nice there is probably something wrong with the way you manipulate the data.

Just keep on looking for the nice solutions and have fun with programming - be that in Elm, Elixir or some other interesting language.

Full solution

module Yatzy.Score where



upper : Int -> List Int -> Int
upper n ds =
  filter n ds
      |> List.sum

chance : List Int -> Int
chance ds =
  ds |> List.sum

yatzy : List Int -> Int
yatzy ds =
    case counts ds of
        [(n, 5)] -> 50
        _  -> 0

counts : List Int -> List (Int, Int)
counts ds =
    List.map (\n -> (n, count n ds)) all_dice
        |> List.filter (\(n,c) -> c>0)
        |> descendingCounts

all_dice = [1..6]

filter : Int -> List Int -> List Int
filter n ds =
    List.filter (\x -> x==n) ds

count : Int -> List Int -> Int
count n ds =
    filter n ds |> List.length

descendingCounts : List (Int, Int) -> List (Int, Int)
descendingCounts raw_counts =
    List.sortWith count_compare raw_counts

count_compare (n1,c1) (n2,c2) =
    case compare c1 c2 of
        GT -> LT
        LT -> GT
        EQ -> compare n2 n1

smallStraight : List Int -> Int
smallStraight ds =
    if List.sort ds == [1..5]
        then 15
        else 0

largeStraight : List Int -> Int
largeStraight ds =
    if List.sort ds == [2..6]
        then 20
        else 0

fourOfAKind : List Int -> Int
fourOfAKind ds =
    case counts ds of
        [(n,5)] -> n*4
        (n,4)::_ -> n*4
        _ -> 0


threeOfAKind : List Int -> Int
threeOfAKind ds =
    case counts ds of
        (n,c)::_ -> if c>=3
                        then n*3
                        else 0
        _ -> 0

onePair : List Int -> Int
onePair ds =
    case counts ds of
        (n,c)::_ -> if c>=2
                        then n*2
                        else 0
        _ -> 0

twoPair : List Int -> Int
twoPair ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c2==2
                                    then 2*(n1+n2)
                                    else 0
        _ -> 0

fullHouse : List Int -> Int
fullHouse ds =
    case counts ds of
        (n1,c1)::(n2,c2)::_ -> if c1==3 && c2==2
                                    then 3*n1 + 2*n2
                                    else 0
        _ -> 0