Learning Elm: 2048 game in Elm
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 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.
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:
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
N being the number of rows/columns, here 4).
The View is simple, we have to render this matrix, beautifully with CSS.
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
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)
mergeRow, we use recursion to reduce the list, and then in
mergeAndFillRowwe fill the list with empty tiles.
RightMove is the same as LeftMove, in the opposite direction. So to reuse
mergeAndFillRowwe will do List reverse before and after calling this function.
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
Listmethods 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
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
Using the same technique as we did in RightMove, we can first reverse the list, then transpose, call
mergeAndFillRow, transpose again, and finally reverse.
For this will create a list of available positions and randomly select one of them.
Reset is initializing the matrix and sending
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, 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.