Mathieu Agopian : Making a pong game in elm (3)

Following the two previous blog posts, let's continue taking tiny steps in our endeavour to create a pong game in elm.

We left off with a ball bouncing off two paddles, and two players able to move their paddles. And the realization that we were a long way off to have a game that is at least mildly enjoyable.

Moving paddles at the same time

For starters, only one player at a time could move their paddle. And this is because we only moved the paddle when we would detect a onKeyDown. Which meant that we depended on a continuous stream of those events to continuously move the paddle when a player would keep pressing the key.

But we saw in the previous post that when a player would press a key and hold it, the events for this key would stop as soon as another key is pressed (eg if the second player wanted to move their paddle).

After some digging around on the internet it seems that the proper way to deal with that issue is to keep track of which key has been pressed (by tracking the onKeyDown events), and then update the state of this key when an onKeyUp is received.

One way to do that would be to store the pressed keys in a list, and then remove a key from the list once we detect that it's released. Another way would be track the key states in a dictionnary:

type KeyState
    = Pressed
    | NotPressed

type alias keyStates =
    { arrowUp : KeyState
    , arrowDown : KeyState
    , charE : KeyState
    , charD : KeyState
    }

Thinking about that a bit more: what if we have the following keyState:

    { arrowUp = Pressed
    , arrowDown = Pressed
    ...
    }

This would mean that we have two keys pressed for the same paddle, how would we decide what to do with that? Maybe we could have some kind of clever algorithm that would update that dictionnary...

If you're like me, you try to stay away from any clever code. As stated by Brian Kernighan

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?

— The Elements of Programming Style, 2nd edition, chapter 2

Maybe we could come up with some other representation of the state. How about storing the state of the paddles movement?

type PaddleMovement
    = MovingUp
    | MovingDown
    | NotMoving

This way, whenever we get an onKeyDown for a key, we would update the paddle movement: pressing down would result in MovingDown, and then pressing up (even if we're still pressing down) would update the movement to MovingUp, and any onKeyUp would reset the state to NotMoving.

     { ball : Ball
     , rightPaddle : Paddle
     , leftPaddle : Paddle
+    , rightPaddleMovement : PaddleMovement
+    , leftPaddleMovement : PaddleMovement
     }


@@ -35,6 +37,12 @@ type alias PaddleInfo =
     }


+type PaddleMovement
+    = MovingUp
+    | MovingDown
+    | NotMoving
+
+
 type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
@@ -56,6 +64,8 @@ init _ =
     ( { ball = initBall
       , rightPaddle = RightPaddle <| initPaddle 480
       , leftPaddle = LeftPaddle <| initPaddle 10
+      , rightPaddleMovement = NotMoving
+      , leftPaddleMovement = NotMoving
       }
     , Cmd.none
     )

commit

Now we need to update the state in the update function, in the KeyDown playerAction case:

         KeyDown playerAction ->
             case playerAction of
                 RightPaddleUp ->
-                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle -10 }
+                    ( { model | rightPaddleMovement = MovingUp }
                     , Cmd.none
                     )

                 RightPaddleDown ->
-                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle 10 }
+                    ( { model | rightPaddleMovement = MovingDown }
                     , Cmd.none
                     )

                 LeftPaddleUp ->
