Building Eloquence game in Elm

I've decided to try out Elm language by implementing a game. At first glance, writing an article about it may appear to be as original as writing “Yet another tutorial about creating a Ruby on Rails blog”. Actually, it is 😆

I, strange though it may sound, consider Elm as a smooth introduction to Haskell and functional world. It combines such features as immutability and strong static typing (strong enough to ditch writing tests 😅) and “beginner-friendliness” by virtue of avoiding “ominous” abstractions for managing side effects. Still, it stays simple and easy to get the hang of.

As an exercise, what about building one of the games of the amazing Elevate App? The rules are simple: just provide as many as possible synonyms to the highlighted word in the sentence.
The source code is located here . I'm just going to highlight basic moments in the article.

Here’s how the complete app works:


Initialize the app's backbone

Create Elm App tool was used to initialize an application with no build configuration. Standard functions are generated (init, update, view, etc...) to be filled in the future.

module Main exposing (..)

import Html exposing (Html, text, div, img)
import Html.Attributes exposing (src)

---- MODEL ----

type alias Model =
    {}

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

---- UPDATE ----

type Msg
    = NoOp

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

---- VIEW ----

view : Model -> Html Msg
view model =
    div []
        [ img [ src "/logo.svg" ] []
        , div [] [ text "Your Elm App is working!" ]
        ]

---- SUBSCRIPTIONS ----

subscriptions : Model -> Sub Msg
subscriptions model =
    always Sub.none

---- PROGRAM ----

main : Program Never Model Msg
main =
    Html.program
        { view = view
        , init = init
        , update = update
        , subscriptions = subscriptions
        }


Model

Model is the state of the application. First and foremost, let’s specify its type. Since the whole state consists of three stages (before a round is played, playing the round, after the round has been played), it should have corresponding types:

type Model
    = StartRound
    | PlayingRound PlayingRoundState
    | GameOver GameOverState
StartRound

It’s just a constant which identifies that the app is in the state of awaiting a round to be played.

PlayingRound

The stage provides a structure for providing an experience of viewing a sentence and enter the synonyms for a specific word in the sentence.

type alias Sentence =
    { prefix : String
    , word : String
    , description : String
    , synonyms : Set.Set String
    }

type alias PlayingRoundState =
    { words : List String
    , sentence : Sentence
    , elapsed : Float
    , word : String
    , wrongWord : Bool
    }
GameOver

The stage displays the number of correctly entered synonyms and a random synonym which could have been entered (if there is one).

type alias GameOverState =
    { hint : Maybe String
    , score : Int
    }

If a user enters all the possible synonyms for a word, there won’t be a synonym, that could be shown as a hint in the GameOver state. All of a sudden hint won’t equal null or undefined in that case. hint has Maybe String , which means that it can be either Just String or Nothing . The type system requires processing every of these cases, which is one of the reasons why we are not going to meet cool Cannot read property 'a' of null in Elm.


Init

The function which specifies the initial state of the application. In our case, it is StartRound screen.

init : ( Model, Cmd Msg )
init =
    ( StartRound, Cmd.none )

View

View is a way to view the state as HTML.

view : Model -> Html Msg
view model =
    case model of
        StartRound ->
            renderStartScreen

        PlayingRound state ->
            renderPlayingScreen state

        GameOver state ->
            renderGameOverScreen state

Since the function just renders a particular screen depending on the type of the model, I won’t put the source code in the article. It still can be found here .


Subscriptions

The application needs to update the timer every second and change the state to GameOver after the 20 seconds from the start of a round is passed. That’s why a subscription needs to be defined.

Subscriptions is how your application can listen for external input.

subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every second Tick

That will call update function every second with a Tick message


Update

update function is the most complicated part of the application, which contains the decent part of the logic (at least in my case). It is a way to update the state depending on the type of the message emitted as a result of users’ interaction with the application.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case model of
        PlayingRound state ->
            updatePlayingRound msg state

        StartRound ->
            updateIdleState msg model

        GameOver state ->
            updateIdleState msg model
updateIdleState

StartRound and GameOver identically handle the same types of messages, hence use the same function. A user should be able to start (or restart) a round in these particular stages. That’s why if the message passed to update is of type StartGame a hard-coded initial playing state is returned.

updateIdleState : Msg -> Model -> ( Model, Cmd Msg )
updateIdleState msg model =
    case msg of
        StartGame ->
            let
                focus =
                    Dom.focus fieldId |> Task.attempt (\_ -> NoOp)

                playingModel =
                    let
                        sentence =
                            Sentence "The application is"
                                "great"
                                "Be more expressive"
                                (Set.fromList
                                    [ "awesome"
                                    , "amazing"
                                    , "breathtaking"
                                    , "exceptional"
                                    , "fabulous"
                                    , "glorious"
                                    , "impressive"
                                    , "incredible"
                                    , "majestic"
                                    , "magnificent"
                                    , "marvelous"
                                    , "superb"
                                    , "supercalifragilisticexpialidocious"
                                    , "unbelievable"
                                    , "unthinkable"
                                    ]
                                )
                    in
                        PlayingRound (PlayingRoundState [] sentence 0 "" False)
            in
                ( playingModel, focus )

        _ ->
            model ! []

There are two things, that I would like to point out here:

updatePlayingRound

The function handles users’ interaction during a playing of a round.

updatePlayingRound : Msg -> PlayingRoundState -> ( Model, Cmd Msg )
updatePlayingRound msg state =
    case msg of
        UpdateWord value ->
            PlayingRound { state | word = value }
                ! []

        AddWord ->
            if Set.member state.word state.sentence.synonyms then
                let
                    oldSentence =
                        state.sentence

                    newSentence =
                        { oldSentence | synonyms = Set.remove state.word oldSentence.synonyms }
                in
                    PlayingRound { state | words = state.word :: state.words, sentence = newSentence, word = "" }
                        ! []
            else
                ( PlayingRound { state | wrongWord = True }
                , Process.sleep 200 |> Task.perform (\_ -> ClearField)
                )

        ClearField ->
            PlayingRound { state | word = "", wrongWord = False } ! []

        EndRound words randomNumber ->
            let
                word =
                    Array.get randomNumber (Array.fromList words)
            in
                GameOver (GameOverState word (List.length state.words)) ! []

        Tick _ ->
            if state.elapsed == period then
                let
                    words =
                        Set.toList state.sentence.synonyms

                    command =
                        Random.generate (EndRound words) (Random.int 0 (List.length words - 1))
                in
                    ( PlayingRound state, command )
            else
                PlayingRound { state | elapsed = state.elapsed + 1 } ! []

        _ ->
            PlayingRound state ! []

There are two things, that I would like to explain here:


Conclusion

What I really liked during my experience of building this game is that the development was led by the language itself. The Elm Architecture, type system, and even immutability was defining the way the application must have been built. It was also interesting to try these and other concepts (such as sum types) in action and see how they do something tangible👆