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

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

We left off with a ball and a single paddle. The ball would move towards the right, bounce off the paddle, and then move left until it left the screen.

Adding a left paddle

Before adding a left paddle, let's slightly change our code to prepare for it, by renaming the current paddle to rightPaddle:

 type alias Model =
     { ball : Ball
-    , paddle : Paddle
+    , rightPaddle : Paddle
     }


@@ -38,9 +38,8 @@ type alias Flags =

 init : Flags -> ( Model, Cmd Msg )
 init _ =
-    ( { ball =
-            initBall
-      , paddle = initPaddle
+    ( { ball = initBall
+      , rightPaddle = initPaddle
       }
     , Cmd.none
     )
@@ -83,7 +82,7 @@ update msg model =
                     model.ball

                 shouldBounce =
-                    shouldBallBounce model.paddle model.ball
+                    shouldBallBounce model.rightPaddle model.ball

                 horizSpeed =
                     if shouldBounce then
@@ -109,7 +108,7 @@ shouldBallBounce paddle ball =


 view : Model -> Svg.Svg Msg
-view { ball, paddle } =
+view { ball, rightPaddle } =
     svg
         [ width "500"
         , height "500"
@@ -117,7 +116,7 @@ view { ball, paddle } =
         , Svg.Attributes.style "background: #efefef"
         ]
         [ viewBall ball
-        , viewPaddle paddle
+        , viewPaddle rightPaddle
         ]

commit

Adding the left paddle should now be very straightforward:

 type alias Model =
     { ball : Ball
     , rightPaddle : Paddle
+    , leftPaddle : Paddle
     }


@@ -40,6 +41,7 @@ init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { ball = initBall
       , rightPaddle = initPaddle
+      , leftPaddle = initPaddle
       }
     , Cmd.none
     )
@@ -108,7 +110,7 @@ shouldBallBounce paddle ball =


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

commit

Yes, you clever person, I know what you're thinking: "we can't see the left paddle! And it's obvious, it's because you placed it exactly at the same position as the right paddle!". I'm proud of you, and yes, you are right. Let's fix that by modifying the initPaddle function which should now take an initial x position.

 init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { ball = initBall
-      , rightPaddle = initPaddle
-      , leftPaddle = initPaddle
+      , rightPaddle = initPaddle 480
+      , leftPaddle = initPaddle 10
       }
     , Cmd.none
     )
@@ -56,9 +56,9 @@ initBall =
     }


