Learning Elm: 2048 game in Elm

5-minute read

Elm is a front-end programming language optimized for developer’s happiness. Elm basics are very straightforward, which is just the Elm Architecture.

The Elm Architecture

  • 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

You can read more about Elm here.

Here, we will construct the famous game: 2048 in Elm.

2048 Game

2048 is a game created by gabrielecirulli which went viral in 2014. We are recreating 2048 because the functioning of the game is very simple which will help us understand the elm architecture very clearly. Let’s call it a replacement for the TODO app for learning.

2048

You can play it here.

Rules of the game

The rules of the game are:

  • 2 similar tiles (same number) can be merged into one, doubling the value on the tile
  • Merging happens in the direction of the key-press
  • A new tile (number 2) is introduced in the game at a random empty space with every key-press
  • Game is over when no new tiles can be added

Structuring the design

Based on the above rules, 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 construct and use a matrix of NxN (N being the number of rows/columns, here 4).

View
The View is simple, we have to render this matrix, beautifully with CSS.

Update
Update function 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 towards the left side
  • RightMove: To enable merging towards the right side
  • UpMove: To enable merging upwards
  • DownMove: To enable merging downwards side
  • 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 deep into messages

  • LeftMove

    Since the tiles are in List of List fashion (tiles in row 1 are in the 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 : List Cell -> List Cell
    mergeAndFillRow list =
        let
            updatedRow =
                mergeRow list
        in
        List.concat
            (updatedRow
                :: [ EmptyCell
                        |> List.repeat
                            ((list |> List.length) - (updatedRow |> List.length))
                   ]
            )
    
    
    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.

  • RightMove

    RightMove is the same as LeftMove, in the opposite direction. So to reuse mergeAndFillRow we will do List reverse before and after calling this function.

  • UpMove

    For RightMove, and LeftMove, the merging operation was fairly easy since the same row elements were in a single list and therefore we could use List methods to reduce and merge tiles, but in this case, the tiles are in column (column 1 tiles are in different lists). Thinking on the same lines as doing the reverse, here we can transpose the matrix and then call the function mergeAndFillRow.
    Transpose is little challenging when we can’t use looping (for/while) constructs, you can refer to the code below (uses recursion):

    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 : 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 in RightMove, we can first reverse the list, then transpose, call mergeAndFillRow, transpose again, and finally reverse.

  • AddTile

    For this will create a list of available positions and randomly select one of them.

  • Reset

    Reset is initializing the matrix and sending AddTile message.

Common functions:

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 use that case, it never crashed.

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

You can look at the complete code here.