-                    ( { model | leftPaddle = model.leftPaddle |> updatePaddle -10 }
+                    ( { model | leftPaddleMovement = MovingUp }
                     , Cmd.none
                     )

                 LeftPaddleDown ->
-                    ( { model | leftPaddle = model.leftPaddle |> updatePaddle 10 }
+                    ( { model | leftPaddleMovement = MovingDown }
                     , Cmd.none
                     )

commit

We're updating the paddle movements, or directions... but we aren't actually moving them. We used to add or substract a number of pixels from their y coordinates directly on the KeyDown playerAction message, but it's not the case anymore.

Updating the movements, reacting to player inputs, updating the world and all that is usually done in a "game loop". The closer we have to a game loop in our program is the onAnimationFrameDelta message. So we'll first update our helper function updatePaddle to take a PaddleMovement instead of an amount:

-updatePaddle : Int -> Paddle -> Paddle
-updatePaddle amount paddle =
+updatePaddle : PaddleMovement -> Paddle -> Paddle
+updatePaddle movement paddle =
+    let
+        amount =
+            case movement of
+                MovingUp ->
+                    -10
+
+                MovingDown ->
+                    10
+
+                NotMoving ->
+                    0
+    in
     case paddle of
         RightPaddle paddleInfo ->
             { paddleInfo | y = paddleInfo.y + amount }

And we can now use that in the update function:

                         | x = ball.x + horizSpeed
                         , horizSpeed = horizSpeed
                     }
+
+                updatedRightPaddle =
+                    updatePaddle model.rightPaddleMovement model.rightPaddle
+
+                updatedLeftPaddle =
+                    updatePaddle model.leftPaddleMovement model.leftPaddle
             in
-            ( { model | ball = updatedBall }, Cmd.none )
+            ( { model
+                | ball = updatedBall
+                , rightPaddle = updatedRightPaddle
+                , leftPaddle = updatedLeftPaddle
+              }
+            , Cmd.none
+            )

         KeyDown playerAction ->
             case playerAction of

commit

And we're now done, both paddles can move at the same time!

Both paddles moving at the same time

What is it that you're saying? That I forgot to manage the case when there's no player action anymore? Of course I didn't, I just wanted to make sure you were still following along. And you were, well done. I never doubted you.

We now need to also subscribe to the onKeyUp events for the keys we're using for the player actions, which means adding

 type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
+    | KeyUp PlayerAction


 type PlayerAction
@@ -160,6 +161,28 @@ update msg model =
                     , Cmd.none
                     )

+        KeyUp playerAction ->
+            case playerAction of
+                RightPaddleUp ->
+                    ( { model | rightPaddleMovement = NotMoving }
+                    , Cmd.none
+                    )
+
+                RightPaddleDown ->
+                    ( { model | rightPaddleMovement = NotMoving }
+                    , Cmd.none
+                    )
+
+                LeftPaddleUp ->
+                    ( { model | leftPaddleMovement = NotMoving }
+                    , Cmd.none
+                    )
+
+                LeftPaddleDown ->
+                    ( { model | leftPaddleMovement = NotMoving }
+                    , Cmd.none
+                    )
+

 updatePaddle : PaddleMovement -> Paddle -> Paddle
 updatePaddle movement paddle =
@@ -248,6 +271,7 @@ subscriptions _ =
     Sub.batch
         [ Browser.Events.onAnimationFrameDelta OnAnimationFrame
         , Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
+        , Browser.Events.onKeyUp (Decode.map KeyUp keyDecoder)
         ]

commit

Source code up to this point.

Clamping the paddles

While it may be fun at first to be able to move your paddle off the screen, and while it may be seen as an extra challenge, let's stick to the original concept, and prevent the paddles from disappearing.

We can do that by making sure we don't update the paddle's y position with a value that's "out of bounds":

     in
     case paddle of
         RightPaddle paddleInfo ->
-            { paddleInfo | y = paddleInfo.y + amount }
+            { paddleInfo
+                | y =
+                    paddleInfo.y
+                        + amount
+                        |> clamp 0 (500 - paddleInfo.height)
+            }
                 |> RightPaddle

         LeftPaddle paddleInfo ->
-            { paddleInfo | y = paddleInfo.y + amount }
+            { paddleInfo
+                | y =
+                    paddleInfo.y
+                        + amount
+                        |> clamp 0 (500 - paddleInfo.height)
+            }
                 |> LeftPaddle

commit

Source code up to this point.

Here we're using the very convenient clamp helper to make sure the y coordinates of the paddles can't go above 500 - paddle.height (which means the full paddle is always displayed), nor below 0.

Adding some verticalization

Up till now the ball would always move horizontally. Never up or down. And as such, the game... well, let's say that it wasn't very challenging. But that changes now!

Let's add a vertSpeed to the ball, and set it to a fixed value for now:

@@ -21,6 +21,7 @@ type alias Ball =
     , y : Int
     , radius : Int
     , horizSpeed : Int
+    , vertSpeed : Int
     }


@@ -78,6 +79,7 @@ initBall =
     , y = 250
     , radius = 10
     , horizSpeed = 4
+    , vertSpeed = 2
     }


@@ -122,6 +124,7 @@ update msg model =
                 updatedBall =
                     { ball
                         | x = ball.x + horizSpeed
+                        , y = ball.y + ball.vertSpeed
                         , horizSpeed = horizSpeed
                     }