-initPaddle : Paddle
-initPaddle =
-    { x = 480
+initPaddle : Int -> Paddle
+initPaddle initialX =
+    { x = initialX
     , y = 225
     , width = 10
     , height = 50

commit

Left paddle with the ball moving towards it

"But wait Mathieu, can't you see the ball is going right through the left paddle!". I sure do, and yes, let's now fix that by changing the shouldBallBounce helper function.

It'll be a bit tricky though, because we need to check both paddles slightly differently: if it's the left paddle, the check on the y position is exactly the same, but the check on the x position now needs to make sure that the ball stays "right of" the left paddle, which means that the ball's center minus its radius is bigger than the left paddle's x position plus its width.

                 shouldBounce =
                     shouldBallBounce model.rightPaddle model.ball
+                        || shouldBallBounce model.leftPaddle model.ball

                 horizSpeed =
                     if shouldBounce then
@@ -104,9 +105,17 @@ update msg model =

 shouldBallBounce : Paddle -> Ball -> Bool
 shouldBallBounce paddle ball =
-    (ball.x + ball.radius >= paddle.x)
-        && (ball.y >= paddle.y)
-        && (ball.y <= paddle.y + 50)
+    if paddle.x == 10 then
+        -- left paddle
+        (ball.x - ball.radius <= paddle.x + paddle.width)
+            && (ball.y >= paddle.y)
+            && (ball.y <= paddle.y + 50)
+
+    else
+        -- right paddle
+        (ball.x + ball.radius >= paddle.x)
+            && (ball.y >= paddle.y)
+            && (ball.y <= paddle.y + 50)

commit

It's now working exactly as expected:

Ball bouncing between the right and left paddle

That's a huge success, let's take a pause, and reflect on our awesomeness!

Refactoring and types

If you're like me, you feel that there's something smelly. Something fishy. Something that isn't quite right.

I mean, what's with the shouldBallBounce function and its check on the paddle's x position? Sure, there's a comment in there, which is a bit like a perfume spread on something smelly: it doesn't make the smelly thing less smelly, it just kinda hides the smell. And I've written "smell" way too often in the last couple sentences (see the code smell definition for the reference).

Instead of checking the x position of a paddle to know if it's the left or the right one, it would be very useful to declare the paddles as left or right.

In other languages, people would use something like an enum, but in elm, there's a wonderful thing called custom types which are very powerful and convenient to use:

type Paddle
    = LeftPaddle
    | RightPaddle

Actually, we need the position information (x, y, width, height) for each paddle, so it should rather be something like:

type Paddle
    = LeftPaddle { x: Int, y: Int, width: Int, height: Int}
    | RightPaddle { x: Int, y: Int, width: Int, height: Int}

By now, this thing might ring a bell to you: in the previous installment we declared another custom type (the Msg) which also encapsulated some data. It did ring a bell to you, didn't it? You're clever, and I'm proud of you. You should also be proud of yourself. And even if it didn't ring a bell to you, I'm sure it'll will in the future, and I'm proud of you all the same!

So what are those custom types? It might be one of the features of elm that I find the most difficult to put in words, even though they feel so natural now that I'm used to them.

They're composed of:

Regarding the optional data attached, you can have variants with and without those. This is how you could type an answer which is either yes, no, or other with some additional information.

type Answer
    = Yes
    | No
    | Other String

Here we used the built-in String type for the associated information, but we can use any type, including one we've defined ourselves

Oh, another cool thing, a variant can have any number of data attached to it, so we could imagine having

type Paddle
    = LeftPaddle Int Int Int Int
    | RightPaddle Int Int Int Int

But then we'd have to remember which Int is for which type of data, which would be inconvenient, more difficult to maintain, and generally seen as bad practice.

Soooooo, after all this chatter, let's

-type alias Paddle =
+type Paddle
+    = RightPaddle PaddleInfo
+    | LeftPaddle PaddleInfo
+
+
+type alias PaddleInfo =
     { x : Int
     , y : Int
     , width : Int
     , height: Int
     }

And now for arguably the best part of elm: the compiler. Let's compile this, and follow the errors that the compiler is giving us, and we'll have everything compiling and working again in a jiffy.

The first error says:

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

Something is off with the body of the `initPaddle` definition:

66|>    { x = initialX
67|>    , y = 225
68|>    , width = 10
69|>    , height = 50
70|>    }

The body is a record of type:

    { height : number, width : number1, x : Int, y : number2 }

But the type annotation on `initPaddle` says it should be:

    Paddle

Here, the compiler complains because we changed to the Paddle type, and the initPaddle function signature still says it's returning a Paddle, but it should instead be a PaddleInfo:

-initPaddle : Int -> Paddle
+initPaddle : Int -> PaddleInfo
 initPaddle initialX =
     { x = initialX
     , y = 225

The next compiler error is:

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

Something is off with the body of the `init` definition:

47|>    ( { ball = initBall
48|>      , rightPaddle = initPaddle 480
49|>      , leftPaddle = initPaddle 10
50|>      }
51|>    , Cmd.none
52|>    )

The body is a tuple of type:

    ( { ball : Ball, leftPaddle : PaddleInfo, rightPaddle : PaddleInfo }
    , Cmd msg
    )

But the type annotation on `init` says it should be:

    ( Model, Cmd Msg )

What this says is that the init function should return a ( Model, Cmd Msg ) but it returns something else instead of the Model: a record that has two fields rightPaddle and leftPaddle that are... PaddleInfo (instead of Paddle). Let's change this:

 init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { ball = initBall
-      , rightPaddle = initPaddle 480
-      , leftPaddle = initPaddle 10
+      , rightPaddle = RightPaddle <| initPaddle 480
+      , leftPaddle = LeftPaddle <| initPaddle 10
       }
     , Cmd.none
     )

As a refresher, the <| syntactic sugar means that we're taking the result of what's on the right of the "arrow", and using it as an argument for what's on the left. So we're using the PaddleInfo we're getting back from the initPaddle helper function, and "attaching" it to the Paddle variant. This could also be rewritten

RightPaddle (initPaddle 480)

The next error is:

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

This is not a record, so it has no fields to access!

113|     if paddle.x == 10 then
            ^^^^^^
This `paddle` value is a:

    Paddle

But I need a record with a x field!

This is interesting: the line 113 is in the shouldBallBounce function, which is where we started all this refactoring. We're now ready to reap the benefits:

 shouldBallBounce : Paddle -> Ball -> Bool
 shouldBallBounce paddle ball =
-    if paddle.x == 10 then
-        -- left paddle
-        (ball.x - ball.radius <= paddle.x + paddle.width)
-            && (ball.y >= paddle.y)
-            && (ball.y <= paddle.y + 50)
-
-    else
-        -- right paddle
-        (ball.x + ball.radius >= paddle.x)
-            && (ball.y >= paddle.y)
-            && (ball.y <= paddle.y + 50)
+    case paddle of
+        LeftPaddle { x, y, width, height } ->
+            (ball.x - ball.radius <= x + width)
+                && (ball.y >= y)
+                && (ball.y <= y + height)
+
+        RightPaddle { x, y, height } ->
+            (ball.x + ball.radius >= x)
+                && (ball.y >= y)
+                && (ball.y <= y + height)

"MATHIEU?! WHAT KIND OF VOODOO IS THAT, YOU TRICKED ME! I thought this was going to be an easy to follow guide, and now I feel miserable!"

I'm sorry, please bear with me for a minute while I dissect what happened here:

This new code might not be much shorter, but it's way more readable, and doesn't involve a paddle.x check against a hard coded value that we might want to change in the future. Also no more need for the comments, as it's self commenting.

And we're left with one last error:

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

This is not a record, so it has no fields to access!

152|         [ x <| String.fromInt paddle.x
                                   ^^^^^^
This `paddle` value is a:

    Paddle

But I need a record with a x field!

The fix is then:

 viewPaddle : Paddle -> Svg.Svg Msg
 viewPaddle paddle =
+    let
+        paddleInfo =
+            case paddle of
+                LeftPaddle info ->
+                    info
+
+                RightPaddle info ->
+                    info
+    in
     rect
-        [ x <| String.fromInt paddle.x
-        , y <| String.fromInt paddle.y
-        , width <| String.fromInt paddle.width
-        , height <| String.fromInt paddle.height
+        [ x <| String.fromInt paddleInfo.x
+        , y <| String.fromInt paddleInfo.y
+        , width <| String.fromInt paddleInfo.width
+        , height <| String.fromInt paddleInfo.height
         ]
         []

"MATHIEU?! YOU DID IT AGAIN!". Ok, sorry, sorry, I was being cheeky here.

The main part is this one:

    let
        paddleInfo =
            case paddle of
                LeftPaddle info ->
                    info

                RightPaddle info ->
                    info
    in

The let..in is where we assign values to names (to "variables"). Here we're assigning the attached data of the LeftPaddle or RightPaddle to the name paddleInfo (which we're using in the rest of the function code). And we're once again using a case to destructure the type. Both cases are the same because in our case both variants of the Paddle custom type have a single attached data of type PaddleInfo, but it could be different: as we explained earlier, we can mix and match any kind of variants for a given custom type.

So when we write LeftPaddle info -> info we're saying "if it's a LeftPaddle then take its attached data and assign it to the name "info", then return this "info" value" (which we then assign to the paddleInfo name in the let..in clause).

And we're now done! We have the exact same behavior, but a code that's more precise and maintainable. I agree that it might look more complex, or sometimes longer, but it gives us more flexibility, and above all, much better help and guards from the compiler, which is invaluable.

commit

Source code up to this point.

Moving the paddle

What good is a game if the player has no control whatsoever on it? It's high time that we give a way for the player to move the paddle. We'll start with the right paddle, just because.

This can't be hard, right? RIGHT? WRONG!

Sometimes elm feel complex. Complicated. Overengineered. Hard. Difficult. And that is because it's trying to do the right thing, not the simple thing. It's trying very very hard to prevent you from shooting yourself in the foot. It's helping you not have any runtime errors.

One such case are JSON decoders. And those are probably one of the major hurdles that elm beginners will face at some point, and feel overwhelmed.

Why decoders?

If they are so difficult, why bother at all? I mean, JSON is easy, right? Strings, bools, arrays, objects, numbers... how hard can it be?

Let's take a step back for a moment: if the elm compiler is going to help you have no runtime exception, it needs to be able to guarantee that there's no bad code branches. That a given field in the JSON object you're getting back is indeed a number, and not a string, or an array, or a null. Because if it can't guarantee that a value is of the proper type, it can't guarantee that the operations you do on that value are valid.

So, if the elm compiler needs to know the type of the fields in the JSON it gets, it needs a way to "decode" this JSON into proper types. And if the JSON doesn't decode properly into those types, then it'll fail in an expected way, and make you write a code branch for that case, so it doesn't end blowing up in your face at runtime.

That's where and why decoders are useful: you provide a "translation" from a untyped JSON to a typed value: if it succeeds, then you can use that typed value. If it fails, you handle this case (by displaying an error message for example).

You can see that as a way to validate the JSON that elm is receiving from the javascript land in the case of an event, or from an http request to a remote API.

So yes, decoders are hard to grasp. But if you trust my own experience, once you get to use them, you'll be missing them in other languages, and hoping there was a way to achieve the same result.

Keyboard events

There's a convenient Browser.Event module that provides event listeners for keyboard events.

Remember when we subscribed to the Browser.Events.onAnimationFrameDelta events in the previous episode? We'll do basically the same this time around.

We need to do a few things:

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

@@ -36,6 +37,7 @@ type alias PaddleInfo =

 type Msg
     = OnAnimationFrame Float
+    | KeyDown String


 type alias Flags =
@@ -107,6 +109,13 @@ update msg model =
             in
             ( { model | ball = updatedBall }, Cmd.none )

+        KeyDown keyString ->
+            let
+                _ =
+                    Debug.log "key pressed" keyString
+            in
+            ( model, Cmd.none )
+

 shouldBallBounce : Paddle -> Ball -> Bool
 shouldBallBounce paddle ball =
@@ -168,4 +177,12 @@ viewPaddle paddle =

 subscriptions : Model -> Sub Msg
 subscriptions _ =
-    Browser.Events.onAnimationFrameDelta OnAnimationFrame
+    Sub.batch
+        [ Browser.Events.onAnimationFrameDelta OnAnimationFrame
+        , Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
+        ]
+
+
+keyDecoder : Decode.Decoder String
+keyDecoder =
+    Decode.field "key" Decode.string

commit

We used Decode.map here to attach the string we get back from the decoder to the KeyDown variant... but only if the keyDecoder succeeded!

Decode.map says "if the decoder succeeds, apply this function to the result". And we needed to do that because the onKeyDown event listener needs a Decoder Msg and not a Decoder String. In order to be able to subscribe to the events, the listener needs to know which message to call when it receives an event.

If you followed along, you'll notice a compiler error:

-- UNKNOWN IMPORT ------------------------------------------------- src/Main.elm

The Main module has a bad import:

    import Json.Decode

Do you want the one from the elm/json package? If so, run this command to add
that dependency to your elm.json file:

    elm install elm/json

If you want a local file, make sure the `Json.Decode` module is in one of the
"source-directories" listed in your elm.json file.

This very helpful message tells us that we need to add the missing dependency to the elm.json file, just like we did with elm/svg in the previous post. Running the elm install elm/json command result in the following changes in the elm.json file:

             "elm/browser": "1.0.1",
             "elm/core": "1.0.2",
             "elm/html": "1.0.0",
+            "elm/json": "1.1.3",
             "elm/svg": "1.0.1"
         },
         "indirect": {
-            "elm/json": "1.1.3",
             "elm/time": "1.0.0",
             "elm/url": "1.0.0",
             "elm/virtual-dom": "1.0.2"

It moved the elm/json dependency from the indirect dependencies to the direct ones.

In the previous post we already used the Debug.log helper, so let's take a minute to explain what that does: it's a function takes any argument (in our case the keyString) and displays it in the browser console, preceeded by the message you provide it (eg key pressed).

This is the result of pressing a few keys in the console:

Debug.log messages in the console

The Debug.log helper also returns exactly what it received, so you can add it anywhere, it's very convenient!

However, be aware that it's not possible to use the elm compiler optimization with a Debug.xxx call, so they need to be removed before you compile for production.

In our case, we wanted to output a debug message to the console, but not do anything else. A common pattern is to use a let..in to fake a variable assignment, but not care about the resulting variable (hence the _ = ...). As we're not doing anything with the keyboard event (yet), we're also returning the exact same model we received in the update function, and send no commands: ( model, Cmd.none).

Decoding arrow key presses

There's a very convenient link in the onKeyDown documentation that brings us straight to something of great interest to us: Decoding for games.

As explained, it would make a lot of sense and give us some guarantees to use a custom type (that we could call PlayerAction):

 type Msg
     = OnAnimationFrame Float
-    | KeyDown String
+    | KeyDown PlayerAction
+
+
+type PlayerAction
+    = RightPaddleUp
+    | RightPaddleDown

Notice here that we didn't add an Other variant, because we'll be doing something else with the decoder... Event listeners in elm have a nice (and sometimes confusing?) behavior: whenever the decoder that they're given fails decoding, the event is simply discarded. Which means that no Msg is sent. It's as if the program simply didn't subscribe to those.

So we're going to modify our decoder to pass the resulting decoded string to another decoder using the Decode.andThen helper. And from this second decoder, we're going to Decode.succeed with one of the PlayerAction variants, or Decode.fail, in which case the event will be discarded.

-keyDecoder : Decode.Decoder String
+keyDecoder : Decode.Decoder PlayerAction
 keyDecoder =
     Decode.field "key" Decode.string
+        |> Decode.andThen keyToPlayerAction
+
+
+keyToPlayerAction : String -> Decode.Decoder PlayerAction
+keyToPlayerAction keyString =
+    case keyString of
+        "ArrowUp" ->
+            Decode.succeed RightPaddleUp
+
+        "ArrowDown" ->
+            Decode.succeed RightPaddleDown
+
+        _ ->
+            Decode.fail "not an event we care about"

commit

Decode.andThen is a bit different than Decoder.map: it also takes the result of a successful decoder, but returns a decoder (not a result). Which means that we can modify a decoder's behavior and make it fail or succeed instead of just modifying its successful result. In our case we make it fail if it's not one of the keys we're interested in... even though the keyDecoder would successfully decode a string from the event.

Neat, isn't it? Are you starting to like decoders? Love them even? I do.

Moving the right paddle

Now that we can detect player actions, we can react to them, and update the paddle position:

-        KeyDown keyString ->
-            let
-                _ =
-                    Debug.log "key pressed" keyString
-            in
-            ( model, Cmd.none )
+        KeyDown playerAction ->
+            case playerAction of
+                RightPaddleUp ->
+                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle -10 }
+                    , Cmd.none
+                    )
+
+                RightPaddleDown ->
+                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle 10 }
+                    , Cmd.none
+                    )
+
+
+updatePaddle : Int -> Paddle -> Paddle
+updatePaddle amount paddle =
+    case paddle of
+        RightPaddle paddleInfo ->
+            { paddleInfo | y = paddleInfo.y + amount }
+                |> RightPaddle
+
+        LeftPaddle paddleInfo ->
+            { paddleInfo | y = paddleInfo.y + amount }
+                |> LeftPaddle

