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}`;
}
}
}
};