commit

We're not doing anything fancy here: adding a new field to the Ball record, initializing it to 2, and adding it to the y coordinates of the ball on each frame.

And behold the result!

Ball moving with a vertical speed

And now we know what we need to do next:

Bouncing the ball off the walls

What good is a ball that we can't see anymore? Let's fix that by mimicking what we did for the bouncing off the paddles:

                     else
                         ball.horizSpeed

+                shouldBounceVertically =
+                    shouldBallBounceVertically model.ball
+
+                vertSpeed =
+                    if shouldBounceVertically then
+                        ball.vertSpeed * -1
+
+                    else
+                        ball.vertSpeed
+
                 updatedBall =
                     { ball
                         | x = ball.x + horizSpeed
-                        , y = ball.y + ball.vertSpeed
+                        , y = ball.y + vertSpeed
                         , horizSpeed = horizSpeed
+                        , vertSpeed = vertSpeed
                     }

                 updatedRightPaddle =
@@ -233,6 +244,15 @@ shouldBallBounce paddle ball =
                 && (ball.y <= y + height)


+shouldBallBounceVertically : Ball -> Bool
+shouldBallBounceVertically ball =
+    let
+        radius =
+            ball.radius
+    in
+    ball.y <= radius || ball.y >= (500 - radius)
+
+
 view : Model -> Svg.Svg Msg
 view { ball, rightPaddle, leftPaddle } =
     svg

commit

Losing and winning

Whenever the ball reaches the left or right side of the screen, the game should reset, and the opposite player should win a point.

So let's detect the win/lose condition:

@@ -44,6 +44,11 @@ type PaddleMovement
     | NotMoving


+type Player
+    = LeftPlayer
+    | RightPlayer
+
+
 type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
@@ -144,6 +149,10 @@ update msg model =

                 updatedLeftPaddle =
                     updatePaddle model.leftPaddleMovement model.leftPaddle
+
+                winner =
+                    maybeWinner updatedBall
+                        |> Debug.log "Winner"
             in
             ( { model
                 | ball = updatedBall
@@ -253,6 +262,18 @@ shouldBallBounceVertically ball =
     ball.y <= radius || ball.y >= (500 - radius)


+maybeWinner : Ball -> Maybe Player
+maybeWinner ball =
+    if ball.x <= ball.radius then
+        Just RightPlayer
+
+    else if ball.x >= (500 - ball.radius) then
+        Just LeftPlayer
+
+    else
+        Nothing
+
+
 view : Model -> Svg.Svg Msg
 view { ball, rightPaddle, leftPaddle } =
     svg

commit

This displays Winner: Nothing in the console on each frame, until there's a Winner: Just RightPlayer as soon as the ball hits the right border... and then on each following frame, as the game doesn't reset. Yet.

"But Mathieu, what is this Maybe thing, and all that Just and Nothing nonsense?". Hoy! Behave, that's no nonsense, that's proper engineering! It's called the Maybe type and it represents "values that may or may not exist", which is exactly what we need here: there may be a winner, or maybe not. The result is either "just a player" or "nothing" (no winner). And we can now use this Maybe Player to update a new custom type that we'll call GameStatus:

@@ -13,6 +13,7 @@ type alias Model =
     , leftPaddle : Paddle
     , rightPaddleMovement : PaddleMovement
     , leftPaddleMovement : PaddleMovement
+    , gameStatus : GameStatus
     }


@@ -49,6 +50,11 @@ type Player
     | RightPlayer


+type GameStatus
+    = NoWinner
+    | Winner Player
+
+
 type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
@@ -73,6 +79,7 @@ init _ =
       , leftPaddle = LeftPaddle <| initPaddle 10
       , rightPaddleMovement = NotMoving
       , leftPaddleMovement = NotMoving
+      , gameStatus = NoWinner
       }
     , Cmd.none
     )
@@ -150,14 +157,19 @@ update msg model =
                 updatedLeftPaddle =
                     updatePaddle model.leftPaddleMovement model.leftPaddle

-                winner =
-                    maybeWinner updatedBall
-                        |> Debug.log "Winner"
+                gameStatus =
+                    case maybeWinner updatedBall of
+                        Nothing ->
+                            NoWinner
+
+                        Just player ->
+                            Winner player
             in
             ( { model
                 | ball = updatedBall
                 , rightPaddle = updatedRightPaddle
                 , leftPaddle = updatedLeftPaddle
+                , gameStatus = gameStatus
               }
             , Cmd.none
             )

