๐ŸŸจ My First Browser Game ยท Episode 2 of 6 ยท See All Episodes
๐Ÿง  Episode 2 ยท Beginner ยท DOM & CSS Skills

Match Maker!
Memory Card Game

No canvas this time, build a card-flipping memory game out of real HTML elements, CSS 3D flips and DOM events. This is the same skill set as building web apps, disguised as a game.

๐Ÿ‘ถ Ages 10+ โฑ๏ธ ~1.5 Hours ๐ŸŸจ JavaScript โœ“ Free
๐Ÿƒ DOM elements ๐Ÿ”€ Array shuffle ๐ŸŽจ CSS 3D flips ๐Ÿ–ฑ๏ธ Click events ๐Ÿง  Match logic โฑ๏ธ Move counter
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
1
๐Ÿƒ
The Board from Code
Generate 16 card divs with JavaScript
Active
๐ŸŽฏ
Goal for this step

Build a 4ร—4 grid of cards entirely from JavaScript.

  • 1New file memory.html: a <div id="board"> styled with display: grid, 4 columns.
  • 28 emoji pairs: double the array, so 16 values.
  • 3Loop the values: document.createElement("div"), add class "card", store its emoji in card.dataset.value, append to the board.
  • 4Cards show "?" until flipped. The page builds itself, that is the DOM.
memory.html
<div id="board"></div>
<style>
#board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
.card  { height: 90px; background: #2a2d3a; border-radius: 12px;
         display: flex; align-items: center; justify-content: center;
         font-size: 2rem; cursor: pointer; color: #fff; }
</style>
<script>
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ"];
let values = [...EMOJI, ...EMOJI];      // 8 pairs = 16 cards

const board = document.getElementById("board");
for (const v of values) {
    const card = document.createElement("div");
    card.className = "card";
    card.dataset.value = v;
    card.textContent = "?";
    board.appendChild(card);
}
</script><div id="board"></div>
<style>
#board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
.card  { height: 90px; background: #2a2d3a; border-radius: 12px;
         display: flex; align-items: center; justify-content: center;
         font-size: 2rem; cursor: pointer; color: #fff; }
</style>
<script>
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ"];
let values = [...EMOJI, ...EMOJI];      // 8 pairs = 16 cards

const board = document.getElementById("board");
for (const v of values) {
    const card = document.createElement("div");
    card.className = "card";
    card.dataset.value = v;
    card.textContent = "?";
    board.appendChild(card);
}
</script>
๐Ÿ“ฆ Full starter file , the Step 1 board builder

A complete, runnable page that builds the 4ร—4 card grid from JavaScript. Open it and you see 16 face-down cards. Every step below adds shuffling, flipping, matching and scoring to this same file.

memory.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
  body { background:#0d1117; display:flex; justify-content:center; padding:16px; }
  #board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
  .card  { height: 90px; background: #2a2d3a; border-radius: 12px;
           display: flex; align-items: center; justify-content: center;
           font-size: 2rem; cursor: pointer; color: #fff; }
</style>
</head>
<body>
<div id="board"></div>
<script>
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ"];
let values = [...EMOJI, ...EMOJI];        // 8 pairs = 16 cards
const board = document.getElementById("board");
for (const v of values) {
  const card = document.createElement("div");
  card.className = "card";
  card.dataset.value = v;
  card.textContent = "?";                  // hidden until flipped
  board.appendChild(card);
}
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
  body { background:#0d1117; display:flex; justify-content:center; padding:16px; }
  #board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
  .card  { height: 90px; background: #2a2d3a; border-radius: 12px;
           display: flex; align-items: center; justify-content: center;
           font-size: 2rem; cursor: pointer; color: #fff; }
</style>
</head>
<body>
<div id="board"></div>
<script>
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ"];
let values = [...EMOJI, ...EMOJI];        // 8 pairs = 16 cards
const board = document.getElementById("board");
for (const v of values) {
  const card = document.createElement("div");
  card.className = "card";
  card.dataset.value = v;
  card.textContent = "?";                  // hidden until flipped
  board.appendChild(card);
}
</script>
</body>
</html>
โœ๏ธ
Fill in the Blanks
+15 XP
New elements are made with document. and attached with board.. Each card remembers its emoji in card.dataset..
๐Ÿง 
Knowledge Check
+15 XP
[...EMOJI, ...EMOJI] createsโ€ฆ
AA copy with 8 items
BOne array containing every emoji twice, the 16 card faces
CA syntax error
2
๐Ÿ”€
The Fisher-Yates Shuffle
Shuffle the deck the CORRECT way
Locked
๐ŸŽฏ
Goal for this step

Randomise card order with the classic fair-shuffle algorithm.

  • 1Shuffling arrays has a right answer: Fisher-Yates, walk backwards, swap each item with a random earlier one.
  • 2It is provably fair: every arrangement equally likely. (The lazy .sort(() => Math.random()-0.5) is genuinely biased!)
  • 3Shuffle values BEFORE building the cards.
  • 4Refresh a few times, different board every game.
memory.html
function shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];   // swap!
    }
    return arr;
}

values = shuffle(values);function shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];   // swap!
    }
    return arr;
}

