A memory card game — 16 shuffled emoji cards, flip animation, match detection, move counter, and a completion timer. Pure HTML, CSS, and JavaScript.
Create 8 pairs of emoji cards, double the array, and shuffle with the Fisher-Yates algorithm.
const EMOJIS = ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼'];
function createDeck() {
// Duplicate each emoji to make pairs
const deck = [...EMOJIS, ...EMOJIS];
// Fisher-Yates shuffle
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
console.log(createDeck());
// ['🐻','🐰','🦊','🐶', ...] — random order every time
CSS 3D transform creates a realistic card flip. The card has a front and back face.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Game</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',sans-serif; background:#0f172a; color:#fff; min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:24px; }
h1 { font-size:1.4rem; margin-bottom:12px; }
.stats { display:flex; gap:32px; margin-bottom:20px; font-size:0.95rem; color:#94a3b8; }
.stats b { color:#fff; }
.board { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; max-width:400px; width:100%; }
.card { aspect-ratio:1; perspective:600px; cursor:pointer; }
.card-inner { width:100%; height:100%; position:relative; transform-style:preserve-3d; transition:transform 0.5s; }
.card.flipped .card-inner { transform:rotateY(180deg); }
.card-face { position:absolute; inset:0; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:2rem; backface-visibility:hidden; }
.card-back { background:#1e293b; border:2px solid #334155; }
.card-back::before { content:'?'; font-size:1.6rem; color:#475569; font-weight:800; }
.card-front { background:#162447; border:2px solid #3b82f6; transform:rotateY(180deg); }
.card.matched .card-front { background:#162d1e; border-color:#22c55e; }
.btn { margin-top:20px; padding:11px 28px; background:#3b82f6; border:none; border-radius:10px; color:#fff; font-size:0.95rem; font-weight:700; cursor:pointer; }
</style>
</head>
<body>
<?php require_once __DIR__ . '/../promo_banner.php'; ?>
<h1>Memory Card Game</h1>
<div class="stats">
<span>Moves: <b id="moves">0</b></span>
<span>Matches: <b id="matches">0</b>/8</span>
<span>Time: <b id="timer">0s</b></span>
</div>
<div class="board" id="board"></div>
<button class="btn" onclick="startGame()">New Game</button>
<script src="game.js"></script>
</body>
</html>
Track flipped cards, check for matches, and handle the lock during animations.
// game.js
const EMOJIS = ['dog','cat','mouse','hamster','rabbit','fox','bear','panda']
.map(e => ({dog:'🐶',cat:'🐱',mouse:'🐭',hamster:'🐹',rabbit:'🐰',fox:'🦊',bear:'🐻',panda:'🐼'})[e]);
let flipped = [], matched = 0, moves = 0, locked = false, timerRef, seconds = 0;
function createDeck() {
const d = [...EMOJIS,...EMOJIS];
for (let i=d.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[d[i],d[j]]=[d[j],d[i]];}
return d;
}
function startGame() {
clearInterval(timerRef); flipped=[]; matched=0; moves=0; locked=false; seconds=0;
document.getElementById('moves').textContent = 0;
document.getElementById('matches').textContent = 0;
document.getElementById('timer').textContent = '0s';
const board = document.getElementById('board');
board.innerHTML = '';
createDeck().forEach((emoji, i) => {
const card = document.createElement('div');
card.className = 'card';
card.dataset.emoji = emoji;
card.innerHTML = `<div class="card-inner"><div class="card-face card-back"></div><div class="card-face card-front">${emoji}</div></div>`;
card.addEventListener('click', () => flipCard(card));
board.appendChild(card);
});
timerRef = setInterval(() => {
seconds++;
document.getElementById('timer').textContent = seconds + 's';
}, 1000);
}
function flipCard(card) {
if (locked || card.classList.contains('flipped') || card.classList.contains('matched')) return;
card.classList.add('flipped');
flipped.push(card);
if (flipped.length === 2) {
moves++;
document.getElementById('moves').textContent = moves;
locked = true;
const [a, b] = flipped;
if (a.dataset.emoji === b.dataset.emoji) {
a.classList.add('matched'); b.classList.add('matched');
matched++;
document.getElementById('matches').textContent = matched;
flipped = []; locked = false;
if (matched === EMOJIS.length) {
clearInterval(timerRef);
setTimeout(() => alert(`You won! ${moves} moves in ${seconds}s`), 300);
}
} else {
setTimeout(() => {
a.classList.remove('flipped'); b.classList.remove('flipped');
flipped = []; locked = false;
}, 1000);
}
}
}
startGame();
You implemented three classic programming patterns in one game: the Fisher-Yates shuffle, state machine game logic (locked/unlocked), and CSS 3D transforms. The same flip-card technique is used for interactive flashcard apps and product showcase carousels.
Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.