Writing A Word Memory Game In Elm - Part 2: Modeling And Building a Basic Word Memory Game
Tags: elm
Published: 2019-03-22 13:04:28

---
title: Writing A Word Memory Game In Elm - Part 2: Modeling And Building a Basic Word Memory Game
published: true
description: A tutorial for writing a nice word memory game in Elm
tags: elm
series: Writing A Word Memory Game In Elm
cover_image: https://thepracticaldev.s3.amazonaws.com/i/zijfhkxcnplbo93mv7if.png
---

This is part 2 of the series "Writing A Word Memory Game In Elm", find:

* [Part 1: Setting Up an Elm Application With Parcel](https://dev.to/mickeyvip/writing-a-word-memory-game-inelm---part-1-setting-up-an-elm-application-with-parcel-1ppk)
* Part 2: Modeling And Building a Basic Word Memory Game
* [Part 3: Rethinking the Model](https://dev.to/mickeyvip/writing-a-word-memory-game-in-elm-part-3-rethinking-the-model-2ajp)
* [Part 4: Spicing Things Up With Randomness](https://dev.to/mickeyvip/writing-a-word-memory-game-in-elm-part-4-spicing-things-up-with-randomness-58ei)
* [Part 5 - More Randomness And More Game](https://dev.to/mickeyvip/writing-a-word-memory-game-in-elm-part-5-more-randomness-and-more-game-1i37)

---

The game view should show the sentence with missing words, and the missing words as a list below it. The player will be able to choose a word and, if this word is correct, it will be marked in green in the list. For the "easy" level the view will show a dropdown list of available words.

![What we want the game to look like](https://thepracticaldev.s3.amazonaws.com/i/6nvhvvkuo2diqac2m2fu.png)


Let's think about how the `Model` of the game should look like. For modeling a word let's declare a custom type `Word` with variants `SentenceWord`  and `HiddenWord`, each holding a `String` value:

```elm
type Word
    = SentenceWord String
    | HiddenWord String
```

The sentence is a `List` of `Word` which we can alias as `Words`:

```elm
type alias Words =
    List Word
```

To update the correct word in a sentence when the player makes a choice, we can use the index of the word within the sentence. This means that we should update the `Word` type to hold an index with a `String` value by using a `Tuple`:

```elm
type Word
    = SentenceWord ( Int, String )
    | HiddenWord ( Int, String )
```

The `Model` now looks like this:

```elm
type alias Model =
    { sentence : String
    , chosenWords : Words
    , chosenSentence : Words
    }
```

For now we can hardcode the `initialModel`:

```elm
initialModel : Model
initialModel =
    { sentence = "The pen is mightier than the sword"
    , chosenWords =
        [ HiddenWord ( 1, "pen" )
        , HiddenWord ( 6, "sword" )
        , HiddenWord ( 3, "mightier" )
        ]
    , chosenSentence =
        [ SentenceWord ( 0, "The" )
        , HiddenWord ( 1, "" )
        , SentenceWord ( 2, "is" )
        , HiddenWord ( 3, "" )
        , SentenceWord ( 4, "than" )
        , SentenceWord ( 5, "the" )
        , HiddenWord ( 6, "" )
        ]
    }
```

In `chosenSentence`, we start by setting the `String` value of each `HiddenWord` to be empty. These will then be updated with the word the player chooses, and compared with `chosenWords` entries by index. If this `HiddenWord` in `chosenSentence` equals to one of `HiddenWord` in `chosenWords`, the choice is correct. For example:

```elm
chosenWords =
    [ HiddenWord ( 1, "pen" )
    , HiddenWord ( 6, "sword" )
    , HiddenWord ( 3, "mightier" )
    ]
    
chosenSentence =
    [ SentenceWord ( 0, "The" )
    , HiddenWord ( 1, "pen" )        -- this choice is correct
    , SentenceWord ( 2, "is" )
    , HiddenWord ( 3, "sword" )      -- this choice is incorrect, wrong index
    , SentenceWord ( 4, "than" )
    , SentenceWord ( 5, "the" )
    , HiddenWord ( 6, "" )
    ]
```

Let's add a new `viewSentence` function to render the sentence:

```elm
viewSentence : Words -> Words -> Html msg
viewSentence sentence chosenWords =
    div [ class "has-text-centered" ]
        (List.map
            (\sentenceWord ->
                case sentenceWord of
                    SentenceWord ( _, word ) ->
                        span [ class "sentence-word" ] [ text word ]

                    HiddenWord ( index, word ) ->
                        span [ class "hidden-word" ] [ text "-----" ]
            )
            sentence
        )
``` 

It's pretty straightforward: we are rendering `Words` that can be `SentenceWord` for showing a regular word and a `HiddenWord` for showing a word that was chosen. As a first iteration we are just rendering dashes. In the next step we will render a dropdown `<select>`.

In order for the words to have a space between them let's add a `style.css` inside `src` folder:

```css
.hidden-word,
.sentence-word {
    display: inline-block;
    margin-left: 0.5em;
    margin-bottom: 1em;
    line-height: 2em;
}

.hidden-word:first-child,
.sentence-word:first-child {
    margin-left: 0;
}
```
And import it inside our `index.js`:

```javascript
import { Elm } from './src/Main.elm';
import './src/style.css';

Elm.Main.init({
    node: document.getElementById('app')
});
```

Update the main `view` function with a call to `viewSentence`:

```elm
view : Model -> Html msg
view model =
    main_ [ class "section" ]
        [ div [ class "container" ]
            [ viewTitle
            , div [ class "box" ]
                [ p
                    [ class "has-text-centered" ]
                    [ text model.sentence ]
                , viewSentence model.chosenSentence model.chosenWords
                ]
            ]
        ]
```

![Rendering a sentence with hidden words](https://thepracticaldev.s3.amazonaws.com/i/30o7o4j5f5cee6nb28r6.png)

Ellie: [https://ellie-app.com/5CQqN2ptGsha1](https://ellie-app.com/5CQqN2ptGsha1)

# Rendering The DropDown For Hidden Words

Let's add the new `viewHiddenWord` function that will render `<select>` for a `HiddenWord`:

```elm
viewHiddenWord : Word -> List Word -> Html msg
viewHiddenWord hiddenWord chosenWords =
    case hiddenWord of
        HiddenWord ( _, hiddenWordText ) ->
            let
                viewOption : String -> Html msg
                viewOption wordString =
                    option
                        [ value wordString, selected (wordString == hiddenWordText) ]
                        [ text <| String.toLower wordString ]

                wordElement : Word -> Html msg                
                wordElement word =
                    case word of
                        HiddenWord ( _, wordString ) ->
                            viewOption wordString

                        SentenceWord ( _, wordString ) ->
                            viewOption wordString
            in
            div [ class "select" ]
                [ select [ class "hidden-word" ] <|
                    option []
                        [ text "" ]
                        :: List.map wordElement chosenWords
                ]

        _ ->
            text ""
```

The `viewHiddenWord` accepts the current hidden word and a list of all missing words and renders a drop down for a selection.

First thing we do is to pattern match the `Word` type to get our `HiddenWord`, because only for `HiddenWord` should we render the `<select>` element. The `SentenceWord` render is still handled in the main `view` function.

The `viewHiddenWord` function renders a `<select>` element and maps over the list of all missing words. For each one of them it calls `wordElement` function. Here we must pattern match both cases but we render the same `<option>` element with word's text.

Let's call it from `viewSentence`:

```elm
viewSentence : Words -> Words -> Html msg
viewSentence sentence chosenWords =
    div [ class "has-text-centered" ]
        (List.map
            (\sentenceWord ->
                case sentenceWord of
                    SentenceWord ( _, word ) ->
                        span [ class "sentence-word" ] [ text word ]

                    HiddenWord ( index, word ) ->
                        viewHiddenWord sentenceWord chosenWords
            )
            sentence
        )
```

We should see that we can now choose any of the missing words:

![Player can choose a word](https://thepracticaldev.s3.amazonaws.com/i/qesgkn6wg0di8y5q74ti.png)

Ellie: [https://ellie-app.com/5CQyR6WT3zca1](https://ellie-app.com/5CQyR6WT3zca1)

# Adding Interaction

The player can chose a word, but our `Model` is not updated with this change. We can use the `onInput` event on `select` and update the `model` with the current selection. Since we have a `chosenSentence` as a `List` of `Tuple`s that have an `index` - we know which `Word` we need to update.

Let's delete the `NoOp` message type and add a new one that will get an index and a new word `String`:

```elm
type Msg
    = WordChanged Int String
```

Change the `update` function to handle the new `Msg` (don't forget to delete the code related to `NoOp` message):

```elm
update : Msg -> Model -> Model
update msg model =
    case msg of
        WordChanged index wordString ->
            let
                updateWord : Word -> Word
                updateWord word =
                    case word of
                        (HiddenWord ( hiddenIndex, _ )) as hiddenWord ->
                            if hiddenIndex == index then
                                HiddenWord ( index, wordString )

                            else
                                hiddenWord

                        _ ->
                            word

                newSentence : Words
                newSentence =
                    List.map updateWord model.chosenSentence
            in
            { model | chosenSentence = newSentence }
```

We map over all the words in the `chosenSentence` and if the index of the current sentence (`hiddenIndex`) word equals to the index of the changed word (`index`) - we replace the `String` part of the `Tuple` with the new `wordString`.

We are using `as` syntax to have a reference to the whole matched word, which we return if the `index` is not equal to `hiddenIndex`.

If the current word is not a `HiddenWord` - we just return the current word.

Let's update the `viewHiddenWord` function to emit the message from `select` element:

```elm
viewHiddenWord : Word -> List Word -> Html Msg
viewHiddenWord hiddenWord chosenWords =
    case hiddenWord of
        HiddenWord ( hiddenIndex, hiddenWordText ) ->
            let
                viewOption : String -> Html Msg
                viewOption wordString =
                    option
                        [ value wordString, selected (wordString == hiddenWordText) ]
                        [ text <| String.toLower wordString ]

                wordElement : Word -> Html Msg
                wordElement word =
                    case word of
                        HiddenWord ( _, wordString ) ->
                            viewOption wordString

                        SentenceWord ( _, wordString ) ->
                            viewOption wordString
            in
            div [ class "select" ]
                [ select [ class "hidden-word", onInput (WordChanged hiddenIndex) ] <|
                    option []
                        [ text "" ]
                        :: List.map wordElement chosenWords
                ]

        _ ->
            text ""
   
```

The `onInput` expects a function that accepts a `String` as a parameter. The `WordChanged` message accepts an `Int` and a `String`. We pass to `onInput` `(WordChanged hiddenIndex)` that is exactly what it needs. In Elm every function is automatically curried and, therefore, `(WordChanged hiddenIndex)` returns a function that is waiting for a `String`. 

Note that we have changed the `msg` (any type) to `Msg` (our specific message type). We also need to make the same change in `ViewSentence` function since it is now also propagates our new message:

```elm
viewSentence : Words -> Words -> Html Msg
   -- omitted
```

The `Model` has initial missing words as an empty `String`:

![The `Model` has initial hidden words as an empty `String`](https://thepracticaldev.s3.amazonaws.com/i/pyjnafiwj7w5m92nwj3y.png)

The `Model` is updated when a word is chosen in the dropdown:

![The `Model` is updated when a word is chosen in the dropdown](https://thepracticaldev.s3.amazonaws.com/i/2a5bi0x8brf5he3m1o7r.png)

Ellie: [https://ellie-app.com/5CQDxjPhpwpa1](https://ellie-app.com/5CQDxjPhpwpa1)
# Hint Correctness

We have a nice interactive game already, but the player has no indication whether the words they chose are correct.

We can render a list of missing words and hint the player if their chosen word is the correct one.

To render a list of missing words and know if any word is a correct choice, we need a list of missing words and a list of sentence words. If a missing word is a member of sentence words - has the same index and a word string - the word was chosen correctly:

```elm
viewChosenWords : Words -> Words -> Html msg
viewChosenWords chosenWords sentenceWords =
    let
        viewChosenWord : Word -> Html msg
        viewChosenWord chosenWord =
            case chosenWord of
                HiddenWord ( _, wordString ) ->
                    let
                        isCorrectGuess : Bool
                        isCorrectGuess =
                            List.member chosenWord sentenceWords

                        className : String
                        className =
                            if isCorrectGuess then
                                "has-text-success"

                            else
                                "has-text-grey-light"
                    in
                    li []
                        [ span [ class className ]
                            [ text wordString
                            , text " "
                            , span [ class "icon is-small" ]
                                [ i [ class "far fa-check-circle" ] [] ]
                            ]
                        ]

                SentenceWord _ ->
                    text ""
    in
    ul [] (List.map viewChosenWord chosenWords)

```
We are using Font Awesome's icon and Bulma's classes for coloring the words in light grey or green.

Add `viewChosenWords` to the main `view` function to render the list:

```elm
view : Model -> Html Msg
view model =
    main_ [ class "section" ]
        [ div [ class "container" ]
            [ viewTitle
            , div [ class "box" ]
                [ p
                    [ class "has-text-centered" ]
                    [ text model.sentence ]
                , viewSentence model.chosenSentence model.chosenWords
                , viewChosenWords model.chosenWords model.chosenSentence
                ]
            ]
        ]
```

And now every time a player choses the correct word - it is colored in green in the chosen words list:

![Hint for a correct choice](https://thepracticaldev.s3.amazonaws.com/i/6nvhvvkuo2diqac2m2fu.png)

Ellie: [https://ellie-app.com/5CQKJZdYGMKa1](https://ellie-app.com/5CQKJZdYGMKa1)

The game works, but the `Model` can be improved. In the next post we will refactor it to eliminate invalid states. Stay tuned!

The current progress is saved in the repo under a Tag v1.0 https://github.com/mickeyvip/words-memory-game/tree/v1.0.