commit

We now have a proper GameState that gets updated whenever the ball reaches the left or right, but we aren't doing anything with it yet. What should we do with it?

Well... if we're in the NoWinner state, it means we should be playing, and as such listening to user input and animation frames. If we're in the Winner ... state, we shouldn't.

 subscriptions : Model -> Sub Msg
-subscriptions _ =
-    Sub.batch
-        [ Browser.Events.onAnimationFrameDelta OnAnimationFrame
-        , Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
-        , Browser.Events.onKeyUp (Decode.map KeyUp keyDecoder)
-        ]
+subscriptions model =
+    case model.gameStatus of
+        NoWinner ->
+            Sub.batch
+                [ Browser.Events.onAnimationFrameDelta OnAnimationFrame
+                , Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
+                , Browser.Events.onKeyUp (Decode.map KeyUp keyDecoder)
+                ]
+
+        Winner _ ->
+            Sub.none

commit

As easy as this! Now the game stops as soon as a player wins.

Now let's restart the game after a 500 milliseconds delay. For that, we'll introduce a new concept: the Task which makes "it easy to describe asynchronous operations": in our case, the Task will be a Process.sleep.

Once we have the Task, we can ask the elm runtime to execute it for us using Task.perform which will return a Cmd Msg. We'll attach a new Msg variant that we'll call SleepDone to that Cmd:

 import Browser
 import Browser.Events
 import Json.Decode as Decode
+import Process
 import Svg exposing (..)
 import Svg.Attributes exposing (..)
+import Task


 type alias Model =
@@ -59,6 +61,7 @@ type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
     | KeyUp PlayerAction
+    | SleepDone ()


 type PlayerAction
@@ -157,13 +160,18 @@ update msg model =
                 updatedLeftPaddle =
                     updatePaddle model.leftPaddleMovement model.leftPaddle

-                gameStatus =
+                ( gameStatus, cmd ) =
                     case maybeWinner updatedBall of
                         Nothing ->
-                            NoWinner
+                            ( NoWinner, Cmd.none )

                         Just player ->
-                            Winner player
+                            let
+                                delayCmd =
+                                    Process.sleep 500
+                                        |> Task.perform SleepDone
+                            in
+                            ( Winner player, delayCmd )
             in
             ( { model
                 | ball = updatedBall
@@ -171,7 +179,7 @@ update msg model =
                 , leftPaddle = updatedLeftPaddle
                 , gameStatus = gameStatus
               }
-            , Cmd.none
+            , cmd
             )

         KeyDown playerAction ->
@@ -218,6 +226,13 @@ update msg model =
                     , Cmd.none
                     )

+        SleepDone _ ->
+            let
+                _ =
+                    Debug.log "restart" "game"
+            in
+            ( model, Cmd.none )
+

 updatePaddle : PaddleMovement -> Paddle -> Paddle
 updatePaddle movement paddle =

commit

This one involves quite a lot, so let's decompose it piece by piece:

On each frame, we now not only change the game status if needed, we also send a Cmd to the elm runtime if there was a win. This command is:

Process.sleep 500
    |> Task.perform SleepDone

As a reminder, that's the same as writing

Task.perform SleepDone (Process.sleep 500)

The Task.perform translates a Task into a Cmd, which can then be sent to the elm runtime, by returning it from the update function. Which brings us to the ( Model, Cmd Msg ) in the type signature of the update function, which is a tuple type. A tuple is a fixed size list of things with types which may differ. This is very different from the List type which is a variable size list of things of the same type.

Back to the code:

( gameStatus, cmd ) =
    case maybeWinner updatedBall of
        Nothing ->
            ( NoWinner, Cmd.none )

        Just player ->
            let
                delayCmd =
                    Process.sleep 500
                        |> Task.perform SleepDone
            in
            ( Winner player, delayCmd )

The first part before the = sign is destructuring a 2-tuple into two variables names gameStatus and cmd. The cmd is the command that will be returned by the update function if we're processing an onAnimationFrameDelta message.

And this command is either Cmd.none (no command) if there's NoWinner, or the delayCmd if there's a winner.

