Making 2048 Game in Flutter by using Explicit Animations — Part 2

Angjelko
13 min readJun 9, 2022

--

2048 Game in Flutter Logo

The source for the project is at Github:
https://github.com/angjelkom/flutter_2048

Before We can move on with Adding the components for the tiles, we need to add the relevant models and the StateNotifiers which will be used to manage the state for the game.

Adding Models

So under models create new file tile.dart and paste the code bellow:

models/tile.dart

The way the model is constructed will be explained in more details when the animation is added, for now you can check the comments in the screenshot briefly explaining each parameter.

Next we will add the Board model under models/board.dart:

models/board.dart

The board.dart represents a model of the 4x4 board containing the tiles we show on that same board alongside some other properties like the score, best score, whether the game was won or lost and the list of tiles currently on the board and also a reference to the state for the previous round, which will be used for the undo functionality.
Apart from that I’ve also added a newGame method.

Managers

Next we will add the managers

under managers folder create a new file board.dart and add the following code:

managers/board.dart

This will be the main StateNotifier we will use to manage all of the flows in the game like: creating a new game, ending a game, moving tiles, merging tiles and etc.
Looking at the code we see that when the app starts the code generates 1 random Tile.

Next go to models/tile.dart and add these two methods:

adding top/left position functions in models/tile.dart

The top methods are self explanatory but to explain it again, they calculate the top and left position based on the index of the tile, this way we won’t need to struggle to calculate manually were to position the tile the two methods use the index to do that. So your tile.dart mode should look like this now:

models/tile.dart

before we move on with implement the logic for the movement, let’s first go back to the ui components and implement the ui that will render the tiles on the board.

Adding the TileBoardWidget

Next under components folder create tile_board.dart file and add the following code:

components/tile_board.dart

This is a basic implementation of the board with the tiles, its almost same as the empty board we previously added the only difference is that this one renders the actual tiles on top of the empty board. Also keep in mind as I said this is basic implementation so we don’t have the needed Animations implemented yet, we will do that later on.

If you run the app now you will see a Tile with number 2 being rendered at random place on the empty board, if you hot reload or restart the app it will render the Tile with number 2 again but at a different position.

We are getting there!

Adding movement algorithm

Now we will mainly focus on implementing the movement logic for the tiles. While working on the project I was thinking what would be the most simplest way to implement the algorithm on how would the code decide where and how to move the tile in certain direction.

Before we continue with the logic lets have a look at a how 2048 works:

So when user swipes in certain direction, the tile moves to the opposite side. So we have left-right, top-bottom and visa-versa.

So let’s have a look at moving tiles from right-to-left:

Let’s imagine the tiles on the board start from index 0 from left-to-right:

Board horizontal movement directions sketch

And let’s say that there are tiles at index 1 and index 2 both with value 2.

The way the 2048 game works is by merging tiles starting from the opposite direction, so when we swipe right-to-left it will start moving from left-to-right towards the left side (if that makes sense). In this example it will first move the tile at index 1 and then the tile at index 2.

So the idea is to keep a list of Tiles and every time it needs to move to a certain direction in this case right-to-left, we loop through the List (starting at index 0) and for each item we compare it with the next item in the array, and decide on certain rules whether.

So the algorithm should do:

  • Sort the List by index (lowest to largest in the case of swiping left), this is to make sure that the next Tile we check in the List is the next we need in the order from the illustration above and not some random Tile.
  • Check the first Tile in the list (let’s call it TileA), what’s the index?
  • Index is 1
  • The TileA is first so the nextIndex is set to 0 for TileA
  • Get the next Tile in the list (let’s call it TileB)
  • TileB is at Index 2
  • Can TileB with index 2 be moved at TileA index 0 (as the TileA will be moved at index 0 we check if we can move TileB at the new TileA’s index)?
  • Yes we can so the TileB nextIndex will be 0 too. But if we couldn’t move the tile, the nextIndex would’ve been simply assigned: TileBNextIndex = TileANextIndex + 1, that way the TileB will be right next to TileA
  • After’ve assigned the new indexes we will use special methods to calculate the Left and Top side for each tile and tell the Animation System how to move each tile from point A to point B in the board
  • After the movement is done we will merge the tiles with same indexes by summing their values.

First of all we will add the algorithm for swipe left, that is moving tiles from right-to-left, and after that adding the other swipes rules will be easy.

In managers/board.dart above the move function add this function:

_calculate function in managers/board.dart

Again this is only for the left swipe but all the other swipes are similar so in the end it will be one general algorithm handling the movement. It might seem a lot but don’t get scared easily, I will try to explain this as simply as possible, and probably from the comments you already understand how the algorithm works.

