How To Make A Match-3 Game With HTML5 Canvas
Everyone knows what a Match-3 or Match-Three game is. At least, everyone knows a game that is a Match-3 game. Games like Bejeweled from PopCap Games or Candy Crush Saga from King are Match-3 games. These games are also called casual games, which are quite popular with a specific group of players. A Match-3 game, in its basic form, has a couple of common elements. There is a two-dimensional grid of tiles. Tiles come in different types, represented by different shapes and colors. The player can change the position of the tiles, by swapping adjacent tiles for example, to create clusters of three or more horizontal or vertical tiles that are from the same type. These clusters of three or more tiles are also called matches or Match-3s. If clusters were formed by this swapping action, the clusters will be removed, the player gets points and gravity will shift the remaining tiles into a stable position, replacing the removed tiles with new, random tiles.
In this tutorial we will be creating such a Match-3 game, using HTML5 Canvas and JavaScript. We will use the HTML5 Canvas Basic Game Framework from my previous tutorial as a base, and build upon it. The result of this tutorial will be an in-browser playable game, with the mechanics of a Match-3 game.

How To Make A Match-3 Game With HTML5 Canvas
Click here to go directly to the end of this article and play the game.
Bejeweled And Candy Crush Saga
Two popular Match-3 games are Bejeweled and Candy Crush Saga. The images below show how these games look like. The core of their gameplay uses the Match-3 mechanic. As explained before, the player needs to create clusters of three or more horizontal or vertical tiles that are from the same type. Once a cluster is formed, the tiles in the cluster are removed. New tiles will appear to take the place of the removed tiles. The images below show that the tiles are arranged in a two-dimensional grid. While the Match-3 mechanic is the same in both games, there are differences.

Bejeweled
Bejeweled uses a grid with 8 columns and 8 rows. Tiles are represented by shiny and colorful gems. There are 7 regular gems. Special gems can be summoned by creating combos and chains of matches. These special gems allow the player to score more points and clear the level faster.