commit

As you can see, we used a small helper function updatePaddle to update the info of a paddle, moving it up or down by a certain amount of pixels.

Moving the left paddle

Now that we have everything in place for the right player, it's straightforward to deal with the left player. We'll use the keys "e" for up, and "d" for down to cope with both azerty and qwerty keyboards. I'm sorry if this doesn't make sense for your keyboard, but I'm sure you'll be able to fix it by changing those keys in your code ;)

Let's do that step by step with the help from the lovely compiler:

 type PlayerAction
     = RightPaddleUp
     | RightPaddleDown
+    | LeftPaddleUp
+    | LeftPaddleDown

We have one compiler error:

-- MISSING PATTERNS ----------------------------------------------- src/Main.elm

This `case` does not have branches for all possibilities:

120|>            case playerAction of
121|>                RightPaddleUp ->
122|>                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle -10 }
123|>                    , Cmd.none
124|>                    )
125|>
126|>                RightPaddleDown ->
127|>                    ( { model | rightPaddle = model.rightPaddle |> updatePaddle 10 }
128|>                    , Cmd.none
129|>                    )

Missing possibilities include:

    LeftPaddleUp
    LeftPaddleDown

I would have to crash if I saw one of those. Add branches for them!

Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.0/missing-patterns> for more
guidance on this workflow.

