Projects: Match Game

Match Game

Notes

Overview

  • This is a basic match game. Clicking or tapping on a tile turns it over. You choose tiles in pairs. Tile sets are completed when you turn over two times with the same face on a turn. If the second tile doesn't match the first, they both turn over and are hidden again.

  • You win the game when all the pairs are matched.

Bitty Details

The JavaScript for the game (shown below) is mostly about implementing the logic for the game itself. It includes tracking which tiles are turned over, how many matches there are, how many turns have been taken, etc...

The logic is independent of Bitty. It could be used anywhere. The thing that Bitty provides is an easy way to connect the interactions with the individual tiles to the logic. Some Bitty specific things to note:

  • Each tile is made up from a button that includes details in custom data-* attributes. Each button is created using the this.api.makeHTML() method.

    Some of the data-* attributes are the same for every tile (e.g. data-use="matchGameMakePick"). Others, like data-pair="PAIR_NUM" are updated via find/replace substitutions with actual data when they are created.

  • The data-state attribute on each tile holds it's state. The game uses that value to determine what content populates the tile.

  • The faces are made by combining two SVG images: a face and a head. The source collections of faces and heads are initially loaded from remote files with this.api.getTXT(). When the tiles are created the text is turned into an SVG with this.api.makeSVG().

    That approach means the external files only need to be downloaded once. They can be reused any number of times to make new SVG elements.

    (Side note, there's no technical need for the faces and the heads to be separate files. That's just how the source images I'm working with came in.)

The Code

Here's the source code for the game

HTML

<bitty-7-0 data-connect="MatchGame">
<div 
  class="match-game-grid" 
  data-init="matchGameGrid"
  data-receive="matchGameGrid"></div>
<div 
  data-init="matchGameStatus"
  data-receive="matchGameStatus"></div>
</bitty-7-0>

JavaScript

const matchGameTemplates = {
  tile: `
<button 
  class="tile-button"
  data-state="hide" 
  data-pair="PAIR_NUM" 
  data-index="INDEX"
  data-use="matchGameMakePick"
  data-receive="matchGameUpdateTile"
>?</button>`,
  winner:
    `<div>Turns: TURNS - Winner!<br /><button data-send="matchGameGrid">Play Again</button></div>`,
};

const sourceHeads = [];
const sourceFaces = [];

function shuffleArray(array) {
  let currentIndex = array.length;
  let randomIndex;
  while (currentIndex != 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex],
      array[currentIndex],
    ];
  }
}

window.MatchGame = class {
  #tries = [];
  #turns = 0;
  #matchCount = 0;
  #tileCount = 16;
  #heads = [];
  #faces = [];

  async bittyInit() {
    for (let i = 0; i < this.#tileCount / 2; i += 1) {
      const headURL = `/versions/7/0/0/svgs/heads/${i}.svg`;
      const headResponse = await this.api.getTXT(headURL);
      if (headResponse.value) {
        sourceHeads.push(headResponse.value);
      }
      const faceURL = `/versions/7/0/0/svgs/faces/${i}.svg`;
      const faceResponse = await this.api.getTXT(faceURL);
      if (faceResponse.value) {
        sourceFaces.push(faceResponse.value);
      }
    }
  }

  matchGameGrid(ev, el) {
    this.#turns = 0;
    this.#matchCount = 0;
    this.#heads = [];
    this.#faces = [];
    const nums = [];
    [...Array(this.#tileCount / 2)].forEach((i, indx) => {
      nums.push(indx);
      nums.push(indx);
    });
    shuffleArray(nums);
    el.replaceChildren();
    [...Array(this.#tileCount)].forEach((_, index) => {
      const num = nums.pop();
      const subs = [
        ["INDEX", index],
        ["PAIR_NUM", num],
      ];
      el.appendChild(
        this.api.makeHTML(matchGameTemplates.tile, subs),
      );
      const head = this.api.makeSVG(sourceHeads[num]);
      head.classList.add("svg-head");
      this.#heads.push(head);
      const face = this.api.makeSVG(sourceFaces[num]);
      face.classList.add("svg-face");
      this.#faces.push(face);
    });
    this.api.trigger("matchGameStatus");
  }

  matchGameMakePick(_ev, el) {
    if (
      el.prop("state") === "hide" || el.prop("state") === "miss"
    ) {
      el.dataset.state = "try";
      this.#tries.push(el.propToInt("pair"));
    }
    this.api.trigger(`
      matchGameUpdateTile
      matchGameClearTries
      matchGameStatus
    `);
  }

  matchGameUpdateTile(_ev, el) {
    if (
      this.#tries.length === 2 &&
      this.#tries.includes(el.propToInt("pair"))
    ) {
      if (
        this.#tries[0] === this.#tries[1]
      ) {
        el.dataset.state = "match";
        this.#matchCount += 1;
      } else {
        if (el.prop("state") === "try") {
          el.dataset.state = "miss";
        }
      }
    } else {
      if (el.prop("state") === "miss") {
        el.dataset.state = "hide";
      }
    }
    if (
      el.prop("state") === "hide"
    ) {
      el.innerHTML = "?";
    } else {
      el.replaceChildren();
      el.appendChild(this.#heads[el.propToInt("index")]);
      el.appendChild(this.#faces[el.propToInt("index")]);
    }
  }

  matchGameClearTries(_ev, _el) {
    if (this.#tries.length === 2) {
      this.#turns += 1;
      this.#tries = [];
    }
  }

  matchGameStatus(_ev, el) {
    if (this.#turns === 0) {
      el.innerHTML = "Ready";
    } else {
      if (this.#matchCount === this.#tileCount) {
        const subs = [
          ["TURNS", this.#turns],
        ];
        const winner = this.api.makeHTML(matchGameTemplates.winner, subs);
        el.replaceChildren(winner);
      } else {
        el.innerHTML = `Turns: ${this.#turns}`;
      }
    }
  }
};