Before I start explaining let’s add the rest of the functions so its more clear.
Above the _calculate function add the following code:

_inRange function in managers/board.dart

Simple right?
And lastly replace the move function with:

move function in managers/board.dart

Let’s see how these 3 functions work together. So let’s say the user swipes left, the tiles move from right-to-left, but the merge and movement happens from left-to-right.

First of all we sort the current tiles on the board by their indexes, as I explained previously we need to make sure the tiles are always sorted by their indexes.

Next we loop through each tile and first we call the _calculate function which calculates the nextIndex for the current tile. The rules are simple:

  • If there is a tile at the end of the list of new tiles (not the existing state list of tiles) and if both the current and the last processed tile are in the same row in the board (that’s what the _inRange function is doing) then the nextIndex would be the last processed tile nextIndex OR index + 1.
    That way the current tile would be right next to the last processed tile.
  • If there is no tile processed at the end of the list of if the last tile processed is NOT in the same row as the current tile, then we will get the most left index as we already know that the current tile is the first tile in that row so we can just get the most left index in this case and assign it as the nextIndex.
  • We create an immutable copy of the current tile with the new nextIndex and add it to the new list of processed tiles (the same list we checked above for last processed tile).

And that’s all for the current tile, simple right?

Before we finish with the current iteration we check the next tile and decide whether the next tile can be moved and merged with the currently processed tile. So again the rules are simple:

  • If the current processed tile and the next tile are with same value (number) and are in the same row in the board, then we assign the current processed tile nextIndex.
  • NOTE: The nextIndex in this case will always be assigned even if the nextIndex ends up being equal to the index of the currently processed tile.
  • Again we create an immutable copy of the next tile with the new nextIndex and add that to the list of processed tiles.
  • If the next tile get’s processed we skip the next iteration as the next tile is already processed and we move to the iteration after the next tile.

Lastly we create an immutable state of the board with the new list of tiles we’ve processed and replace the current state which will trigger a rebuild.

The managers/board.dart file should look like this:

managers/board.dart

We aren’t done yet, we still need to implement the rest of the algorithm for the other directions and only after that we can move on to writing the needed UI components and Animations.

Next let’s do the opposite which is swiping right, that is left-to-right.

Let’s first look at the preview hand sketch:

Board horizontal movement directions sketch

If you haven’t guessed until now, the way to implement the right swipe is go opposite in the logic, so going from the end of the list to the beginning of the list of tiles also known as descending order.

We can do that by updating the sorting logic we do at the beginning of the movement, so replace the sorting logic at the top of the move function with:

sort the list of tiles in managers/board.dart

This basically means if he swipes left keep multiple the compareTo output by 1 else multiple it by -1 which would sort the tiles in descending order.

We aren’t done yet we also need to update the _calculate function.

First at the beginning of the function add the following line:

asc variable in the_calculate function in managers/board.dart

right bellow that replace the int nextIndex variable with:

nextIndex variable in the _calculate function in managers/board.dart

This basically says that if the sorting is ascending it will pick the first index from the left side of the row and if its descending it will pick the first index from the right side of the row.
And to briefly explain the formula I use to determine that:

  • let’s say the title.index = 6 (which is the 3rd tile from the left and 2nd from right side, in the second row)
  • ceil means it will ALWAYS round up to the next largest integer
  • NOTE: don’t confuse ceil it with floor or round as even if the value is 2.1 output would be 3.
  • ((6 + 1) / 4) = 1.75
  • Ceil(1.75) = 2
  • If it’s ascending: 2 * 4 – 4 = 4, which is the first index from the left side in the second row
  • If it’s descending: 2 * 4 – 1 = 7, which is the last index from the left side and first index from the right side in the second row

Lastly we need to update the nextIndex logic again in the _calculate function but the logic where set the index in case the the last processed tile is in range with the current tile, so replace that line with:

nextIndex variable in the _calculate function in managers/board.dart

This again is self explanatory, but basically it means if the order is ascending add it next to the last processed tile, else add it before it because we are swiping right we process tiles in the descending order os the tile would go before the last processed tile.

And were done with the right swipe. The _calculate function should look like:

_calculate function in managers/board.dart

And move function:

move function in managers/board.dart

We are not done yet next we will do the swipe down movement.

First let’s have a look at our earlier sketch for horizontal swipe that is left/right swipe:

Board horizontal movement directions sketch

So the question is how can we reuse the logic we have for left/right swipe also for up/down swipe? We could do it if we sort the indexes on the board so that we “treat” down swipe like right swipe and up swipe like left swipe.

So if we visually rotate the board in our might the new board for the up/down swipe would look like this:

