Match Three with JS

Written by Hunter Jansen on March 10, 2017

For a little while now, I’ve been thinking that it might be fun to make a match 3 bejewelled style game with some town building elements. But, I know that I want to do it with just JavaScript and the DOM + CSS. I wanted to avoid canvas and libraries like 3js or anything like that.

ScreenShot

The Plan

Start off with a proof of concept, to leverage the DOM as heavily as possible. But I really wasn’t sure if I what I wanted to do was even possible. So I started reading match three in JavaScript examples - and the only ones I found involved canvas or other gaming libraries that I didn’t want to use. One of those articles was from rembound, and I used the core matching logic from this article as a kick off point, but quickly ended up changing it a LOT for my use case.

The end results can be found here with a demo available on my site (There’s output in the console as well).

I started off without no dependencies, but ended up pulling in jQuery, as a means to more easily select and interact with DOM elements. Also, I’ve decided to break this writeup into two separate posts, as otherwise it’ll be very long.

The logic

Logical

This section mainly focuses on the matching, board creating, and cascading logic

Creating a board

At first, I might have over-engineered this part. Really all I needed was a series of tile types, and a board containing those tiles. I decided on an 8x8 board and four tile types:

let board = {
  rows: 8,
  columns: 8
}

const colors = {
  0: {
    color: 'green',
    id: 0
  },
  1: {
    color: 'red',
    id: 1
  },
  2: {
    color: 'yellow',
    id: 2
  },
  3: {
    color: 'grey',
    id: 3
  },
  4: {
    color: 'orange',
    id: 4
  }
};

After that it was a case of populating an 8x8 array inside of the board where each index was assigned a type of tile - which is just done with a couple of loops:

for (var i = 0; i < board.rows; i++) {
    for (var j = 0; j < board.columns; j++) {
    board.tiles[i][j].type = this.getRandomTile();
    }
}

Detecting Matches

We’ve got a board, that’s all fine and good - but we can’t let the player actually play yet; since our board creation allows the possibility of matches already existing. We could change our board creation to not allow this, but where’s the fun in that? We need to create the match checking functionality anyway, so we may as well do it now.

This is the first part in which things get a little hairy and can likely be optimized by someone smarter than me. I used the same approach as the resource I linked above used:

//find horizontal clusters
for (var i = 0; i < board.rows; i++) {
    let rowNum = i;
    var matchLength = 1;
    for (var j = 0; j < board.columns; j++) {
        let columnNum = j;
        var checkCluster = false;

        if (columnNum == board.columns - 1) {
        checkCluster = true;
        } else {
        if (board.tiles[rowNum][columnNum].type === board.tiles[rowNum][columnNum + 1].type &&
            board.tiles[rowNum][columnNum].type !== -1) {
            matchLength++;
        } else {
            checkCluster = true;
        }
        }

        if (checkCluster) {
        if (matchLength >= 3) {
            clusters.push({
            column: columnNum + 1 - matchLength,
            row: rowNum,
            length: matchLength,
            horizontal: true
            })
        }
        matchLength = 1;
        }
    }
}

Basically:

  • Loop through each row
    • Loop through each tile in that row
    • If the tile after is the same type:
      • Increment length counter
    • Else if tiles are different:
      • If length counter is 3 or more
        • Push information about the cluster (starting tile & length of cluster) to an external array (clusters)
      • Increment length counter

If you follow this logic for each row and then for each column, you have all the current matches in your clusters array - if the clusters array is longer than 0, you have matches on the board - the next step is to clear them!

Resolving Clusters

So, we’ve built a board, and checked if there are matches - the next step is to resolve those matches!

This is also pretty hairy, and it’s hard to give a simple example that matches my end code - but basically we:

  • Loop through the clusters - and assign the tiles in the clusters to a temporary 'to clear' state
  • Loop through the tiles in the columns from bottom to top
  • If the current tile is to be deleted or blank, make the current tile match that of the tile above & make the tile above blank
  • If there is no tile above and the current tile is deleted or blank - set to a random tile

If you do all that then your clusters are cleared. But what’s it look like in code? Something like, but maybe not exactly this:

for (var i = 0; i < board.columns; i++) {
    var shift = 0;
    for (var j = board.rows - 1; j >= 0; j--) {
        let tile = board.tiles[i][j];
        // Loop from bottom to top
        if (tile.type === deletedTile) {
            tile.type = blankTile;
            this.drawTile(i, j);
        }
    }
}

Check if any matches are available

Match

The last big part of any basic match three system is to check if there are any matches available. That’s how you know if the game’s over, after all.

To accomplish this, we lean heavily on our detecting matches segment from earlier. The workflow goes like this:

  • For each row
    • For each tile
      • Swap current tile with next tile
      • Check if any matches
      • If a match, track that move in an external variable (let's call it moves)
      • Swap tiles back to original positions

You follow this train of thought through both the rows and the columns; if there are any items in moves then the game isn’t over! In practicality, it looks like this:

for (var i = 0; i < board.rows - 1; i++) {
    let rowNum = i;
    for (var j = 0; j < board.columns - 1; j++) {
        let columnNum = j;
        this.swapTiles(rowNum, columnNum, rowNum + 1, columnNum);
        this.findClusters();
        this.swapTiles(rowNum + 1, columnNum, rowNum, columnNum);
        if (clusters.length > 0) {
            moves.push({
                x1: rowNum,
                y1: columnNum,
                x2: rowNum + 1,
                y2: columnNum
            });
        }
    }
}

What’s neat is that by having allthe moves, you could even have the computer play the game to completion itself, it DOES know all the available moves after all.

Part One Complete

And with that, the main logic of the match-three part of the app is done. There is a bunch of other logic to do with actually cascading and drawing and all that other stuff, but that requires a slightly different headspace. So, I’ll write about that in the next article - since this one is already pretty long. I’ll update with a link to part two as soon as it’s posted.

** Edit: The follow up post can be found here

Got any feedback, questions, or similar experiences?

Hit me up on twitter (link in the footer), make an issue on the repo (further up in the post), or drop me a line at hunter@hyperwidget.com

Thanks for reading! -Hunter