This is part 4 of the series "Writing A Word Memory Game In Elm", find:
- Part 1: Setting Up an Elm Application With Parcel
- Part 2: Modeling And Building a Basic Word Memory Game
- Part 3: Rethinking the Model
- Part 4: Spicing Things Up With Randomness
- Part 5 - More Randomness And More Game
Choosing the same words every time is pretty boring. Let's make our game choose random words each time.
Randomness is a side effect and does not fit into functional programming concepts, since generating a random number returns something different each time.
In order to make our application handle side effects we need to upgrade it from Browser.sandbox to Browser.element which will add commands and subscriptions for interacting with the outside world.
Let's change our main definition to use Browser.element:
main : Program () Model Msg
main =
Browser.element
{ view = view
, init = init
, update = update
, subscriptions = subscriptions
}
There are now several changes we need to make in order for our current application to compile:
-
initexpects a function instead ofModel, so we'll add theinitfunction -
subscriptionsexpects a function that we'll add -
updatereturns aTuple(Model, Cmd Msg)instead of aModel
Let's follow the compiler and fix the errors.
We'll start by adding the subscriptions function. We actually will not need subscriptions for now, so we can return Sub.none, meaning no subscriptions:
---- SUBSCRIPTIONS ----
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
Now update - let's change the signature to
update : Msg -> Model -> ( Model, Cmd Msg )
and return the expected tuple (last line):
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
WordChanged index wordString ->
let
updateWord : Int -> Word -> Word
updateWord wordIndex word =
case word of
HiddenWrd hiddenWord ->
if wordIndex == index then
HiddenWrd
{ hiddenWord
| playerChoice = PlayerChoice wordString
}
else
word
_ ->
word
newSentence : Words
newSentence =
List.indexedMap updateWord model.sentence
in
( { model | sentence = newSentence }, Cmd.none )
The last one is the init function. It takes flags which we will not be using and can just ignore by declaring the first argument as a unit type (). It returns a Tuple (Model, Cmd Msg) similar to update. We will use our initialModel and Cmd.none meaning no commands to run:
init : () -> ( Model, Cmd Msg )
init _ =
( initialModel, Cmd.none )
This should make the application compile and the game should function as before.
The current progress is saved in the repo under a Tag v3.0:
https://github.com/mickeyvip/words-memory-game/tree/v3.0.
How do we choose random words from the list of words? Elm guide has an example of Random usage: https://guide.elm-lang.org/effects/random.html, and elm/random package has a nice API for generating random numbers.
I have found that Random.list might be useful: we can produce 3 random values from 0 to the length of words and take words at those indexes.
indexes : Int -> Int -> Random.Generator (List Int)
indexes quantity listLength =
Random.list quantity (Random.int 0 (listLength - 1))
Unfortunately, it may produces a list with repeated values. We can overcome this problem by, for example, keeping generating new random values until we have the required number of unique values. But in a search for simpler or "already made" solution I have found elm-community/random-extra package, which, as elm/random documentation says, may some day be merged with elm/random.
elm-community/random-extra has Random.List module which exposes a shuffle function which, as its name states, shuffles a list. So we can apply it on our list of words and take the first 3. Sounds simple enough.
Let's install it (stop the app if it is running):
$ elm install elm-community/random-extra
Accept the installation prompt by hitting y (or "enter").
We also need to have elm/random as a direct dependency, while up until now had been installed as an indirect dependency. When we install it, Elm installer will prompt us to move it from indirect to direct dependencies. Choose 'y':
$ elm install elm/random
I found it in your elm.json file, but in the "indirect" dependencies.
Should I move it into "direct" dependencies for more general use? [Y/n]:
Dependencies loaded from local cache.
Dependencies ready!
As wee see in the documentation, calling shuffle produces a Generator (List a), which is only a description of what we want to do. To actually execute the Generator we need to call Random.generate which in turn produces a Command, as described in the Random example in the Elm guide we saw earlier.
What we can do is take a list of indexes [0, 1, 2, 3...], shuffle this list and take the first 3 indexes. Those will be the indexes of the hidden words; their placement in the new list will be the sortKeys.
Let's add a function that will produce a Command:
import Random
import Random.List
--- COMMANDS ----
chooseWords : Words -> Cmd Msg
chooseWords chosenSentence =
List.range 0 (List.length chosenSentence - 1)
|> Random.List.shuffle
|> Random.map (List.take 3)
|> Random.map (List.indexedMap Tuple.pair)
|> Random.generate WordsChosen
The List.range 0 (List.length chosenSentence - 1) will give us the [0, 1, 2, 3, 4, 5, 6] (since we have 7 words in our sentence).
The shuffle will reorder the list.
The List.take 3 will take first 3 items from the reordered list.
The List.indexedMap Tuple.pair will map the list of word indexes to a List of Tuples where the first index is the sortKey and the second index is the index of the word. For example [(0, 1), (1, 6), (2, 3)].
Then we execute the command by calling Random.generate and passing it the new message WordsChosen.
To manipulate the result of the Random we use Random.map - similar to map of other types.
Let's add a new message, WordsChosen, that gets a list of chosen indexes:
type Msg
= WordChanged Int String
| WordsChosen (List (Int, Int))
Now the compiler will guide us to handle the new message in the update function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- omitted
WordsChosen sortIndexes ->
I didn't find any easier way to update the sentence so what I did is as follows: List.indexMap the current sentence, so we have a word index and a word.
WordsChosen sortIndexes ->
let
sentence =
List.indexedMap
(\wordIndex word ->
-- omitted
model.sentence
in
( { model | sentence = sentence }, Cmd.none )
Look if there is a sortIndex pair for a current word (wordIndex equals the second member of the Tuple):
-- omitted
sentence =
List.indexedMap
(\wordIndex word ->
let
hiddenIndex =
List.filter
(\( _, originalWordIndex ) ->
originalWordIndex == wordIndex
)
sortIndexes
-- omitted
If hiddenIndex is not empty, we need to replace current word with HiddenWord, and the sortKey will be the first member of the Tuple. Else we need to leave the current word as is:
-- omitted
newWord =
case List.head hiddenIndex of
Just ( sortKey, _ ) ->
HiddenWrd
{ sortKey = sortKey
, answer = Answer (wordToString word)
, playerChoice = PlayerChoice ""
}
Nothing ->
word
-- omitted
Here is the full code for the WordsChosen message handler:
WordsChosen sortIndexes ->
let
sentence =
List.indexedMap
(\wordIndex word ->
let
hiddenIndex =
List.filter
(\( _, originalWordIndex ) ->
originalWordIndex == wordIndex
)
sortIndexes
newWord =
case List.head hiddenIndex of
Just ( sortKey, _ ) ->
HiddenWrd
{ sortKey = sortKey
, answer = Answer (wordToString word)
, playerChoice = PlayerChoice ""
}
Nothing ->
word
in
newWord
)
model.sentence
in
( { model | sentence = sentence }, Cmd.none )
Let's also set all the words as SentenceWord:
initialModel : Model
initialModel =
{ sentence =
[ SentenceWrd "The"
, SentenceWrd "pen"
, SentenceWrd "is"
, SentenceWrd "mightier"
, SentenceWrd "than"
, SentenceWrd "the"
, SentenceWrd "sword"
]
}
We also need to update our init function to execute the chooseWords command:
init : () -> ( Model, Cmd Msg )
init _ =
( initialModel, chooseWords initialModel.sentence )
Refresh the page - we should see our game is working and each refresh makes it choose different words!
Let's add a nice "New Game" button so we don't need to refresh the page for a new game:
view : Model -> Html Msg
view model =
main_ [ class "section" ]
[ div [ class "container" ]
[ viewTitle
, div [ class "box" ]
[ p
[ class "has-text-centered" ]
[ viewOriginalSentence model.sentence
, viewSentence model.sentence
]
, viewHiddenWords (hiddenWords model.sentence)
-- new code
, div [ class "is-clearfix" ]
[ button
[ class "button is-info is-pulled-right"
, onClick NewGame
]
[ text "New Game" ]
]
]
]
]
Let's add NewGame message:
type Msg
= WordChanged Int String
| WordsChosen (List ( Int, Int ))
| NewGame
And handle it in the update function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- omitted
NewGame ->
( initialModel, chooseWords initialModel.sentence )
And after the page is refreshed, we can click "New Game" to restart the game.
The current progress is saved in the repo under a Tag v4.0 https://github.com/mickeyvip/words-memory-game/tree/v4.0.
Indicating Winning
It would be a good time to show the player that the sentence is correct.
After each change, we can check if every HiddenWord has a playerChoice that is equal to answer. If it is the case - we can render the "You Win" view.
Let's add a function that will check if there is a win:
isWin : Words -> Bool
isWin sentence =
List.all
(\word ->
case word of
SentenceWrd _ ->
True
HiddenWrd { answer, playerChoice } ->
answerToString answer == playerChoiceToString playerChoice
)
sentence
List.all will return True if for all of its members the predicate function returns True. If the word is SentenceWrd, there is nothing to check and we just return True. If the word is HiddenWrd, we return the result of comparing answer and playerChoice.
We can shorten isWin: we only interested in HiddenWords and we already have a function that takes a Words and returns List HiddenWord, which is hiddenWords. Let's use it:
isWin : Words -> Bool
isWin sentence =
List.all
(\{ answer, playerChoice } ->
answerToString answer == playerChoiceToString playerChoice
)
(hiddenWords sentence)
And if isWin handles HiddenWords only, we don't need to send it Words at all:
isWin : List HiddenWord -> Bool
isWin hiddenWordList =
List.all
(\{ answer, playerChoice } ->
answerToString answer == playerChoiceToString playerChoice
)
hiddenWordList
That looks much better.
We can get even fancier and remove the argument altogether and use currying ability of Elm, similar to how JSON decoders are declared:
isWin : List HiddenWord -> Bool
isWin =
List.all
(\{ answer, playerChoice } ->
answerToString answer == playerChoiceToString playerChoice
)
We are now returning a function that waits for List HiddenWord argument. This can be a little confusing for newcomers (like myself) though.
When the player wins, let's show the sentence and the "You Win!" message, and hide the sentence with drop-downs and the list of words. To make this easier we can refactor the main view to show viewGame or viewWin views depending on whether the win is True or False:
view : Model -> Html Msg
view model =
let
win : Bool
win =
isWin <| hiddenWords model.sentence
currentView : Html Msg
currentView =
if win then
viewWin model.sentence
else
viewGame model.sentence
in
main_ [ class "section" ]
[ div [ class "container" ]
[ viewTitle
, div [ class "box" ]
[ currentView ]
]
]
viewGame : Words -> Html Msg
viewGame sentence =
div []
[ viewSentence sentence
, viewHiddenWords (hiddenWords sentence)
]
viewWin : Words -> Html Msg
viewWin sentence =
div []
[ div [ class "notification is-primary" ]
[ h3 [ class "title is-3 has-text-centered" ] [ text "You Win!" ]
]
, h4
[ class "title is-4 has-text-centered" ]
[ viewOriginalSentence sentence ]
, div [ class "is-clearfix" ]
[ button
[ class "button is-info is-pulled-right"
, onClick NewGame
]
[ text "New Game" ]
]
]
This should be pretty straightforward and need no explanation. On the viewWin view we have a "You Win!" message, the sentence and the "New Game" button. I have also added is-big class to the title to make it bigger. On the viewGame view we show the game as before.
The current progress is saved in the repo under a Tag v5.0 https://github.com/mickeyvip/words-memory-game/tree/v5.0.
Generating Sentence Automatically
Until now we had manually set sentence, but in the actual game there will be many sentences as Strings and we want to be able to generate sentence from them.
How can we do it? We can use String.words. That will return a list of words that we can turn into what we need.
Let's first change our Model to have currentSentence : String, set it to "The pen is mightier than the sword" and set sentence to be an empty list:
type alias Model =
{ currentSentence : String
, sentence : Words
}
initialModel : Model
initialModel =
{ currentSentence = "The pen is mightier than the sword"
, sentence = []
}
Now we'll add a generateSentence function that will generate the sentence with the help of getWords:
getWords : String -> List String
getWords sentence =
sentence
|> String.words
generateSentence : String -> List Word
generateSentence sentenceString =
sentenceString
|> String.words
|> List.map SentenceWrd
Let's update our init function to invoke generateSentence:
init : () -> ( Model, Cmd Msg )
init _ =
let
sentence : Words
sentence =
generateSentence initialModel.currentSentence
model : Model
model =
{ initialModel | sentence = sentence }
in
( model, chooseWords sentence )
The game should work as before, only now the generating of the sentence is automatic.
However, when winning the game and clicking "New Game" we see an empty box. This is because we reset the model to initialModel which has sentence set to []. Let's change the update function to call init that generates the sentence and executes chooseWords command:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- omitted
NewGame ->
init ()
And now we can start a new game!
Refactoring Model Into Game States
We again got to a state that can get out of sync. The currentSentence is needed only for generating the sentence and we have to set sentence to [] at the beginning of each game because it is on our Model, but actually it is not needed for the first step.
Let's redefine our Model so it has 2 states:
-
Started- will have only thecurrentSentence : String -
Playing- will have only thesentence : Words
The game will start in Started state and call generateSentence. After that the state will be changed to Playing.
type alias StateStarted =
{ currentSentence : String }
type alias StatePlaying =
{ sentence : Words
}
type GameState
= Started StateStarted
| Playing StatePlaying
Now let's update our Model to be of type GameState:
type alias Model =
GameState
Our initialModel can be set to:
initialModel : Model
initialModel =
Started (StateStarted "The pen is mightier than the sword")
No unnecessary sentence in the initial state!
The init function becomes a little bit more verbose:
init : () -> ( Model, Cmd Msg )
init _ =
let
( model, cmd ) =
case initialModel of
Started { currentSentence } ->
let
sentence : Words
sentence =
generateSentence currentSentence
in
( Playing (StatePlaying sentence), chooseWords sentence )
Playing _ ->
( initialModel, Cmd.none )
in
( model, cmd )
We now need to pattern match our Model to know in which game state the game is right now. If it's in Started state, we will generate the sentence and execute chooseWords. In the other case (which at this point should not happen), we will return the initialModel as is and not execute any command.
In the update function we also need to take the game state into account:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
WordChanged index wordString ->
let
model_ =
case model of
Playing { sentence } ->
let
updateWord : Int -> Word -> Word
updateWord wordIndex word =
case word of
HiddenWrd hiddenWord ->
if wordIndex == index then
HiddenWrd
{ hiddenWord
| playerChoice = PlayerChoice wordString
}
else
word
_ ->
word
newSentence : Words
newSentence =
List.indexedMap updateWord sentence
in
Playing (StatePlaying newSentence)
Started _ ->
model
in
( model_, Cmd.none )
-- omitted
This change should be pretty straightforward: if the game is in Playing state - extract the sentence from the state, update it and return Playing state with the updated newSentence, otherwise - leave the model as is.
The same thing for WordsChosen:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- omitted
WordsChosen sortIndexes ->
let
model_ =
case model of
Playing { sentence } ->
let
sentence_ =
List.indexedMap
(\wordIndex word ->
let
hiddenIndex =
List.filter
(\( _, originalWordIndex ) ->
originalWordIndex == wordIndex
)
sortIndexes
newWord =
case List.head hiddenIndex of
Just ( sortKey, _ ) ->
HiddenWrd
{ sortKey = sortKey
, answer = Answer (wordToString word)
, playerChoice = PlayerChoice ""
}
Nothing ->
word
in
newWord
)
sentence
in
Playing (StatePlaying sentence_)
Started _ ->
model
in
( model_, Cmd.none )
The view has to be updated also:
view : Model -> Html Msg
view model =
let
currentView =
case model of
Playing { sentence } ->
let
win : Bool
win =
isWin <| hiddenWords sentence
in
if win then
viewWin sentence
else
viewGame sentence
Started { currentSentence } ->
viewStarted currentSentence
in
main_ [ class "section" ]
[ div [ class "container" ]
[ viewTitle
, div [ class "box" ]
[ currentView ]
]
]
When the game state is Started - we will render viewStarted view that will just show the current sentence:
viewStarted : String -> Html msg
viewStarted currentSentence =
p []
[ text currentSentence ]
Although we will not see it, since the game goes into Playing state after the init function, but we will use it in the future.
Now the game compiles and works as before, while the Model is much better designed.
It actually looks logical to have another playing state - Win. Thus the logic of winning will reside in the update function and not in the view like it is now, which feels odd.
type alias StateWin =
{ currentSentence : String
}
type GameState
= Started StateStarted
| Playing StatePlaying
| Win StateWin
The init:
init : () -> ( Model, Cmd Msg )
init _ =
let
( model, cmd ) =
case initialModel of
Started { currentSentence } ->
let
sentence : Words
sentence =
generateSentence currentSentence
in
( Playing (StatePlaying sentence), chooseWords sentence )
Playing _ ->
( initialModel, Cmd.none )
Win _ ->
( initialModel, Cmd.none )
in
( model, cmd )
Then update:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
WordChanged index wordString ->
let
model_ =
case model of
Playing { sentence } ->
let
updateWord : Int -> Word -> Word
updateWord wordIndex word =
case word of
HiddenWrd hiddenWord ->
if wordIndex == index then
HiddenWrd
{ hiddenWord
| playerChoice = PlayerChoice wordString
}
else
word
_ ->
word
newSentence : Words
newSentence =
List.indexedMap updateWord sentence
isWin_ =
isWin (hiddenWords newSentence)
in
if isWin_ then
Win (StateWin (sentenceToString newSentence))
else
Playing (StatePlaying newSentence)
Started _ ->
model
Win _ ->
model
in
( model_, Cmd.none )
WordsChosen sortIndexes ->
let
model_ =
case model of
Playing { sentence } ->
-- omitted
Win _ ->
model
in
( model_, Cmd.none )
We added the winning check to the Playing case. If there is a winning, we will change the state to Win. If the game is in Win state already - just return current model similarly to Started case.
We added sentenceToString function that converts Words to String:
sentenceToString : Words -> String
sentenceToString sentence =
sentence
|> List.map wordToString
|> String.join " "
An we can reuse it in viewOriginalSentence:
viewOriginalSentence : Words -> Html Msg
viewOriginalSentence words =
p
[ class "has-text-centered" ]
[ text <| sentenceToString words ]
The view is now much simpler:
view : Model -> Html Msg
view model =
let
currentView =
case model of
Playing { sentence } ->
viewGame sentence
Started { currentSentence } ->
viewStarted currentSentence
Win { currentSentence } ->
viewWin currentSentence
in
--- omitted
Now we see that we need to refactor viewWin to accept String and not Words:
viewWin : String -> Html Msg
viewWin sentence =
div []
[ div [ class "notification is-primary" ]
[ h3 [ class "title is-3 has-text-centered" ] [ text "You Win!" ]
]
, h4
[ class "title is-4 has-text-centered" ]
[ p
[ class "has-text-centered" ]
[ text sentence ]
]
, div [ class "is-clearfix" ]
[ button
[ class "button is-info is-pulled-right"
, onClick NewGame
]
[ text "New Game" ]
]
]
And there is no need for viewOriginalSentence, since we just use the same code that generates HTML inside `viewWin, so just delete it.
The game should compile and work as before, but now we have a 3 specific game states with data relevant to each state.
The current progress is saved in the repo under a Tag v6.0 https://github.com/mickeyvip/words-memory-game/tree/v6.0.
Stay tuned for the upcoming post, where we will have more sentences to choose from for each new game!


Top comments (4)
Thank you! This is exactly the example of Random.List that I needed.
Glad you have found this helpful, Brady!
This is the best response for me as an author.
The pace of your writing is such a joy! Thank you!
Thank you so much for the kind words.
Now I feel kind of bad for not finishing the series... Maybe some day I will.