Memory Game
by Pratik Rai
Read the full write-upBuild a memory game: sixteen face-down cards, eight matching pairs. Flip two — if they match they stay up, if not they flip back after a short pause.
The shuffled deck is given to you.
Requirements
All cards start face down. Clicking one turns it face up.
When two cards are face up, compare them after roughly 700ms — matching cards stay up, others flip back.
While two cards are being compared, clicking a third does nothing.
Clicking an already-matched card, or the card that is already face up, does nothing.
Show the move count and how many pairs are matched, and say something when all eight are found.
New game reshuffles and clears everything.
Notes
A face-down card should render no symbol at all — hiding it with colour still leaves it in the DOM for anyone who looks.
Card flip animations are out of scope.
Hints
Memory Game (reference solution)
One way to do it, not the only way. Yours passes if it renders.
Solution files
1import { useState, useEffect } from 'react';
2
3const SYMBOLS = ['🌵', '🍄', '🌊', '⭐', '🔥', '🌙', '🍋', '🐚'];
4
5// A shuffled deck of 16 cards: every symbol twice. Fisher-Yates, given to you.
6function buildDeck() {
7 const cards = SYMBOLS.flatMap((symbol, i) => [
8 { id: i + '-a', symbol },
9 { id: i + '-b', symbol },
10 ]);
11 for (let i = cards.length - 1; i > 0; i--) {
12 const j = Math.floor(Math.random() * (i + 1));
13 const swap = cards[i];
14 cards[i] = cards[j];
15 cards[j] = swap;
16 }
17 return cards;
18}
19
20const PEEK_MS = 700;
21
22export default function App() {
23 const [deck, setDeck] = useState(buildDeck);
24 const [flipped, setFlipped] = useState([]); // ids being compared, 0-2 of them
25 const [matched, setMatched] = useState(() => new Set()); // matched symbols
26 const [moves, setMoves] = useState(0);
27
28 // The pause is the whole point of the game — the player has to see the second
29 // card before it goes away. An effect keyed on `flipped` runs the comparison
30 // once per pair, and the cleanup cancels it if a new game starts mid-pause.
31 useEffect(() => {
32 if (flipped.length !== 2) return undefined;
33 const [a, b] = flipped.map((id) => deck.find((card) => card.id === id));
34 const timer = setTimeout(() => {
35 if (a && b && a.symbol === b.symbol) {
36 setMatched((prev) => new Set(prev).add(a.symbol));
37 }
38 setFlipped([]);
39 }, PEEK_MS);
40 return () => clearTimeout(timer);
41 }, [flipped, deck]);
42
43 const handleFlip = (card) => {
44 if (matched.has(card.symbol)) return;
45 if (flipped.includes(card.id)) return;
46 if (flipped.length === 2) return; // a comparison is already running
47 setFlipped((prev) => [...prev, card.id]);
48 if (flipped.length === 1) setMoves((n) => n + 1);
49 };
50
51 const reset = () => {
52 setDeck(buildDeck());
53 setFlipped([]);
54 setMatched(new Set());
55 setMoves(0);
56 };
57
58 const won = matched.size === SYMBOLS.length;
59
60 return (
61 <div>
62 <h1>Memory</h1>
63
64 <div className="meta">
65 <span>Moves: {moves}</span>
66 <span>Matched: {matched.size} / {SYMBOLS.length}</span>
67 </div>
68
69 <div className="grid">
70 {deck.map((card) => {
71 const isMatched = matched.has(card.symbol);
72 const isUp = isMatched || flipped.includes(card.id);
73 return (
74 <button
75 key={card.id}
76 className={
77 'card' + (isUp ? ' card-up' : '') + (isMatched ? ' card-matched' : '')
78 }
79 onClick={() => handleFlip(card)}
80 disabled={isMatched}
81 aria-label={isUp ? card.symbol : 'Hidden card'}
82 >
83 {isUp ? card.symbol : ''}
84 </button>
85 );
86 })}
87 </div>
88
89 <button className="reset" onClick={reset}>New game</button>
90
91 {won && <p className="won" role="status">Solved in {moves} moves.</p>}
92 </div>
93 );
94}How it works
The instinct is to compare the two cards inside the click handler, and it does not work: at the moment of the click, state still holds the previous value, so the second card has not been recorded yet. Moving the comparison into an effect keyed on the flipped array lets React finish the render first — the player sees the second card, and only then does the timer decide.
The cleanup on that timer is not decoration. Press "New game" during the pause and without it the pending callback still fires against the fresh deck, flipping away cards the player never touched.
Matched cards are tracked by symbol rather than by id, because a pair is matched together and there is no case where one half is matched and the other is not. It also makes the win condition a size comparison instead of a division.