That's an easy one:

                     , Cmd.none
                     )

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

 updatePaddle : Int -> Paddle -> Paddle
 updatePaddle amount paddle =

The thing is, once we fixed that the compiler compiles successfully... but pressing the "e" or "d" keys has no effect whatsoever... elm compiler, why have you failed me!!!!!

Remember when we talked about the (sometimes confusing) behavior of event listeners in elm that are silently ignored if the decoder fails? And remember that we used that to our advantage to reduce the code a bit instead of dealing with an Other variant for the PlayerAction custom type?

Well here we are now: it's up to us to remember that we need to update the decoder:

         "ArrowDown" ->
             Decode.succeed RightPaddleDown

+        "e" ->
+            Decode.succeed LeftPaddleUp
+
+        "d" ->
+            Decode.succeed LeftPaddleDown
+
         _ ->
             Decode.fail "not an event we care about"

commit

Source code up to this point.

Well, it seems we have a mostly working pong game. Both players can move their paddles up or down. We would still have a LOOOOOONG way to go to make it a real, playable and enjoyable game though.

We did 80% of the work in 20% of the time. We now need 80% of the time to finish the remaining 20% of the work ;)

Some of the things that are blatantly missing:

Some ideas to make it more enjoyable:

So what did you think? Did this give you a feel for the elm language? Did it make you want to give it a go for gamedev or webdev?

I would be curious to see if you come up with completed/improved games of your own!


There's now a follow up.