The end of the previous diff is simply processing the SleepDone message. At the moment the only thing it's doing is printing a debug message in the console. As we've seen previously, using the _ means we don't care about the variable (so we don't care about the "game" string that we passed to the Debug.log function, and we don't care either about the data attached to the SleepDone message).

"But wait Mathieu, if we don't care about the data attached to the SleepDone variant, why does it even have it in the first place?". Very good question. Brace yourselves for the answer:

A task always returns something. Sometimes, this "thing" is uninteresting, in which case we use the unit, which is represented by () and has only one value: (). And if we go back to the Process.sleep signature, it says it returns a Task x () (so a Task that returns a unit).

And this thing that the Task returns is the same thing that is attached to the message that the Task.perform takes as its first argument. Hence the SleepDone ().

Let me show you a little nugget of cleverness (but please remember, being clever is usually a bad idea, so use this sparingly): the always helper is a function that always returns the same thing, whatever the argument you give it. This seems pretty useless, but we could use it to our advantage in our case:

@@ -61,7 +61,7 @@ type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
     | KeyUp PlayerAction
-    | SleepDone ()
+    | SleepDone


 type PlayerAction
@@ -169,7 +169,7 @@ update msg model =
                             let
                                 delayCmd =
                                     Process.sleep 500
-                                        |> Task.perform SleepDone
+                                        |> Task.perform (always SleepDone)
                             in
                             ( Winner player, delayCmd )
             in
@@ -226,7 +226,7 @@ update msg model =
                     , Cmd.none
                     )

-        SleepDone _ ->
+        SleepDone ->
             let
                 _ =
                     Debug.log "restart" "game"

We don't care about the data attached to the message by Task.perform, so we just discard it by using the always helper. This might seem confusing, but keep in mind that a custom type variant is also a constructor. So when we wanted to create a new right paddle, we would do RightPaddle paddleInfo. You can see RightPaddle as a function with the following type signature:

RightPaddle : PaddleInfo -> Paddle

So you can also see (always SleepDone) as a function that takes a parameter and returns SleepDone, and we could call it alwaysSleepDone:

alwaysSleepDone : a -> Msg

Using a type starting with a lowercase (the a in the type signature just above) means that it could be any type, including a unit. In any case, we don't care about the type that's being passed to the helper, so no need to be specific here.

So the final diff would be:

@@ -61,7 +61,7 @@ type Msg
     = OnAnimationFrame Float
     | KeyDown PlayerAction
     | KeyUp PlayerAction
-    | SleepDone ()
+    | SleepDone


 type PlayerAction
@@ -167,9 +167,13 @@ update msg model =

                         Just player ->
                             let
+                                alwaysSleepDone : a -> Msg
+                                alwaysSleepDone =
+                                    always SleepDone
+
                                 delayCmd =
                                     Process.sleep 500
-                                        |> Task.perform SleepDone
+                                        |> Task.perform alwaysSleepDone
                             in
                             ( Winner player, delayCmd )
             in
@@ -226,7 +230,7 @@ update msg model =
                     , Cmd.none
                     )

-        SleepDone _ ->
+        SleepDone ->
             let
                 _ =
                     Debug.log "restart" "game"

commit

Source code up to this point.

This didn't give us much in terms of readability, or at least it's debatable. I know I'd rather have obvious types and slightly more complex code, but I guess that's a matter of taste.

Restarting the game

So what should happen once the delay has elapsed, and the game should "restart"? Well the ball should be reset to its initial position and speed, and the game status should be NoWinner again:

         SleepDone ->
-            let
-                _ =
-                    Debug.log "restart" "game"
-            in
-            ( model, Cmd.none )
+            ( { model
+                | ball = initBall
+                , gameStatus = NoWinner
+              }
+            , Cmd.none
+            )

commit

Once the ball touches one of the "goals", the whole game stops for half a second, and then the ball position is reset.

Keeping track of the score

Let's add a score record with the rightPlayerScore and leftPlayerScore fields to the model, and update them whenever there's a win:

@@ -16,6 +16,7 @@ type alias Model =
     , rightPaddleMovement : PaddleMovement
     , leftPaddleMovement : PaddleMovement
     , gameStatus : GameStatus
+    , score : Score
     }


@@ -71,6 +72,12 @@ type PlayerAction
     | LeftPaddleDown