Board vertical movement directions sketch

So basically we’ve rotated the previous sketch counter clockwise, so the new order of indexes are these now.

Now the easiest way to implement this in code is to just have a const list of array which will reference the “real” index for the vertical swipe based on the index. This sounds confusing so let’s give it an example:

  • Let’s say that the tile is at index 11 in the Horizontal sketch, when user swipes left/right that would still be at index 11, but if user swipes up/down then we ask the code what would be the index of the tile if we have the board counter clockwise rotated in order to reuse the same logic? If we look at the Vertical sketch the index would be 3, because the index 11 in the vertical sketch is at the same place where index 3 is in the horizontal sketch.

Let’s go and add the code so it’s more clear.

On top of the manager as a final variable add this List:

verticalOrder variable in managers/board.dart

We will use this to track the “real” index we need.

Next let’s update all the places where the index is used. First we will update the sorting function inside the move function. Replace the sort function with:

sorting tiles in move function in managers/board.dart

This is self explanatory but basically the only thing changed about the sorting function is that if the user swipes up/down we will retrieve the “real” up/down index based on the left/right index in order to correctly sort the list of tiles.

Next scroll down in the move function and replace this line:

index variable in move function in managers/board.dart

with this line:

index and nextIndex variables in move function in managers/board.dart

again here we use the verticalOrder map to retrieve the up/down indexes.

Next we need to update the _calculate function so on top of the function bellow the asc variable add the following code:

vert variable in _calculate function in managers/board.dart

Next replace this line of code:

nextIndex variable in _calculate function in managers/board.dart

with this line of codes:

index and nextIndex variables in _calculate function in managers/board.dart

And this lines of code:

lastIndex variable in _calculate function in managers/board.dart

with this lines:

lastIndex variable in _calculate function in managers/board.dart

Now we have implemented the swipe down movement.

Last we need to do for the movement flow is to implement the swipe up, but because we’ve just added a logic which reuses the left/right swipe, the only change we need to do to implement the swipe up is to update the asc variable, so replace the asc variables both in the move and _calculate function with:

asc variable in _calculate function in managers/board.dart

Before we move with adding the Animations and AnimationControllers let’s add the rest of the logic so that its easier to understand when we connect the ui and animations and avoid constant jump between logic and ui.

After the tiles are moved the ones that overlap with same number get merged, so next we will add the merge logic.

Inside the BoardManager StateNotifier add the following function:

merge function in managers/board.dart

The comments should be self explanatory but the process is this:

  • loop through each tile and if the next tile is at the same index with current tile then sum the value of both tiles and add it as the value for the new tile
  • assign the nextIndex or index to the new tile
  • mark the new tile as merged (would be needed for later)
  • skip the next tile if it got merged.
  • If the tiles got moved generate a new tile at random position that are left empty on the board.

And that’s it for the merge function.

Now after the tiles are merged we need to end the round and mark the merged tiles as false (also needed).

So first we will add a private function called _endRound:

_endRound function in managers/board.dart

Basically what this function does is:

  • first we make sure the tiles are sorted by index
  • Next if there are 16 tiles on the board, so no more tiles can be added we do some checks:
  • For each tile we check if the tile can be merged either left/right/above/bellow and if so then the game is not lost, if not then the game will be lost
  • If there are less than 16 tiles on the board it means there are still places to add new tiles which automatically means the game is not lost
  • we mark each tile merged: false (needed, will be explained later)
  • If it has number 2048 if yes then the game is won, yeah I wish..

To complete the end round logic we need to first add 2 more managers, so under managers folder create a file round.dart and add the following code:

managers/round.dart

So the RoundManager holds a simple boolean which will be used to track when a round starts and when rounds ends, this will be used in order to prevent animation issues which can happen if the animation gets started before the previous animation ends. When we add the animations it will be much clear.

Next under managers create a file next_direction.dart and add the following code:

managers/next_direction.dart

Instead of canceling the next round we prevented from running until current round finishes using the RoundManager, we will use the NextDirectionManager to “queue” the next round and start it automatically as soon the next round finishes.
This will prevent the game to feel like it’s being lag-ish or slow to the end user.

Now let’s end the round. so right bellow the _endRound add following function:

endRound function in managers/board.dart

Note the difference between the endRound and _endRound, the first one is the main function which will be called to end the round and the second is a private function which get’s called by endRound.

To briefly explain the endRound does the following:

  • End the round
  • Automatically start the next round if any direction was queued.

That would be all for this chapter. In the next chapter we will finally add the AnimationControllers and the Explicit Animations. Thanks for reaching so far and head over to the third part by clicking the link bellow.

--

--