Mathieu Agopian : Making a pong game in elm

Let's make a pong game in elm, by taking tiny steps.

If you haven't already, the very first step is to install elm, and to keep in mind that the folks on the slack channel are very friendly and helpful if you ever face an issue that you can't manage to solve on your own. You are not alone!

Create the project

$ elm init

This creates a elm.json file for you, and an empty src/ folder. Running elm make will compile the project and tell you:

$ elm make
Dependencies loaded from local cache.
Dependencies ready!
-- NO INPUT --------------------------------------------------------------------

What should I make though? I need more information, like:

    elm make src/Main.elm
    elm make src/This.elm src/That.elm

However many files you give, I will create one JS file out of them.

Ok, so let's create a very basic src/Main.elm file:

module Main exposing (main)

import Html


main =
    Html.text "Hello world"

Running elm make src/Main.elm will compile successfully and create an index.html file for us. Opening it in the browser displays a very dull Hello world message. Dull, for sure, but still a success!

Source code up to this point.

Display a ball

We're going to use the elm/svg, so we need to install it:

elm install elm/svg

Here's the result in the elm.json file once elm added the dependency for us:

         "direct": {
             "elm/browser": "1.0.1",
             "elm/core": "1.0.2",
-            "elm/html": "1.0.0"
+            "elm/html": "1.0.0",
+            "elm/svg": "1.0.1"
         },
         "indirect": {
             "elm/json": "1.1.3",

Once that's done, let's first draw a playing field (an empty light gray box), by changing our src/Main.elm file to:

module Main exposing (main)

import Svg exposing (..)
import Svg.Attributes exposing (..)


main =
    svg
        [ width "500"
        , height "500"
        , viewBox "0 0 500 500"
        , Svg.Attributes.style "background: #efefef"
        ]
        [ circle
            [ cx "250"
            , cy "250"
            , r "10"
            ]
            []
        ]

commit

This displays a 500px by 500px light gray rectangle, with a 10px circle right in the center:

the pong ball

Let's pull this circle out and make a viewBall function:

         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ circle
-            [ cx "250"
-            , cy "250"
-            , r "10"
-            ]
-            []
+        [ viewBall
         ]
+
+
+viewBall : Svg msg
+viewBall =
+    circle
+        [ cx "250"
+        , cy "250"
+        , r "10"
+        ]
+        []

commit

This doesn't give us much, yet. Now what's our next tiny step? Well, to display a ball, the function only needs its coordinates:

         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ viewBall
+        [ viewBall 250 250
         ]


-viewBall : Svg msg
-viewBall =
+viewBall : Int -> Int -> Svg.Svg msg
+viewBall x y =
     circle
-        [ cx "250"
-        , cy "250"
+        [ cx <| String.fromInt x
+        , cy <| String.fromInt y
         , r "10"
         ]
         []

commit

Here we started using integers for the positions (a number of pixels), instead of strings. This is because we will need to do some mathematics on the position, to get the ball moving. So we'll pass integers around, store them in our state, and at the last moment we'll translate them back to strings.

Talking about state, let's have a Ball type alias for a record holding the position:

 import Svg.Attributes exposing (..)


+type alias Ball =
+    { x : Int
+    , y : Int
+    }
+
+
+ball =
+    { x = 250
+    , y = 250
+    }
+
+
 main =
     svg
         [ width "500"
         , height "500"
         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ viewBall 250 250
+        [ viewBall ball
         ]


-viewBall : Int -> Int -> Svg.Svg msg
-viewBall x y =
+viewBall : Ball -> Svg.Svg msg
+viewBall { x, y } =
     circle
         [ cx <| String.fromInt x
         , cy <| String.fromInt y

commit

Source code up to this point.

Move the ball

Well, the ball isn't worth much if it's not moving, so let's do that. But let's pause for a moment, how would we achieve that?

In javascript we'd probably use setInterval, or setTimeout, and move the ball by a small amount every 16ms or so (which would give us 60fps: 1000ms / 16ms = 62.5 frames per second). Or we could use requestAnimationFrame which is an even better solution.

In elm we have the onAnimationFrameDelta event that we can subscribe to, which gives us the number of milliseconds elapsed since the previous animation frame. This way we can

  1. animate the ball as smoothly as possible
  2. move the ball by the proper amount, computed with the elapsed time between two frames

To subscribe to browser events we first need to change our program to be embedded as a Browser.element.

Let's do that a tiny step at a time. First let's extract the svg into its own view:

 main =
+    view ball
+
+
+view : Ball -> Svg.Svg ()
+view ball_ =
     svg
         [ width "500"
         , height "500"
         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ viewBall ball
+        [ viewBall ball_
         ]

commit

Now let's actually change the main to be a Browser.element. At the top of the file:

 module Main exposing (main)

+import Browser
 import Svg exposing (..)
 import Svg.Attributes exposing (..)

And below:

     }


+main : Program () () ()
 main =
-    view ball
+    Browser.element
+        { init = \_ -> ( (), Cmd.none )
+        , view = \_ -> view ball
+        , update = \_ _ -> ( (), Cmd.none )
+        , subscriptions = \_ -> Sub.none
+        }


 view : Ball -> Svg.Svg ()

commit

Well, that's an awful lot of empty parens (). Those are of the unit type in elm. They are just placeholders for now, as we're going to need some model, flags, messages...

Also, what's with all those \_ ->? That's how we write anonymous functions. And the _ simply means we don't care about that argument's value.

So the anonymous function provided for the element's update \_ _ -> ( (), Cmd.none ) can be written:

someFunction someArg someOtherArg = ( (), Cmd.none )

Let's start with a Model: this is, by convention in elm, the name of the state, the place were we store all the data needed by our system: the ball position, the paddles, the score, you name it. For now the only piece of data we have is the ball position, so our model can simply be a type alias to it.

The Model goes near the top of the file by convention:

 import Svg.Attributes exposing (..)


+type alias Model =
+    Ball
+
+
 type alias Ball =
     { x : Int
     , y : Int

And the main:

     }


-main : Program () () ()
+main : Program () Model ()
 main =
     Browser.element
-        { init = \_ -> ( (), Cmd.none )
-        , view = \_ -> view ball
-        , update = \_ _ -> ( (), Cmd.none )
+        { init = \_ -> ( ball, Cmd.none )
+        , view = view
+        , update = \_ model -> ( model, Cmd.none )
         , subscriptions = \_ -> Sub.none
         }


-view : Ball -> Svg.Svg ()
-view ball_ =
+view : Model -> Svg.Svg ()
+view model =
     svg
         [ width "500"
         , height "500"
         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ viewBall ball_
+        [ viewBall model
         ]

commit

Let's have a proper initialization function now: given some flags (none in our case, so we'll just keep the unit for now), it generates the initial model and commands (and we'll use Cmd.none for now):

     }


-ball =
-    { x = 250
-    , y = 250
-    }
+init : () -> ( Model, Cmd () )
+init _ =
+    ( { x = 250
+      , y = 250
+      }
+    , Cmd.none
+    )


 main : Program () Model ()
 main =
     Browser.element
-        { init = \_ -> ( ball, Cmd.none )
+        { init = init
         , view = view
         , update = \_ model -> ( model, Cmd.

commit

Ok, we should be mostly set up to receive events from the browser, and in particular the animation frames. A few missing pieces: we need to subscribe to those events, and we need to provide a message to the subscription, which will act as a kind of callback. The elm runtime will call our future update function with this message and our current model, to allow us to update the model. This updated model will then be passed down the view function to update what we see on the screen.

Let's define a Msg type (this is the conventional name used in elm):

     }


+type Msg
+    = OnAnimationFrame Float
+
+
 init : () -> ( Model, Cmd () )
 init _ =
     ( { x = 250

commit

This is a custom type named OnAnimationFrame which takes (includes? encapsulates? wraps? boxes?) a float which is the number of milliseconds since the previous animation frame.

We can now use this Msg type everywhere we used the unit previously...

In the init type definition:

     = OnAnimationFrame Float


-init : () -> ( Model, Cmd () )
+init : () -> ( Model, Cmd Msg )
 init _ =
     ( { x = 250
       , y = 250

In the main type definition:

     )


-main : Program () Model ()
+main : Program () Model Msg
 main =
     Browser.element
         { init = init

In the view type definition:

         }


-view : Model -> Svg.Svg ()
+view : Model -> Svg.Svg Msg
 view model =
     svg
         [ width "500"

And in the viewBall type definition:

         ]


-viewBall : Ball -> Svg.Svg msg
+viewBall : Ball -> Svg.Svg Msg
 viewBall { x, y } =
     circle
         [ cx <| String.fromInt x

commit

We can now subscribe to the event.

First add the Browser.Events import at the top of the file:

 module Main exposing (main)

 import Browser
+import Browser.Events
 import Svg exposing (..)
 import Svg.Attributes exposing (..)

Then use a subscriptions function in the main:

         { init = init
         , view = view
         , update = \_ model -> ( model, Cmd.none )
-        , subscriptions = \_ -> Sub.none
+        , subscriptions = subscriptions
         }

Then add the subscriptions function at the bottom of the file:

         , r "10"
         ]
         []
+
+
+subscriptions : Model -> Sub Msg
+subscriptions _ =
+    Browser.Events.onAnimationFrameDelta OnAnimationFrame

commit

If you compiled your elm files with the --debug option for elm make, you should see many many messages being received in the time traveller debug window:

OnAnimationFrame messages being listed in the time travel debugger

Let's finally add a (mostly empty) update function, and add a Flags type alias on the unit type to clean things up a bit:

 type Msg
     = OnAnimationFrame Float


-init : () -> ( Model, Cmd Msg )
+type alias Flags =
+    ()
+
+
+init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { x = 250
       , y = 250
       }
     , Cmd.none
     )


-main : Program () Model Msg
+main : Program Flags Model Msg
 main =
     Browser.element
         { init = init
         , view = view
-        , update = \_ model -> ( model, Cmd.none )
+        , update = update
         , subscriptions = subscriptions
         }


+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    ( model, Cmd.none )
+
+
 view : Model -> Svg.Svg Msg
 view model =
     svg

commit

Source code up to this point.

We can be proud of ourselves: we changed a lot of code, but nothing changed visually: the ball still isn't moving! Promise, we're moving this ball next ;)

Move the ball (for real)

Now that we have everything in place, moving the ball is simply a matter of changing the x or y coordinates. And where better to do that than on each animation frame? We already have a subscription that fires an "event" (a message in elm's vocabulary). We also have an update function that is called with those messages, allowing us to return a new (and updated) model.

Yes, we need to return a new model because in elm everything is immutable: you don't change a model, you don't mutate it, you simply create a new one:

 update : Msg -> Model -> ( Model, Cmd Msg )
 update msg model =
-    ( model, Cmd.none )
+    case msg of
+        OnAnimationFrame timeDelta ->
+            ( { model | x = model.x + 4 }, Cmd.none )


 view : Model -> Svg.Svg Msg

commit

Source code up to this point.

Instead of always returning the same model we were passed in the update function, we now add 4 pixels to the x coordinates of the ball.

The { foo | bar = crux } notation is syntactic sugar to create a new record from the content of the foo record, but with the bar field set to the new crux value.

Once compiled and the browser tab refreshed, you should see the mighty ball moving rather quickly towards the right (and then disappearing!).

Now that the ball is moving, let's set us up quickly with some better tooling.

Tooling

At this point, you should start getting tired of running elm make, then switching to the browser, refreshing the page... modifying the code, and then starting over. Those few steps can quickly become tedious, and that's why most elm developers take advantage of:

Live reloading

Live reloading is automatically re-compiling the code whenever a file changes, and then automatically refreshing the browser tab. There are a few tools available that I know of:

They all offer some kind of web server that injects some javascript in the page, and then whenever a file changes, recompile the project, and communicate with the loaded web page using a websocket so it reloads. I've used all three in various projects, and they all have their pros and cons. I find elm-live to be one of the smallest and simplest to use, but really do feel free to pick one (but do pick one, it's really worth it ;)

Auto code formatting

Another tedious task is indenting and formatting the code properly. In elm indentation matters, and as in any written code, readability is important. The elm community has very broadly adopted a common code formatting tool that gives us a way to all share a same code style, which is a real blessing.

It also appears that once you stop caring about a code style, and let the machine do it for you, it really frees your mind from this chore, and removes all the bikeshedding and useless discussions and trolls.

This code formatting tool is elm-format, and can be configured to automatically reformat the code on save in most editors.

With those two tools installed and set up, let us continue with our game: adding a paddle!

Adding a paddle

The right paddle will be a simple rectangle (for now at least), 10 pixels wide, 50 pixels high, 10 pixels from the right border, and will start at the middle.

This makes the starting paddle coordinates:

         , Svg.Attributes.style "background: #efefef"
         ]
         [ viewBall model
+        , rect
+            [ x "480"
+            , y "225"
+            , width "10"
+            , height "50"
+            ]
+            []
         ]

commit

Behold the mighty paddle!

Right paddle with the ball moving towards it

One problem though... the ball isn't boucing off of it. What good is a paddle that doesn't bounce?

But first, what does "bouncing" mean? In our case, bouncing of a vertical paddle means changing the horizontal direction of the ball. But we don't have a proper "direction" yet. For now, we only need a horizontal "direction" (or speed, or vector):

 type alias Ball =
     { x : Int
     , y : Int
+    , horizSpeed : Int
     }


@@ -28,6 +29,7 @@ init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { x = 250
       , y = 250
+      , horizSpeed = 4
       }
     , Cmd.none
     )
@@ -47,7 +49,7 @@ update : Msg -> Model -> ( Model, Cmd Msg )
 update msg model =
     case msg of
         OnAnimationFrame timeDelta ->
-            ( { model | x = model.x + 4 }, Cmd.none )
+            ( { model | x = model.x + model.horizSpeed }, Cmd.none )


 view : Model -> Svg.Svg Msg

commit

In case you're wondering what those @@ -28,6 +29,7 @@ init : Flags -> ( Model, Cmd Msg ) lines mean: that's the diff unified format telling us where the line is located: it's line 28, and it gives us a bit of context: it's in the init function.

What we did here was to extract the number of pixels we were adding to the x position of the ball into a horizSpeed state in the model.

Now that we have a horizontal speed, boucing (horizontally) simply means "reversing" its value, which is achieved by changing its sign. If we were adding 4 pixels per frame and bounced of the right paddle, we'd now need to substract 4 pixels per frame to the x coordinate.

Before going further, let's do some cleanup and slight reorganization:

 type alias Ball =
     { x : Int
     , y : Int
+    , radius : Int
     , horizSpeed : Int
     }

@@ -29,6 +30,7 @@ init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { x = 250
       , y = 250
+      , radius = 10
       , horizSpeed = 4
       }
     , Cmd.none
@@ -72,11 +74,11 @@ view model =


 viewBall : Ball -> Svg.Svg Msg
-viewBall { x, y } =
+viewBall { x, y, radius } =
     circle
         [ cx <| String.fromInt x
         , cy <| String.fromInt y
-        , r "10"
+        , r <| String.fromInt radius
         ]
         []

commit

 type alias Model =
-    Ball
+    { ball : Ball
+    }


 type alias Ball =
@@ -28,15 +29,22 @@ type alias Flags =

 init : Flags -> ( Model, Cmd Msg )
 init _ =
-    ( { x = 250
-      , y = 250
-      , radius = 10
-      , horizSpeed = 4
+    ( { ball =
+            initBall
       }
     , Cmd.none
     )


+initBall : Ball
+initBall =
+    { x = 250
+    , y = 250
+    , radius = 10
+    , horizSpeed = 4
+    }
+
+
 main : Program Flags Model Msg
 main =
     Browser.element
@@ -51,18 +59,22 @@ update : Msg -> Model -> ( Model, Cmd Msg )
 update msg model =
     case msg of
         OnAnimationFrame timeDelta ->
-            ( { model | x = model.x + model.horizSpeed }, Cmd.none )
+            let
+                ball =
+                    model.ball
+            in
+            ( { model | ball = { ball | x = ball.x + ball.horizSpeed } }, Cmd.none )


 view : Model -> Svg.Svg Msg
-view model =
+view { ball } =
     svg
         [ width "500"
         , height "500"
         , viewBox "0 0 500 500"
         , Svg.Attributes.style "background: #efefef"
         ]
-        [ viewBall model
+        [ viewBall ball
         , rect
             [ x "480"
             , y "225"

commit

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


@@ -19,6 +20,14 @@ type alias Ball =
     }


+type alias Paddle =
+    { x : Int
+    , y : Int
+    , width : Int
+    , height : Int
+    }
+
+
 type Msg
     = OnAnimationFrame Float

@@ -31,6 +40,7 @@ init : Flags -> ( Model, Cmd Msg )
 init _ =
     ( { ball =
             initBall
+      , paddle = initPaddle
       }
     , Cmd.none
     )
@@ -45,6 +55,15 @@ initBall =
     }


+initPaddle : Paddle
+initPaddle =
+    { x = 480
+    , y = 225
+    , width = 10
+    , height = 50
+    }
+
+
 main : Program Flags Model Msg
 main =
     Browser.element
@@ -70,7 +89,7 @@ update msg model =


 view : Model -> Svg.Svg Msg
-view { ball } =
+view { ball, paddle } =
     svg
         [ width "500"
         , height "500"
@@ -78,13 +97,7 @@ view { ball } =
         , Svg.Attributes.style "background: #efefef"
         ]
         [ viewBall ball
-        , rect
-            [ x "480"
-            , y "225"
-            , width "10"
-            , height "50"
-            ]
-            []
+        , viewPaddle paddle
         ]


@@ -98,6 +111,17 @@ viewBall { x, y, radius } =
         []


+viewPaddle : Paddle -> Svg.Svg Msg
+viewPaddle paddle =
+    rect
+        [ x <| String.fromInt paddle.x
+        , y <| String.fromInt paddle.y
+        , width <| String.fromInt paddle.width
+        , height <| String.fromInt paddle.height
+        ]
+        []
+
+
 subscriptions : Model -> Sub Msg
 subscriptions _ =
     Browser.Events.onAnimationFrameDelta OnAnimationFrame

commit

We're now ready to detect the bounce, and reverse the direction the ball is moving.

Source code up to this point.

Bouncing the ball off the paddle

The ball should bounce off the paddle, which is when the ball "touches" the paddle. That means that the ball should bounce (reverse direction) when both those conditions are met:

Let's make a helper function for that, which given a ball and a paddle will return True if the ball should bounce, and display the result of that check in each animation frame in the console:

                 ball =
                     model.ball

+                shouldBounce =
+                    shouldBallBounce model.paddle model.ball
+                        |> Debug.log "shouldBounce"
+
                 updatedBall =
                     { ball | x = ball.x + ball.horizSpeed }
             in
             ( { model | ball = updatedBall }, Cmd.none )


+shouldBallBounce : Paddle -> Ball -> Bool
+shouldBallBounce paddle ball =
+    (ball.x + ball.radius >= paddle.x)
+        && (ball.y >= paddle.y - 50 // 2)
+        && (ball.y <= paddle.y + 50 // 2)
+
+
 view : Model -> Svg.Svg Msg
 view { ball, paddle } =
     svg

commit

When running the code, you should see shouldBounce: False displayed several times per second until the ball reaches the right paddle, and the message then displaying shouldBounce: True.

It works! We can now use this to update the ball's horizontal speed (its direction) according to the check:

                 shouldBounce =
                     shouldBallBounce model.paddle model.ball
-                        |> Debug.log "shouldBounce"
+
+                horizSpeed =
+                    if shouldBounce then
+                        ball.horizSpeed * -1
+
+                    else
+                        ball.horizSpeed

                 updatedBall =
-                    { ball | x = ball.x + ball.horizSpeed }
+                    { ball
+                        | x = ball.x + horizSpeed
+                        , horizSpeed = horizSpeed
+                    }
             in
             ( { model | ball = updatedBall }, Cmd.none )

commit

Such joy, a mighty ball boucing off a glorious paddle! We're so good! The world is ours! This feeling is why I became a developer in the first place. Feeling invincible, powerful, knowledgeable.

The horror

And at the same time, there's a nagging feeling in the back of my head: how can I make sure that the code is working properly? I mean, most of it is very straightforward, not much complexity. But maybe the shouldBallBounce which seems a bit cryptic.

To make the doubt go away, let's try a few different starting positions for the ball: if it starts at 10, everything seems fine (it doesn't bounce and disappears off screen as expected). If it starts at 400, same thing. What about 200? OH NOES!!!!!!11!1!! It bounces back, even though it's very clearly way above the paddle! Wait, what about 260? OH NOES!!!!!!11!1!! It goes right through the paddle!

 initBall : Ball
 initBall =
     { x = 250
-    , y = 250
+    , y = 260
     , radius = 10
     , horizSpeed = 4
     }

the horror

commit

Ok, I'm so bad. I'm worthless. I know nothing about programming. I should stop and switch jobs. Maybe become a shepherd or something.

But first, let's try to find the issue here. I'm glad we tried a few different cases, and caught the issue early, it should be easier to deal with. By the way, that's why people like testing their code with corner cases (either using automated tests, or manual ones).

Let's look at the shouldBallBounce code again:

shouldBallBounce : Paddle -> Ball -> Bool
shouldBallBounce paddle ball =
    (ball.x + ball.radius >= paddle.x)
        && (ball.y >= paddle.y - 50 // 2)
        && (ball.y <= paddle.y + 50 // 2)

Mmmmm... it seems we dealt with the paddle as if its y position was in the center of the paddle. But in SVG, it's the top left of the rectangle! That's why we had to computer the center to display it vertically centered in the game window.

So the correct code should instead be:

 shouldBallBounce : Paddle -> Ball -> Bool
 shouldBallBounce paddle ball =
     (ball.x + ball.radius >= paddle.x)
-        && (ball.y >= paddle.y - 50 // 2)
-        && (ball.y <= paddle.y + 50 // 2)
+        && (ball.y >= paddle.y)
+        && (ball.y <= paddle.y + 50)


 view : Model -> Svg.Svg Msg

commit

Source code up to this point.

This looks way better, if the starting y position of the ball is between 225 and 275 it properly bounces.

We still have the corner case of the ball's center grazing off the paddle (so the ball's y position being for example 224 or 276): the center isn't within the paddle, but the ball's radius being 10 pixels, there's still 9 pixels of the ball clipping through the paddle. We could decide that we instead want the ball to bounce off, but it then looks weird because the hitbox of the ball is a square (we check on the x and the y, not the shortest distance between the ball and the paddle which could be in diagonal.

Anyway, let's decide that this is good enough for now, and an improvement to make in the future!

Also, let's feel good about ourselves again, and maybe even more: we were clever and persistent enough to investigate the bug and fix it! Maybe we can still be working in a programming job after all.

(Yes, this kind of ups and downs are very very common, at least in my case).

Now draw the rest of the owl

(For the reference to this meme, check How to draw an owl)

We've come a long way, but we're still a long way from having a finished pong game! This post being long enough as it is, let's wrap it here.

I might publish a follow up some day, please let me know via email or twitter if you'd be interested.

I've tried something different with the code snippets in this blog post, using the diff format. Did it make it easier to follow? Or worse? It does have the main drawback of not having the elm syntax highlighting. But on the other hand it shows exactly what changes between two code excerpts.

What do you think?


There's now a follow up.