Building a memory game in Elm. Step by step, from scratch.

This is the classic Memory Game that I always use to approach a new frontend framework/language. It is based on a super old implementation that somebody made years ago – this little game is my point of reference to understand how things work.

What are we going to build

Here’s a working demo of the finished project:

The whole thing will be about 200LOC so it should be pretty easy to follow. I’m not going to touch on what Elm is or why you might want to use it, there’s enough of that around for you to read already.

Step 1 – Draw the grid

We want to get some static markup going, so let’s begin with something very simple.

module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)


main : Program Never Model Msg
main =
    Html.program
        { init = createModel
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }


type alias Model =
    {}


type Msg
    = NoOp


createModel : ( Model, Cmd Msg )
createModel =
    ( {}, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( model, Cmd.none )


view : Model -> Html Msg
view model =
    h1 [] [ text "There you go!" ]

There’s not much to see here yet, just a giant There you go! on our screen (You can check it out here). Let’s change that.

We’ll begin with the createCard function, which will be the single div showing the card picture.

createCard : Html Msg
createCard =
    div [ class "container" ]
        -- try changing ("flipped", False) into ("flipped", True)
        [ div [ classList [ ( "card", True ), ( "flipped", True ) ] ]
            [ div [ class "card-back" ] []
            , div [ class "front card-dinosaur" ] []
            ]
        ]

There’s a bunch of hardcoded stuff in here, but it will be enough for now. Specifically, we’ll want to replace the dinosaur image and we’ll want the flipped class to update depending on the state of our card.

For now, let’s just place a few cards on the page, by changing our view:

view : Model -> Html Msg
view model =
    div [ class "wrapper" ]
        [ createCard
        , createCard
        , createCard
        , createCard
        , createCard
        ]

Our grid is starting to take shape.

Step 2 – Show some cards

Time to show some real cards.

First of all, we need to introduce a couple of types:

type alias Card =
    { id : String
    , group : Group
    , flipped : Bool
    }


type Group
    = A
    | B

The Card type is quite simple, we need to have a group property to differentiate between the two different copies of the same card in the deck. Let’s also list all the available cards:

cards : List String
cards =
    [ "dinosaur"
    , "8-ball"
    , "baked-potato"
    , "kronos"
    , "rocket"
    , "skinny-unicorn"
    , "that-guy"
    , "zeppelin"
    ]

We’ll define a function initCard that returns a Card object:

initCard : Group -> String -> Card
initCard group name =
    { id = name
    , group = group
    , flipped = False
    }

It can be used like this: initCard A "dinosaur" and that’s exactly what we’re going to do in order to create our deck:

deck : Deck
deck =
    let
        groupA =
            List.map (initCard A) cards

        groupB =
            List.map (initCard B) cards
    in
        List.concat [ groupA, groupB ]

deck will hold two copies of all the cards we defined. You might notice this doesn’t take shuffling into account, we’ll look into it later.

Cool, let’s update the Model so that we can show actual cards in our view.

type Model
    = Playing Deck

And the createModel function.

createModel : ( Model, Cmd Msg )
createModel =
    -- Our model now constists of the unshuffled deck
    ( Playing deck, Cmd.none )

Our game will only have a generic Playing state for now, we’ll add more later on.

And now to the view layer. createCard, as it is, is pretty useless because we can’t say what’s the card we want to create, so let’s add an argument and update the function accordingly.

cardClass : Card -> String
cardClass card =
    "card-" ++ card.id


createCard : Card -> Html Msg
createCard card =
    div [ class "container" ]
        -- try changing ("flipped", False) into ("flipped", True)
        [ div [ classList [ ( "card", True ), ( "flipped", False ) ] ]
            [ div [ class "card-back" ] []
            , div [ class ("front " ++ cardClass card) ] []
            ]
        ]

We introduced cardClass to get the proper CSS class. You can see that the front image is now dynamic, and this also shows how you can concatenate strings in Elm (which are just aliases for List Char – that’s why it works!)

We can get rid of the hardcoded cards that we have and properly map through our deck, by changing the view.

view : Model -> Html Msg
view model =
    case model of
        Playing deck ->
            div [ class "wrapper" ] (List.map createCard deck)

Notice we had to pattern match on the model. It looks dumb with a single case but we’ll add more. Now change ("flipped", False) to ("flipped", True) in createCard and look at all the beautiful card pictures (do it yourself below, just edit the code and run it! It’s on line 110).

Step 3 – Introducing Messages and Commands

You might already be familiar with the concept of Messages in Elm, but I feel like Commands are the truly beautiful thing about TEA (which took me a while to realize is an achronym for “The Elm Architecture”).

Commands are our way to describe side effects – impure, nasty stuff that our programs need to do at some point, but undermine the purity of our code. With Commands, you can describe (this is very important) what you want the platform (or runtime) to do, and then tell it to send back the results through a Message.

You might be wondering what an example of an effect could be: HTTP requests, saving to localStorage, reading the time. Ultimately, even rendering our Elm app and having it inserted into the DOM is a side effect, but managed by the Elm runtime. So, what kind of side effect is involved here? At the end of the day, this program is so simple that it could do without any effects. Well, we need to shuffle the deck remember? That implies we need some kind of random number generator, which is impure.

We can return commands along side the updated model in the udpate function (that explains why the returned type is a tuple in the form of (Model, Cmd Msg)). But we can also hand over Commands to the runtime when we first create our model in our main function. So let’s get down to do some random number generation.

First of all, import the Random module.

import Random

You can read about how this module works in the docs but the gist of it is that you first need to create an appropriate Generator, depending on the type you’re dealing with, and then use Random.generate to create the Command that will be handed off to the runtime. We need to generate random numbers, so we can stick with Generator Int but it would be cool if we could get back a list of random numbers instead of a single one, and for that we can use Random.list. So we can create a function randomList which gets an argument len and returns a Command that will generate len random numbers from 0 to 100.

randomList : (List Int -> Msg) -> Int -> Cmd Msg
randomList msg len =
    Random.int 0 100
        |> Random.list len
        |> Random.generate msg

As we said, when the runtime executes our command, it will pass back the results in the update function using a Message of our choice. We can extend our Msg type to include a Shuffle (List Int) value.

type Msg
    = NoOp
    | Shuffle (List Int)

We can now return a Command in our createModel

createModel : ( Model, Cmd Msg )
createModel =
    let
        model =
            Playing deck

        cmd =
            randomList Shuffle (List.length deck)
    in
        ( model, cmd )

That’s pretty cool, so we can now use the random list of numbers to shuffle our deck. Let’s take care of the update function for a second, because right now it’s pretty static and dumb. We want it to react to the different actions that will be coming through – right now we’re interested in Shuffle.

-- a ! b is equivalent to (a, Cmd.batch b)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            model ! []

        Shuffle xs ->
            let
                newDeck =
                    shuffleDeck deck xs
            in
                Playing newDeck ! []

We need to implement shuffleDeck

shuffleDeck : Deck -> List comparable -> Deck
shuffleDeck deck xs =
    List.map2 (,) deck xs
        |> List.sortBy Tuple.second
        |> List.unzip
        |> Tuple.first

Here’s what it does:

  1. It creates tuples of this form (Card, Int) where the second element is the random number
  2. Then the list gets sorted by the second element, effectively shuffling it.
  3. We use unzip to transform our list of tuples to the original form with two lists ([Card], [Int])
  4. We’re no longer interested in the random numbers (second list) so we only return the first element.

If we use the ("flipped", False) trick again on createCard, you can see that by refreshing a few times our cards change order, yay! Try it for yourself!

Step 4 – Make cards flippable

Looking back at our Model we can see there’s a flipped property that indicates whether the card is facing up or down. We want to switch this when the user clicks on a card.

That’s easy enough to do, but we need to add the Flip tag in our Msg to indicate the user has clicked on a card.

type Msg
    = NoOp
    | Shuffle (List Int)
    | Flip Card

Great, now let’s take this action into account in the update function. We’ll need to implement an helper function (let’s call it flip) that, given our desired flipped state and two cards, takes care of updating the flipped property if the cards match.

flip : Bool -> Card -> Card -> Card
flip isFlipped a b =
    if (a.id == b.id) && (a.group == b.group) then
        { b | flipped = isFlipped }
    else
        b

This might seem pointless but it’s actually very much needed when extending the update function, because there is no mutable stuff (luckily!) so we have to return a new copy of the deck and, in order to do that, it’s quite useful to have a flip function that only updates the card we want to update. Let’s see how we might use it:

-- update function
Flip card ->
    case model of
        Playing deck ->
            let
                newDeck =
                    List.map (flip True card) deck
            in
                Playing newDeck ! []

We need to import onClick so that users can click on cards:

import Html.Events exposing (onClick)

At this point, we can proceed updating the createCard function so that it tags click events with Flip.

--- createCard
[ div [ classList [ ( "card", True ), ( "flipped", card.flipped ) ], onClick (Flip card) ]

You’ll now be able to flip cards back and forth without any constraints. In the next step we’re going to implement a proper game logic when Flip is dispatched, so that cards can be flipped only when the game allows it and matched cards remain face up.

Step 5 – Game logic

So far, our model consisted of a single state Playing Deck. We’ll add two more: Guessing Deck Card and MatchCard Deck Card Card. It’s intuitive to see how they might be used, but just to make sure we’re on the same page:

  • Our game will be in the state Guessing Deck Card when a single card is flipped and another one awaits to be flipped and (eventually) matched.
  • The state MatchCard Deck Card Card will represent the game state where two cards are flipped simultaneously, whether they match or not (the check will be made at the next Flip, otherwise you wouldn’t be able to see the card you just flipped).
type Model
    = Playing Deck
    | Guessing Deck Card
    | MatchCard Deck Card Card

Cool, now we have to decide how to update our model whenever a Flip action comes through:

  1. If the game is in the Playing state, it means we have to flip the first card. This is easy, we’ll just transition to the Guessing state.
  2. If the game is in the Guessing state we can transition to the MatchCard state. This is where we will check if the game is over later on.
  3. The MatchCard state is the most interesting. We have two cards that the user flipped, and we have to decide if they match and can stay face up or if they should be turned face down (in which case, the currentFlip action does not have any impact on the game, it just resets the model to a Playing state).

Let’s try to implement this, it is definitely the trickiest part of the game, but it’s not that difficult, maybe just a little bit overwhelming if you’re not familiar with how to reason in FP yet.

First, we’ll define checkIfCorrect:

checkIfCorrect : Card -> Model -> ( Model, Cmd Msg )
checkIfCorrect card model =
    case model of
        Playing deck ->
            let
                newDeck =
                    List.map (flip True card) deck
            in
                Guessing newDeck card ! []

        Guessing deck guess ->
            let
                newDeck =
                    List.map (flip True card) deck

                newModel =
                    MatchCard newDeck guess card
            in
                newModel ! []

        MatchCard deck guess1 guess2 ->
            if guess1.id == guess2.id then
                {-
                   user has guessed correctly!
                   keep both cards flipped and then run update
                   again to flip the new card that has been just clicked
                -}
                update (Flip card) (Playing deck)
            else
                -- flip the two cards face down because they don't match
                let
                    flipGuess =
                        flip False guess1 >> flip False guess2

                    newDeck =
                        List.map flipGuess deck
                in
                    Playing newDeck ! []

And then we’ll change the Flip branch in the update function:

Flip card ->
    if card.flipped then
        -- if a user clicks on an image that's flipped already
        -- then don't do anything
        model ! []
    else
        checkIfCorrect card model

This should all make sense, but I want to go over a couple of things that might not be immediately clear.

First, in order to change the flip status of two cards, we define flipGuess using function composition (spefically, the >> operator). Another way of writing flipGuess would be:

flipGuess card = flip False guess (flip False current card)

It is just easier to write functions as a result of composition, once you understand how that works.

Second, we can see that update is calling itself recursively when the two cards match. This is so we don’t need to repeat the same logic we already have in the Playing branch. update is just a function after all, there’s nothing magical behind it and we can use it as any other function!

You can play with what we have here. You’ll see that the game works as expected, except that it never terminates. We need to add one last game state so that we can detect when the player wins and another action to reset the game and start over.

Step 6 – Game over and final touches

All right, we’re pretty much done!

The only thing left is to detect when the player has won the game, and display a message to play again. Let’s update our Model and add a new GameOver state.

type Model
    = Playing Deck
    | Guessing Deck Card
    | MatchCard Deck Card Card
    | GameOver Deck

Now we have to change our update function to transition in the GameOver state when appropriate. This is done in the Guessing branch, where we already have a card flipped. When we flip the second card, and all cards are flipped, it means the game is over!

Guessing deck guess ->
    let
        newDeck =
            List.map (flip True card) deck

        -- when all cards are flipped, the game is over
        isOver =
            List.all .flipped newDeck

        newModel =
            if isOver then
                GameOver newDeck
            else
                MatchCard newDeck guess card
    in
        newModel ! []

We’ll have to deal with the GameOver branch in the update function as well. This doesn’t make much sense because a Flip action shouldn’t come through when the game is over. Still, it’s good that the compiler forces us to take care of this as well, and we’ll just return the current model as is:

GameOver deck ->
    GameOver deck ! []

Let’s also create a playAgainOverlay which will be shown when the game finishes.

playAgainOverlay : Html Msg
playAgainOverlay =
    div [ class "congrats" ]
        [ p [] [ text "Yay! You win!" ]
        , text "Do you want to "
        , span [ onClick Reset ] [ text "play again?" ]
        ]

We’re adding a new Action to our Messages – Reset – indicating we want to clear the current game and start fresh. Let’s add it to the Msg type:

type Msg
    = NoOp
    | Reset -- add Reset
    | Shuffle (List Int)
    | Flip Card

And the update function:

Reset ->
    createModel

Our addition to update is extremely straightforward because we already have createModel laying around and it’s exactly what we need. All that is left is to change view to show the overlay when we’re in the GameOver state.

wrapper : Deck -> Html Msg -> Html Msg
wrapper deck overlay =
    div [ class "wrapper" ]
        [ div [] (List.map createCard deck)
        , overlay
        ]


game : Deck -> Html Msg
game deck =
    wrapper deck (text "")


view : Model -> Html Msg
view model =
    case model of
        Playing deck ->
            game deck

        Guessing deck _ ->
            game deck

        MatchCard deck _ _ ->
            game deck

        GameOver deck ->
            wrapper deck playAgainOverlay

We’re changing the markup a bit, but the essence remains the same. The game function will just show some empty text in place of the overlay when the game is still running.

This is it! There’s other stuff that you might want to add as an exercise, such as counting the moves the player has made and stop the game if it’s taking them too long (in terms of time and or moves).

Hopefully this has been helpful, the full source code is available on runelm.io and you can play with it here directly in your browser!

If you enjoyed this article, do let me know and share it along! You can follow me on Twitter if this kind of things interest you.