values = shuffle(values);
๐Ÿ’ก
[a, b] = [b, a] is JavaScriptโ€™s one-line swap, destructuring assignment. No temp variable needed.
โŒจ๏ธ
Code Challenge
+20 XP
Complete Fisher-Yates:
memory.html
for (let i = arr.length - 1; i > for (let i = arr.length - 1; i > ; i--) {
    const j = Math.floor(Math.random() * (i + ; i--) {
    const j = Math.floor(Math.random() * (i + ));
    [arr[i], arr[j]] = [arr));
    [arr[i], arr[j]] = [arr], arr[i]];
}], arr[i]];
}
๐Ÿ’ก Hint: Walk down to index 1; the random pick includes position i itself (hence +1); then swap the two.
๐Ÿง 
Knowledge Check
+15 XP
Why learn a named algorithm for something as small as shuffling?
AInterviews only
BBecause the obvious hack is subtly WRONG (biased), some problems have known correct answers worth knowing
CShuffling is rare
3
๐ŸŽจ
CSS Flip Animation
Cards flip in 3D with a .flipped class
Locked
๐ŸŽฏ
Goal for this step

Flip cards over with a smooth CSS 3D rotation.

  • 1The animation is pure CSS: transition: transform 0.35s and a .flipped class that sets transform: rotateY(180deg).
  • 2JavaScript only toggles the class, CSS does the motion. That division of labour is how modern web apps animate.
  • 3On flip, also reveal the emoji (swap textContent from "?" to the dataset value).
  • 4Click a card โ†’ click handler adds .flipped. Satisfying already!
