Elm is one of the many languages that compile to javascript. It's a statically typed purely functional language. At first glance, I enjoyed the idea and syntax, so I thought I'd give it a try. I like writing the snake game because it gives me a chance to see many facets of the language. Check out the finished product!

What's it look like?

To output the html

<div class="container">
    <p>Hello World</p>
</div>

Your elm code would look like this

div [class "container"] [ p [] [ text "Hello World" ] ]

Both div and p are functions in elm. They take two arrays as their parameters. The first array is attributes, and the second is inner html. If the element contains no more child elements and only text (like p above) you can use the text function to do that. This small example is how html is formed with Elm. If you'd like to see more elm, there are plenty of examples online.

Architecture

Elm has a fairly opinionated architecture, as in you're strongly recommended to do things the Elm way. Each elm application generally has a Model, View, Message (type) and Update. Model and view are pretty self explainatory - model contains the state of your application and view composes the html structure. The Message contains all the types of actions which can be done on the application. These actions come from the user or external things like a job that occurs every 10 minutes or a websocket receiving a message. Finally, the Update is a function which handles each type of message.

Model

type alias Model =
  { snake : List Point -- last item in list is snake head
  , currentDirection : Direction
  , nextDirections : List Direction -- last item in list is next direction
  , foods : List Point
  , gameState : GameState
  , options : Options
  , score : Int
  }

Let's see. There are two types here that aren't obvious - GameState which is one of three choices: Initializing, Playing or GameOver. This tells my view which view to render and some other things. Options is a type which stores the difficulty and game board size. The user can pick these before each game. It's a pretty basic and intuitive model which I appreciate. I'm using a queue for nextDirections so when the user quickly presses a combination of buttons, those direction changes can happen over the following ticks.

Message

type Msg
  = Tick Time
  | KeyDown KeyCode
  | NewFood Int Int
  | StartGame
  | DifficultyChanged Difficulty
  | GameBoardSizeChanged Int
  | ChangeDirection Direction

Here is the Message type which can be any of the possibilities listed. I believe these are called Union Types in Elm and they're sort of like an ultra powerful Enum in other languages.

  • The Tick message happens when the game advances. This gets called every 100-180 milliseconds depending on difficulty.
  • KeyDown is pretty obvious - when a user presses a button
  • NewFood is an event that gets raised when the random number generator finishes and a new food gets added to the board
  • StartGame gets called when the game starts
  • DifficultyChanged listens to the difficulty combo box
  • GameBoardSizeChanged listens to the game board size combo box
  • ChangeDirection is used for whenever the user wants the snake to change directions

View

The view is much to large to include the majority of it. Here's one section which draws a rectangle (rect) with the appropriate fill color in the SVG given an X and Y coordinate.

cellView : Model -> Int -> Int -> Maybe (Svg Msg)
cellView model currentY currentX =
  let
    cellWidth = (model.options.gameBoardSize |> columnWidth)
    cellHeight = (model.options.gameBoardSize |> rowHeight)
    pixelsX = (toFloat currentX) * totalCellWidth
    pixelsY = (toFloat currentY) * totalCellHeight
    currentPoint = Point currentX currentY
  in
    case determineCellState model currentPoint of
      Snake ->
        Just ( rect
          [ pixelsX |> floor |> toString |> x
          , pixelsY |> floor |> toString |> y
          , cellWidth |> ceiling |> toString |> width
          , cellHeight |> ceiling |> toString |> height
          , fill (cellFillColor Snake)
          ] [] )
      Food ->
        Just ( rect
          [ pixelsX |> toString |> x
          , pixelsY |> toString |> y
          , cellWidth |> toString |> width
          , cellHeight |> toString |> height
          , fill (cellFillColor Food)
          ] [] )
      Empty -> Nothing

I dont' think it's worth going line by line through this code, but basically it calculates the pixel coordinates for the rectangle, then draws a rectangle with the color determined by determineCellState and cellFillColor. Depending on determineCellState this function may not draw a rectangle, because there's no need to draw any rectangle if there is no food or snake segment at that point. Elm doesn't have the concept of null (hooray!), so I'm using Maybe to accomplish this.

The two states which actually draw rectangles (Snake and Food) are somewhat repetative, so it'd probably be good to extract the actual drawing of a rectangle into it's own method, but it'd be pretty boring and logic-less, so I didn't.

The rest of the View can be seen here.

Update

The Update portion of the app is also pretty large because it contains all the logic of the game. Lets look at a function which checks if the snake head has a collision with itself or the wall causing a game over.

detectDeath : Maybe Point -> List Point -> Int -> Bool
detectDeath snakeHead snake gameBoardSize =
  case snakeHead of
    Just sh ->
      let
        outsideGameBoard = sh.x < 0 || sh.y < 0 || sh.x >= gameBoardSize || sh.y >= gameBoardSize
        snakeTail = List.take ((List.length snake) - 1) snake
        eatSelf = List.any (\seg -> seg == sh) snakeTail
      in
        outsideGameBoard || eatSelf
    Nothing -> False

This function unintuitively takes in the snake and the snakeHead. The reason for this is snake is a linked list, and snakeHead is the last member in that list. So it's a bit expensive to get snakeHead in multiple places. So I grab it once in the calling method and use it multiple times. This function checks if the snakeHead is outside the game board (represented by outsideGameBoard) or if the snake has eaten itself. If either of these are true, then it is game over.

The rest of Update can be seen here.

Deploying

I used GitLab Pages for deployment. Since the compiled output of this project is static javascript, it can be hosted as a static web page. There weren't many posts on using GitLab Pages with Elm, so I had to do a little experimenting, but eventually I got it to a very slim CI configuration.

image: node:latest

cache:
  paths:
    - elm-stuff

before_script:
  - npm install --unsafe-perm -g elm
  - elm --version
  - elm-package install --yes

pages:
  script:
    - elm-make Main.elm --output public/app.js
  artifacts:
    paths:
    - public/
  only:
    - master

It grabs the node container, installs elm, installs elm packages for this project, then builds the project placing app.js in the public directory. For GitLab Pages the public directory is exposed, and that's all it takes! Now, every time I push to my repository, the newest version of the site is built and deployed. Slick, short and sweet. My experience with GitLab and GitLab Pages has been superb.

Thanks for reading! Try out Elm if you like the syntax, the strong typing, or separation of concerns brought about by the app architecture. I enoyed it.