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.