In this post I will be creating a small game, 2048 in Elm. 2048 is a game created by gabrielecirulli which went viral in 2014.

I chose 2048 because the game is simple which will help me/us understand Elm clearly. Also, I didn’t want to make another todo app.

You can play here.

Whats the game?

  • There are some number tiles on the board (4x4). The number on the tile will be a power of 2 with minimum being 2. See below image: 2048
  • You can press arrow keys: up, left, down, and right. This will trigger “merge” in the direction of the key-press. “merge” is the process of merging same number tiles into one and doubling its value. See below gif.
  • The score is calculated by sum of number on tiles on the board, so you play to maximise your score by merging as many tiles as possible.
  • A new tile (number 2) is introduced in the game at any random empty space with every key-press
  • Game is over when no new tiles can be added, thats your final score (highest).

Simple eh!

The Elm Architecture

Every Elm application follows the Elm Architecture, which is pretty straightforward.

  • Model — the state of your application
  • View — a way to turn your state into HTML
  • Update — a way to update your state based on messages

We won’t deep dive into each of them, you can read more about Elm here.

Lets dive into building the game.

Building the game in Elm

Shaping Elm Architecture

Based on the game’s functioning, let’s start defining the architecture of our game from Elm’s perspective:

Model
We have to store every tile’s data (number on the tiles and position of the tile). To store these tiles, we can introduce a matrix of NxN (N being the number of rows/columns, here 4). We can also store tiles as a list but matrix will be easy to render and visualize.

View
Since we kept our Model close to our View, in View we have to render this matrix into a matrix, with CSS.

Update
In Update function we will regenerate the Model (state) depending upon the message it receives. Let’s look at different kind of messages we must have:

  • LeftMove: To enable merging leftwards
  • RightMove: To enable merging rightwards
  • UpMove: To enable merging upwards
  • DownMove: To enable merging downwards
  • Reset: For resetting the grid to the initial state
  • AddTile: To add a tile at any random available space

The structure of the game looks good, now we have to implement the handling of these messages.

Diving into our messages

  • LeftMove

    LeftMove will merge same number tiles on a row in left direction. Since the tiles to be merged are in same list, we can do map-reduce for rows and merge 2 tiles with the same number into one and double the number on the tile. Lets look at the code below which merges tiles in a list and returns the list:

    mergeAndFillRow

    mergeAndFillRow : List Cell -> List Cell
    mergeAndFillRow list =
      let
          updatedRow =
              mergeRow list
      in
      List.concat
          (updatedRow
              :: [ EmptyCell
                      |> List.repeat
                          ((list |> List.length) - (updatedRow |> List.length))
                 ]
          )
    

    mergeRow

    mergeRow : List Cell -> List Cell
    mergeRow list =
      case list of
          [] ->
              []
      
          x1 :: xs ->
              if x1 == EmptyCell then
                  mergeRow xs
      
              else
                  case xs of
                      [] ->
                          [ x1 ]
      
                      x2 :: xs2 ->
                          if x1 == x2 then
                              mergeCell ( x1, x2 ) :: mergeRow xs2
      
                          else
                              x1 :: mergeRow (x2 :: xs2)
    

    In mergeRow, we use recursion to reduce the list, and then in mergeAndFillRow we fill the list with empty tiles. Functional programming is fun!

  • RightMove

    RightMove is LeftMove in the opposite direction (Thanks, I think). So we will reuse mergeAndFillRow. How? By reversing the List before and after calling this function.

  • UpMove

    For RightMove, and LeftMove, the merging operation is fairly easy since row elements are in a single list and therefore we can use List methods to reduce and merge tiles, but in UpMove case, the tiles are in multi columns, meaning items are in different lists. Here, elementary Mathematics comes to our rescue. We can transpose the matrix and then call the function mergeAndFillRow to implement UpMove.
    Since Elm is a functional programming language there are no looping (for/while) constructs, and this makes transpose little challenging.

    You can refer to the code below, uses recursion:

    transposeMap

    transposeMap : Grid -> Grid -> Grid
    transposeMap grid2 grid1 =
    case Array.get 0 grid1 of
          Just e ->
              transposeMap (grid2 |> transposeForIdx 0 e) (grid1 |> Array.slice 1 (grid1 |> Array.length))
    
          Nothing ->
              grid2
    

    transposeForIdx

    transposeForIdx : Int -> Array Cell -> Grid -> Grid
    transposeForIdx idx list grid2 =
      case Array.get 0 list of
          Just e ->
              transposeForIdx (idx + 1)
                  (list |> Array.slice 1 (list |> Array.length))
                  (case grid2 |> Array.get idx of
                      Just l ->
                          grid2 |> Array.set idx (l |> Array.push e)
      
                      Nothing ->
                          grid2 |> Array.push (Array.fromList [ e ])
                  )
      
          Nothing ->
              grid2
    
  • DownMove

    Using the same technique as we did with RightMove, we can: reverse the list, transpose, call mergeAndFillRow, transpose again, and finally reverse.

  • AddTile

    For this will create a list of available positions of tiles by traversing the matrix, and then randomly adding number tile 2 on any one of available positions.

  • Reset

    Reset is initializing the matrix and sending AddTile message.

Merging cells:

mergeCell is a common function which merges 2 tiles (Cell) and returns one tile:

swap : ( Cell, Cell ) -> ( Cell, Cell )
swap ( cell1, cell2 ) =
    ( cell2, cell1 )


mergeCell : ( Cell, Cell ) -> Cell
mergeCell ( cell1, cell2 ) =
    case ( cell1, cell2 ) of
        ( Tile val1, Tile val2 ) ->
            Tile (val1 + val2)

        ( Tile val1, EmptyCell ) ->
            Tile val1

        _ ->
            mergeCell (swap ( cell1, cell2 ))

Note: The above mergeCell method fails when arguments are EmptyCell and EmptyCell, it gets stuck in infinite recursion. But since we never have that case, it never crashes.

Subscriptions

For integrating key-presses and touch coordinates, we will use subscriptions.

Game is ready, now time for icing on the cake.

Adding CSS

I have replicated same color codes as in the original game.

You can look at the complete code here.