+type alias Score =
+    { rightPlayerScore : Int
+    , leftPlayerScore : Int
+    }
+
+
 type alias Flags =
     ()

@@ -83,6 +90,10 @@ init _ =
       , rightPaddleMovement = NotMoving
       , leftPaddleMovement = NotMoving
       , gameStatus = NoWinner
+      , score =
+            { rightPlayerScore = 0
+            , leftPlayerScore = 0
+            }
       }
     , Cmd.none
     )
@@ -160,10 +171,10 @@ update msg model =
                 updatedLeftPaddle =
                     updatePaddle model.leftPaddleMovement model.leftPaddle

-                ( gameStatus, cmd ) =
+                ( gameStatus, score, cmd ) =
                     case maybeWinner updatedBall of
                         Nothing ->
-                            ( NoWinner, Cmd.none )
+                            ( NoWinner, model.score, Cmd.none )

                         Just player ->
                             let
@@ -174,14 +185,19 @@ update msg model =
                                 delayCmd =
                                     Process.sleep 500
                                         |> Task.perform alwaysSleepDone
+
+                                updatedScore =
+                                    updateScores model.score player
+                                        |> Debug.log "score"
                             in
-                            ( Winner player, delayCmd )
+                            ( Winner player, updatedScore, delayCmd )
             in
             ( { model
                 | ball = updatedBall
                 , rightPaddle = updatedRightPaddle
                 , leftPaddle = updatedLeftPaddle
                 , gameStatus = gameStatus
+                , score = score
               }
             , cmd
             )
@@ -306,6 +322,16 @@ maybeWinner ball =
         Nothing


+updateScores : Score -> Player -> Score
+updateScores score winner =
+    case winner of
+        RightPlayer ->
+            { score | rightPlayerScore = score.rightPlayerScore + 1 }
+
+        LeftPlayer ->
+            { score | leftPlayerScore = score.leftPlayerScore + 1 }
+
+
 view : Model -> Svg.Svg Msg
 view { ball, rightPaddle, leftPaddle } =
     svg

commit

Now that we have the score, we can display it ;)

@@ -188,7 +188,6 @@ update msg model =

                                 updatedScore =
                                     updateScores model.score player
-                                        |> Debug.log "score"
                             in
                             ( Winner player, updatedScore, delayCmd )
             in
@@ -333,7 +332,7 @@ updateScores score winner =


 view : Model -> Svg.Svg Msg
-view { ball, rightPaddle, leftPaddle } =
+view { ball, rightPaddle, leftPaddle, score } =
     svg
         [ width "500"
         , height "500"
@@ -343,6 +342,7 @@ view { ball, rightPaddle, leftPaddle } =
         [ viewBall ball
         , viewPaddle rightPaddle
         , viewPaddle leftPaddle
+        , viewScore score
         ]


@@ -376,6 +376,19 @@ viewPaddle paddle =
         []


+viewScore : Score -> Svg.Svg Msg
+viewScore score =
+    g
+        [ fontSize "100px"
+        , fontFamily "monospace"
+        ]
+        [ text_ [ x "100", y "100", textAnchor "start" ]
+            [ text <| String.fromInt score.leftPlayerScore ]
+        , text_ [ x "400", y "100", textAnchor "end" ]
+            [ text <| String.fromInt score.rightPlayerScore ]
+        ]
+
+
 subscriptions : Model -> Sub Msg
 subscriptions model =
     case model.gameStatus of

commit

Source code up to this point.

Tada!!!

Displaying the score

Maybe it's time to talk briefly about the view. You might have been wondering how that was working.

In elm we have packages that provide helpers to build the node tree. There's one for Html and one for SVG, and they both work the same way. Each node builder has two parameters:

One notable exception is the text helper which just returns plain text.

This is how you would build a paragraph with the foobar class:

Html.p
    [ Html.Attributes.class "foobar" ]
    [ Html.text "Hello world"]

So building a node tree (a DOM) is a matter of calling helpers and providing them their list of attributes and children.

And that's exactly what we did with the viewScore function:

The use of an underscore in text_ is needed to disambiguate between the <text> SVG node and the plain text helper text.

We now have a fully playable game! It might not be pretty, or fun, but it has all the minimal requirements, congratulations!

Next time, we might have a look at how to add a few bells and whistles just for the sake of introducing a few more elm concepts ;)


There's now a follow up.