Candy Crush Saga
In the image above, Candy Crush Saga uses 8 columns and 5 rows. Progressing through the game changes the layout of the grid. The shape of the grid gets modified and holes may appear. There are 6 different types of regular tiles, which look like pieces of candy. Special types can be created by making combos and chains. The special candies allow for greater points and faster clearing of the level. Each level has a specific goal before the level is won.
While the two games have a different art style, different levels and different combos and goals, at the core, they are the same game. The player progresses through the games in a different way, but at their core, the games use the standard Match-3 mechanic, which we explain in the rest of this tutorial.
Generating A Level
We define a level to be a two-dimensional grid of level.columns by level.rows tiles, that have a certain type. For this tutorial, we will define the type of a tile to be a specific color only, but you could define a shape for a specific type of tile. So first, define the level and the possible tile types.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Level object var level = { x: 250, // X position y: 113, // Y position columns: 8, // Number of tile columns rows: 8, // Number of tile rows tilewidth: 40, // Visual width of a tile tileheight: 40, // Visual height of a tile tiles: [], // The two-dimensional tile array selectedtile: { selected: false, column: 0, row: 0 } }; // All of the different tile colors in RGB var tilecolors = [[255, 128, 128], [128, 255, 128], [128, 128, 255], [255, 255, 128], [255, 128, 255], [128, 255, 255], [255, 255, 255]]; |
At our init() function, we initialize the level.tiles array:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Initialize the game function init() { // Initialize the two-dimensional tile array for (var i=0; i<level.columns; i++) { level.tiles[i] = []; for (var j=0; j<level.rows; j++) { // Define a tile type and a shift parameter for animation level.tiles[i][j] = { type: 0, shift:0 } } } // (...) } |
We have defined our level format and the possible tile colors, so now we can start generating a level. An initial level should have a couple of properties. Let’s define them:
- The level should be filled with tiles of random type
- There are no matches or clusters of three or more horizontal or vertical tiles
- There should be at least one valid move possible
We could create a clever algorithm that only inserts proper tiles in the two-dimensional grid, while maintaining the properties defined above. For this tutorial, we are creating a simpler algorithm, that uses brute-force methods to achieve our goals. Our two-dimensional grid is small enough to allow us to use brute-force methods, without slowing the game down. This is our algorithm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Create a random level function createLevel() { var done = false; // Keep generating levels until it is correct while (!done) { // Create a level with random tiles for (var i=0; i<level.columns; i++) { for (var j=0; j<level.rows; j++) { level.tiles[i][j].type = getRandomTile(); } } // Resolve the clusters resolveClusters(); // Check if there are valid moves findMoves(); // Done when there is a valid move if (moves.length > 0) { done = true; } } } |
As you can see, we fill the tile array with random tiles. We check to see if there are clusters. If there are clusters, remove them, until there are no more clusters. Fill the removed clusters with new random tiles. Finally, we check if there is at least one valid move. If there is a valid move, we are done. If there was no valid move, the level is unplayable and we generate a new level.
Let’s implement the resolveClusters() function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Remove clusters and insert tiles function resolveClusters() { // Check for clusters findClusters(); // While there are clusters left while (clusters.length > 0) { // Remove clusters removeClusters(); // Shift tiles shiftTiles(); // Check if there are clusters left findClusters(); } } |
We keep removing clusters until there are no clusters left. While removing clusters, we shift the tiles downward and insert new random tiles, like we would when a user removes a tile by hand. We will implement the findMoves(), removeClusters() and shiftTiles() functions later in this tutorial. Finding clusters with the findClusters() function will be explained next.
How To Find Clusters
A cluster, also called a match, is defined by three or more horizontal or vertical neighboring tiles of the same type. To make things easy, we create an algorithm to find the horizontal clusters and an algorithm to find the vertical clusters. These two algorithms will look the same, with only minor differences. To find horizontal clusters, we could look at one row at a time. At each row, we start with the tile in the first column and initialize our matchlength counter to one. From this first tile, we move to the tile in the next column, and see if the tile is of the same type. If it is, we increase our matchlength and move to the next tile, until we are at the last column. If the tile has a different type, we check if matchlength is greater than or equal to three. If it is, we have found a cluster and we add it to the clusters array. If we are at the last column, we move to the next row and repeat the process. To find vertical clusters, we do the same, but we move through the grid in a different direction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | // Array of clusters var clusters = []; // { column, row, length, horizontal } // Find clusters in the level function findClusters() { // Reset clusters clusters = [] // Find horizontal clusters for (var j=0; j<level.rows; j++) { // Start with a single tile, cluster of 1 var matchlength = 1; for (var i=0; i<level.columns; i++) { var checkcluster = false; if (i == level.columns-1) { // Last tile checkcluster = true; } else { // Check the type of the next tile if (level.tiles[i][j].type == level.tiles[i+1][j].type && level.tiles[i][j].type != -1) { // Same type as the previous tile, increase matchlength matchlength += 1; } else { // Different type checkcluster = true; } } // Check if there was a cluster if (checkcluster) { if (matchlength >= 3) { // Found a horizontal cluster clusters.push({ column: i+1-matchlength, row:j, length: matchlength, horizontal: true }); } matchlength = 1; } } } // Find vertical clusters for (var i=0; i<level.columns; i++) { // Start with a single tile, cluster of 1 var matchlength = 1; for (var j=0; j<level.rows; j++) { var checkcluster = false; if (j == level.rows-1) { // Last tile checkcluster = true; } else { // Check the type of the next tile if (level.tiles[i][j].type == level.tiles[i][j+1].type && level.tiles[i][j].type != -1) { // Same type as the previous tile, increase matchlength matchlength += 1; } else { // Different type checkcluster = true; } } // Check if there was a cluster if (checkcluster) { if (matchlength >= 3) { // Found a vertical cluster clusters.push({ column: i, row:j+1-matchlength, length: matchlength, horizontal: false }); } matchlength = 1; } } } } |
How To Find Available Moves
If we can swap two horizontal or vertical neighboring tiles and create a cluster of three or more tiles, we have found a valid move. We create an algorithm that uses brute-force methods to try out all horizontal and vertical swaps, and find out if the swaps made one or more cluster using the findClusters() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // Array of moves var moves = []; // { column1, row1, column2, row2 } // Swap two tiles in the level function swap(x1, y1, x2, y2) { var typeswap = level.tiles[x1][y1].type; level.tiles[x1][y1].type = level.tiles[x2][y2].type; level.tiles[x2][y2].type = typeswap; } // Find available moves function findMoves() { // Reset moves moves = [] // Check horizontal swaps for (var j=0; j<level.rows; j++) { for (var i=0; i<level.columns-1; i++) { // Swap, find clusters and swap back swap(i, j, i+1, j); findClusters(); swap(i, j, i+1, j); // Check if the swap made a cluster if (clusters.length > 0) { // Found a move moves.push({column1: i, row1: j, column2: i+1, row2: j}); } } } // Check vertical swaps for (var i=0; i<level.columns; i++) { for (var j=0; j<level.rows-1; j++) { // Swap, find clusters and swap back swap(i, j, i, j+1); findClusters(); swap(i, j, i, j+1); // Check if the swap made a cluster if (clusters.length > 0) { // Found a move moves.push({column1: i, row1: j, column2: i, row2: j+1}); } } } // Reset clusters clusters = [] } |
Removing Clusters
We know how to find clusters. The only thing left is removing them. When a cluster is removed, a hole appears in the grid with empty tiles. The tiles that are above this hole should be shifted to the bottom of the grid and new tiles should appear at the top of the grid. The removeClusters() function below, uses the found clusters in the clusters array and creates holes in the grid by setting the type of the tiles to -1. After the holes are created, the algorithm calculates how much the remaining tiles need to be shifted downwards and stores this number in the shift parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Remove the clusters function removeClusters() { // Change the type of the tiles to -1, indicating a removed tile loopClusters(function(index, column, row, cluster) { level.tiles[column][row].type = -1; }); // Calculate how much a tile should be shifted downwards for (var i=0; i<level.columns; i++) { var shift = 0; for (var j=level.rows-1; j>=0; j--) { // Loop from bottom to top if (level.tiles[i][j].type == -1) { // Tile is removed, increase shift shift++; level.tiles[i][j].shift = 0; } else { // Set the shift level.tiles[i][j].shift = shift; } } } } |
Clusters are removed, but the tiles need to be shifted and new tiles need to be inserted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Shift tiles and insert new tiles function shiftTiles() { // Shift tiles for (var i=0; i<level.columns; i++) { for (var j=level.rows-1; j>=0; j--) { // Loop from bottom to top if (level.tiles[i][j].type == -1) { // Insert new random tile level.tiles[i][j].type = getRandomTile(); } else { // Swap tile to shift it var shift = level.tiles[i][j].shift; if (shift > 0) { swap(i, j, i, j+shift) } } // Reset shift level.tiles[i][j].shift = 0; } } } |
Removing clusters and shifting tiles can create new clusters. These new clusters should be removed as well. The removeClusters() and shiftTiles() functions should be called repeatedly in an animation sequence, to remove all of the clusters, while increasing the score and applying combo systems.
Making A Simple AI Bot
As a bonus, we can create a simple artificial intelligence bot that plays the game for us. We know all of the possible moves by using the findMoves() function. If we take a random move and pretend we are a player that swaps two tiles, we can automatically play the game. When the game is in a ready state, we execute the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Check if there are moves available findMoves(); if (moves.length > 0) { // Get a random valid move var move = moves[Math.floor(Math.random() * moves.length)]; // Simulate a player using the mouse to swap two tiles mouseSwap(move.column1, move.row1, move.column2, move.row2); } else { // No moves left, Game Over. We could start a new game. // newGame(); } |
Match-3 Example
Here is the finished game. The code can be expanded to implement proper animations, a combo system and more game mechanics. Adjacent tiles can be swapped by clicking and dragging. The full source code is available on GitHub, licensed under GPLv3. Check out my article How To Load And Draw Images With HTML5 Canvas to learn how to add images to the game.