memory.html
<style>
.card { transition: transform 0.35s; }
.card.flipped { transform: rotateY(180deg); background: #ffd166; color: #222; }
</style>
<script>
board.addEventListener("click", function(e) {
    const card = e.target.closest(".card");
    if (!card || card.classList.contains("flipped")) return;
    card.classList.add("flipped");
    card.textContent = card.dataset.value;
});
</script><style>
.card { transition: transform 0.35s; }
.card.flipped { transform: rotateY(180deg); background: #ffd166; color: #222; }
</style>
<script>
board.addEventListener("click", function(e) {
    const card = e.target.closest(".card");
    if (!card || card.classList.contains("flipped")) return;
    card.classList.add("flipped");
    card.textContent = card.dataset.value;
});
</script>
โœ๏ธ
Fill in the Blanks
+15 XP
One listener on the whole handles every card via e.target.closest, that pattern is called event . The motion itself is defined in , not JavaScript.
๐Ÿง 
Knowledge Check
+15 XP
One click listener on the board instead of 16 on the cards isโ€ฆ
ASlower but tidier
BEvent delegation, fewer listeners, and it even works for cards added later
CRequired by browsers
4
๐Ÿง 
Match Logic
Two flipped cards: stay or flip back
Locked
๐ŸŽฏ
Goal for this step

Compare pairs and lock matches or flip mismatches back.

  • 1Track state: let first = null; let locked = false;
  • 2First click โ†’ remember the card. Second โ†’ compare dataset values.
  • 3Match: both get .matched (and stop being clickable). No match: lock the board, wait 0.8 s (setTimeout), flip both back, unlock.
  • 4The lock stops players spam-flipping a third card during the wait, the classic memory-game bug.
memory.html
let first = null, locked = false;

function onCardClick(card) {
    if (locked || card.classList.contains("flipped")) return;
    flip(card);

    if (!first) { first = card; return; }

    if (first.dataset.value === card.dataset.value) {
        first.classList.add("matched");
        card.classList.add("matched");
        first = null;
    } else {
        locked = true;
        setTimeout(function() {
            unflip(first); unflip(card);
            first = null;
            locked = false;
        }, 800);
    }
}let first = null, locked = false;

function onCardClick(card) {
    if (locked || card.classList.contains("flipped")) return;
    flip(card);

    if (!first) { first = card; return; }

    if (first.dataset.value === card.dataset.value) {
        first.classList.add("matched");
        card.classList.add("matched");
        first = null;
    } else {
        locked = true;
        setTimeout(function() {
            unflip(first); unflip(card);
            first = null;
            locked = false;
        }, 800);
    }
}
โŒจ๏ธ
Code Challenge
+20 XP
Handle the mismatch with a delay:
memory.html
locked = locked = ;
;
(function() {
    unflip(first); unflip(card);
    first = null;
    locked = (function() {
    unflip(first); unflip(card);
    first = null;
    locked = ;
}, 800);;
}, 800);
๐Ÿ’ก Hint: Freeze input, schedule the flip-back for later with the browser timer function, then unfreeze inside it.
๐Ÿง 
Knowledge Check
+15 XP
What breaks WITHOUT the locked flag?
ACSS stops working
BPlayers can flip a 3rd card during the 0.8 s wait, tangling the two-card state machine
CsetTimeout fails
5
โฑ๏ธ
Moves, Timer & Winning
Count pairs tried, time the game, detect victory
Locked
๐ŸŽฏ
Goal for this step

Add a move counter, a live timer and the win banner.

  • 1Moves = pair attempts: increment on every SECOND flip.
  • 2Timer: setInterval every second updates a seconds counter on screen; start on first flip, stop on win.
  • 3Win check: count .matched cards === 16 โ†’ show "You won in 14 moves and 63 seconds!"
  • 4Show moves and time in a HUD line above the board.
memory.html
let moves = 0, seconds = 0, timerId = null;

function startTimer() {
    if (timerId) return;                 // only once
    timerId = setInterval(function() {
        seconds++;
        document.getElementById("time").textContent = seconds + "s";
    }, 1000);
}

function checkWin() {
    if (document.querySelectorAll(".matched").length === 16) {
        clearInterval(timerId);
        document.getElementById("msg").textContent =
            "๐ŸŽ‰ You won in " + moves + " moves and " + seconds + "s!";
    }
}let moves = 0, seconds = 0, timerId = null;

function startTimer() {
    if (timerId) return;                 // only once
    timerId = setInterval(function() {
        seconds++;
        document.getElementById("time").textContent = seconds + "s";
    }, 1000);
}

function checkWin() {
    if (document.querySelectorAll(".matched").length === 16) {
        clearInterval(timerId);
        document.getElementById("msg").textContent =
            "๐ŸŽ‰ You won in " + moves + " moves and " + seconds + "s!";
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
A repeating timer uses with 1000 ms, and is stopped with (timerId). The win condition counts elements with the class.
๐Ÿง 
Knowledge Check
+15 XP
setTimeout vs setInterval, which is which?
AThey are identical
BsetTimeout fires ONCE after a delay (flip-back); setInterval fires REPEATEDLY (the ticking clock)
CsetInterval is newer
6
๐Ÿ†
Difficulty & Best Scores
4ร—4 vs 6ร—6 boards and localStorage records
Locked
๐ŸŽฏ
Goal for this step

Add difficulty levels and persistent best scores per level.

  • 1Difficulty buttons: Easy 4ร—4 (8 pairs) and Hard 6ร—6 (18 pairs, extend the emoji list!). Rebuild the board from a size variable; set the grid columns in code.
  • 2Store bests per mode: keys "memory-best-4" and "memory-best-6", fewest moves wins.
  • 3Show the record next to each button, beat-your-best is the retention loop.
  • 4You just built interface, state, animation and persistence. That IS front-end development. Episode 3 returns to canvas!
memory.html
function newGame(size) {                  // 4 or 6
    board.style.gridTemplateColumns =
        "repeat(" + size + ", 90px)";
    const pairs = (size * size) / 2;
    values = shuffle([...EMOJI.slice(0, pairs),
                      ...EMOJI.slice(0, pairs)]);
    buildBoard(values);
    moves = 0; seconds = 0;
}

// after a win:
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);function newGame(size) {                  // 4 or 6
    board.style.gridTemplateColumns =
        "repeat(" + size + ", 90px)";
    const pairs = (size * size) / 2;
    values = shuffle([...EMOJI.slice(0, pairs),
                      ...EMOJI.slice(0, pairs)]);
    buildBoard(values);
    moves = 0; seconds = 0;
}

// after a win:
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);
โŒจ๏ธ
Code Challenge
+20 XP
Compute the pair count for any board size:
memory.html
const pairs = (size * const pairs = (size * ) / ) / ;;
๐Ÿ’ก Hint: A square board has sizeร—size cards, and every pair covers two of them.
๐Ÿง 
Knowledge Check
+15 XP
The fairest "best score" for memory is fewest moves, not fastest time, becauseโ€ฆ
ATimers are buggy
BMoves measure MEMORY skill; time also measures mouse speed and panic
ClocalStorage prefers integers
โœ… See the complete finished Memory Match , all 6 steps assembled

A self-building card grid, a fair Fisher-Yates shuffle, CSS flip animation, match logic with the lock-during-wait fix, a move counter and timer, a win banner, plus Easy/Hard difficulty and best scores in localStorage , one file. Save it as memory.htmlmemory.html, open it, and play. Stuck on a step? Compare your file against this.

memory.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Memory Match</title>
<style>
  body { background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif;
         display:flex; flex-direction:column; align-items:center; gap:12px; padding:16px; }
  #hud { display:flex; gap:16px; align-items:center; flex-wrap:wrap; justify-content:center; }
  button { background:#2a2d3a; color:#fff; border:1px solid #3a3f52; border-radius:10px;
           padding:8px 14px; font-size:.9rem; cursor:pointer; }
  button:hover { background:#3a3f52; }
  #board { display:grid; gap:10px; justify-content:center; margin:8px auto; }
  .card { height:90px; width:90px; background:#2a2d3a; border-radius:12px;
          display:flex; align-items:center; justify-content:center;
          font-size:2rem; cursor:pointer; color:#fff; transition:transform .35s; }
  .card.flipped { transform:rotateY(180deg); background:#ffd166; color:#222; }
  .card.matched { background:#06d6a0; color:#08221b; cursor:default; }
  #msg { font-size:1.1rem; min-height:1.4em; }
</style>
</head>
<body>
  <h2>๐Ÿง  Memory Match</h2>
  <div id="hud">
    <button onclick="newGame(4)">Easy 4ร—4</button>
    <button onclick="newGame(6)">Hard 6ร—6</button>
    <span>Moves: <b id="moves">0</b></span>
    <span>Time: <b id="time">0s</b></span>
    <span id="best"></span>
  </div>
  <div id="board"></div>
  <div id="msg"></div>

<script>
// 18 emoji so Hard 6x6 (18 pairs) works; Easy 4x4 uses the first 8
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ",
               "๐Ÿถ","๐ŸŒŸ","๐Ÿ•","๐ŸŽธ","๐Ÿง","๐Ÿ”ฅ","๐Ÿ”","๐ŸŽฏ","๐Ÿ™","๐ŸŒˆ"];
const board = document.getElementById("board");
let values = [], first = null, locked = false, moves = 0, seconds = 0, timerId = null, size = 4;

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];      // one-line swap
  }
  return arr;
}

function buildBoard(vals) {
  board.innerHTML = "";
  board.style.gridTemplateColumns = "repeat(" + size + ", 90px)";
  for (const v of vals) {
    const card = document.createElement("div");
    card.className = "card";
    card.dataset.value = v;
    card.textContent = "?";
    board.appendChild(card);
  }
}

function flip(card)   { card.classList.add("flipped");    card.textContent = card.dataset.value; }
function unflip(card) { card.classList.remove("flipped"); card.textContent = "?"; }

function startTimer() {
  if (timerId) return;
  timerId = setInterval(function () {
    seconds++;
    document.getElementById("time").textContent = seconds + "s";
  }, 1000);
}

function showBest() {
  const b4 = localStorage.getItem("memory-best-4") || "โ€”";
  const b6 = localStorage.getItem("memory-best-6") || "โ€”";
  document.getElementById("best").textContent = "Best: 4ร—4 " + b4 + " ยท 6ร—6 " + b6;
}

function checkWin() {
  if (document.querySelectorAll(".matched").length === size * size) {
    clearInterval(timerId); timerId = null;
    const key = "memory-best-" + size;
    const best = Number(localStorage.getItem(key) || 999);
    if (moves < best) localStorage.setItem(key, moves);
    document.getElementById("msg").textContent = "๐ŸŽ‰ You won in " + moves + " moves and " + seconds + "s!";
    showBest();
  }
}

function onCardClick(card) {
  if (locked || card.classList.contains("flipped") || card.classList.contains("matched")) return;
  startTimer();
  flip(card);
  if (!first) { first = card; return; }

  moves++;
  document.getElementById("moves").textContent = moves;

  if (first.dataset.value === card.dataset.value) {
    first.classList.add("matched");
    card.classList.add("matched");
    first = null;
    checkWin();
  } else {
    locked = true;
    setTimeout(function () {
      unflip(first); unflip(card);
      first = null; locked = false;
    }, 800);
  }
}

board.addEventListener("click", function (e) {
  const card = e.target.closest(".card");
  if (card) onCardClick(card);
});

function newGame(n) {
  size = n;
  const pairs = (size * size) / 2;
  values = shuffle([...EMOJI.slice(0, pairs), ...EMOJI.slice(0, pairs)]);
  buildBoard(values);
  moves = 0; seconds = 0; first = null; locked = false;
  clearInterval(timerId); timerId = null;
  document.getElementById("moves").textContent = "0";
  document.getElementById("time").textContent = "0s";
  document.getElementById("msg").textContent = "";
  showBest();
}

newGame(4);
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Memory Match</title>
<style>
  body { background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif;
         display:flex; flex-direction:column; align-items:center; gap:12px; padding:16px; }
  #hud { display:flex; gap:16px; align-items:center; flex-wrap:wrap; justify-content:center; }
  button { background:#2a2d3a; color:#fff; border:1px solid #3a3f52; border-radius:10px;
           padding:8px 14px; font-size:.9rem; cursor:pointer; }
  button:hover { background:#3a3f52; }
  #board { display:grid; gap:10px; justify-content:center; margin:8px auto; }
  .card { height:90px; width:90px; background:#2a2d3a; border-radius:12px;
          display:flex; align-items:center; justify-content:center;
          font-size:2rem; cursor:pointer; color:#fff; transition:transform .35s; }
  .card.flipped { transform:rotateY(180deg); background:#ffd166; color:#222; }
  .card.matched { background:#06d6a0; color:#08221b; cursor:default; }
  #msg { font-size:1.1rem; min-height:1.4em; }
</style>
</head>
<body>
  <h2>๐Ÿง  Memory Match</h2>
  <div id="hud">
    <button onclick="newGame(4)">Easy 4ร—4</button>
    <button onclick="newGame(6)">Hard 6ร—6</button>
    <span>Moves: <b id="moves">0</b></span>
    <span>Time: <b id="time">0s</b></span>
    <span id="best"></span>
  </div>
  <div id="board"></div>
  <div id="msg"></div>

<script>
// 18 emoji so Hard 6x6 (18 pairs) works; Easy 4x4 uses the first 8
const EMOJI = ["๐Ÿ","๐Ÿš€","๐ŸŽ","๐ŸŽฎ","๐Ÿฑ","โšก","๐ŸŒ™","๐ŸŽฒ",
               "๐Ÿถ","๐ŸŒŸ","๐Ÿ•","๐ŸŽธ","๐Ÿง","๐Ÿ”ฅ","๐Ÿ”","๐ŸŽฏ","๐Ÿ™","๐ŸŒˆ"];
const board = document.getElementById("board");
let values = [], first = null, locked = false, moves = 0, seconds = 0, timerId = null, size = 4;

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];      // one-line swap
  }
  return arr;
}

function buildBoard(vals) {
  board.innerHTML = "";
  board.style.gridTemplateColumns = "repeat(" + size + ", 90px)";
  for (const v of vals) {
    const card = document.createElement("div");
    card.className = "card";
    card.dataset.value = v;
    card.textContent = "?";
    board.appendChild(card);
  }
}

function flip(card)   { card.classList.add("flipped");    card.textContent = card.dataset.value; }
function unflip(card) { card.classList.remove("flipped"); card.textContent = "?"; }

function startTimer() {
  if (timerId) return;
  timerId = setInterval(function () {
    seconds++;
    document.getElementById("time").textContent = seconds + "s";
  }, 1000);
}

function showBest() {
  const b4 = localStorage.getItem("memory-best-4") || "โ€”";
  const b6 = localStorage.getItem("memory-best-6") || "โ€”";
  document.getElementById("best").textContent = "Best: 4ร—4 " + b4 + " ยท 6ร—6 " + b6;
}

function checkWin() {
  if (document.querySelectorAll(".matched").length === size * size) {
    clearInterval(timerId); timerId = null;
    const key = "memory-best-" + size;
    const best = Number(localStorage.getItem(key) || 999);
    if (moves < best) localStorage.setItem(key, moves);
    document.getElementById("msg").textContent = "๐ŸŽ‰ You won in " + moves + " moves and " + seconds + "s!";
    showBest();
  }
}

function onCardClick(card) {
  if (locked || card.classList.contains("flipped") || card.classList.contains("matched")) return;
  startTimer();
  flip(card);
  if (!first) { first = card; return; }

  moves++;
  document.getElementById("moves").textContent = moves;

  if (first.dataset.value === card.dataset.value) {
    first.classList.add("matched");
    card.classList.add("matched");
    first = null;
    checkWin();
  } else {
    locked = true;
    setTimeout(function () {
      unflip(first); unflip(card);
      first = null; locked = false;
    }, 800);
  }
}

board.addEventListener("click", function (e) {
  const card = e.target.closest(".card");
  if (card) onCardClick(card);
});

function newGame(n) {
  size = n;
  const pairs = (size * size) / 2;
  values = shuffle([...EMOJI.slice(0, pairs), ...EMOJI.slice(0, pairs)]);
  buildBoard(values);
  moves = 0; seconds = 0; first = null; locked = false;
  clearInterval(timerId); timerId = null;
  document.getElementById("moves").textContent = "0";
  document.getElementById("time").textContent = "0s";
  document.getElementById("msg").textContent = "";
  showBest();
}

newGame(4);
</script>
</body>
</html>
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
DOM building, shuffling, CSS animation and state logic, that is literally web-app development. Episode 3: back to canvas for Breakout!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 3: Breakout โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi