Memory Game with React
Master machine coding by building a card memory game with React, implementing game logic, animations, and state management.
Machine Coding Challenge: Memory Game
Create a production-ready memory game that demonstrates advanced React patterns, state management, animations, and real-world game development concepts.
Problem Statement
Design and implement a Memory Game with the following requirements:
✅ Core Features (Must Have)
- Shuffled deck of paired cards (6 pairs = 12 cards)
- Flip cards on click (maximum 2 cards visible at a time)
- Match detection and card removal
- Move counter and score tracking
- Game completion detection
- Restart functionality
- Responsive grid layout
Advanced Features (Nice to Have)
- Card flip animations with CSS transitions
- Timer and best time tracking
- Difficulty levels (easy: 6 pairs, medium: 8 pairs, hard: 10 pairs)
- Sound effects for matches and flips
- Local storage for high scores
- Accessibility features (keyboard navigation, screen reader support)
- Dark/Light theme toggle
UI/UX Requirements
- Clean, modern card design
- Smooth flip animations
- Loading states and error handling
- Game completion modal
- Responsive design for mobile and desktop
- Accessibility compliance (ARIA labels, keyboard navigation)
🧠 Clarifying Questions & Scope
Before diving into implementation, clarify these aspects:
Functional Requirements
- Card Structure: What data should each card have? (type, image, id?)
- Grid Layout: How many cards per row? (3x4, 4x3, or dynamic?)
- Match Logic: Should we support different matching criteria?
- Scoring: How should we calculate and display scores?
- Persistence: Should we save game state in localStorage?
Technical Constraints
- Time Limit: 2-3 hours for implementation
- Framework: React with hooks
- Styling: CSS Grid/Flexbox or Tailwind CSS?
- Animations: CSS transitions or Framer Motion?
Success Criteria
- Clean, maintainable code structure
- Proper game logic and state management
- Smooth animations and transitions
- Responsive and accessible design
- Performance optimization for smooth gameplay
🏗️ High-Level Architecture
1. Component Structure
MemoryGame/
├── GameBoard/ # Main game container
│ ├── Card/ # Individual card component
│ └── CardGrid/ # Grid layout container
├── GameHeader/ # Score, moves, timer
├── GameControls/ # Restart, difficulty, settings
├── GameModal/ # Completion modal
└── GameStats/ # High scores, statistics2. State Management Strategy
interface Card {
id: string;
type: string;
image: string;
isFlipped: boolean;
isMatched: boolean;
isDisabled: boolean;
}
interface GameState {
cards: Card[];
openCards: number[];
matchedPairs: string[];
moves: number;
timer: number;
isGameComplete: boolean;
isGameStarted: boolean;
difficulty: 'easy' | 'medium' | 'hard';
bestScore: number;
bestTime: number;
}3. Game Logic Flow
Game Start → Shuffle Cards → Render Grid → User Clicks → Flip Card → Check Match → Update State → Check Win → Show Modal🛠️ Step-by-Step Implementation
Step 1: Core Game Component
// components/MemoryGame.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { GameBoard } from './GameBoard';
import { GameHeader } from './GameHeader';
import { GameControls } from './GameControls';
import { GameModal } from './GameModal';
import { useGameLogic } from '../hooks/useGameLogic';
import { useLocalStorage } from '../hooks/useLocalStorage';
interface MemoryGameProps {
difficulty?: 'easy' | 'medium' | 'hard';
}
const MemoryGame: React.FC<MemoryGameProps> = ({
difficulty = 'easy'
}) => {
const {
cards,
openCards,
matchedPairs,
moves,
timer,
isGameComplete,
isGameStarted,
handleCardClick,
handleRestart,
handleDifficultyChange
} = useGameLogic(difficulty);
const [bestScore, setBestScore] = useLocalStorage('memory-game-best-score', 0);
const [bestTime, setBestTime] = useLocalStorage('memory-game-best-time', 0);
// Update best scores when game completes
useEffect(() => {
if (isGameComplete) {
if (moves < bestScore || bestScore === 0) {
setBestScore(moves);
}
if (timer < bestTime || bestTime === 0) {
setBestTime(timer);
}
}
}, [isGameComplete, moves, timer, bestScore, bestTime, setBestScore, setBestTime]);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
<div className="max-w-4xl mx-auto p-6">
<GameHeader
moves={moves}
timer={timer}
isGameStarted={isGameStarted}
bestScore={bestScore}
bestTime={bestTime}
/>
<GameControls
onRestart={handleRestart}
difficulty={difficulty}
onDifficultyChange={handleDifficultyChange}
/>
<GameBoard
cards={cards}
openCards={openCards}
onCardClick={handleCardClick}
/>
{isGameComplete && (
<GameModal
moves={moves}
timer={timer}
onRestart={handleRestart}
/>
)}
</div>
</div>
);
};Step 2: Custom Hooks for Game Logic
// hooks/useGameLogic.ts
import { useState, useEffect, useCallback } from 'react';
import { generateCards, shuffleCards } from '../utils/cardUtils';
interface UseGameLogicProps {
difficulty: 'easy' | 'medium' | 'hard';
}
export function useGameLogic({ difficulty }: UseGameLogicProps) {
const [cards, setCards] = useState<Card[]>([]);
const [openCards, setOpenCards] = useState<number[]>([]);
const [matchedPairs, setMatchedPairs] = useState<string[]>([]);
const [moves, setMoves] = useState(0);
const [timer, setTimer] = useState(0);
const [isGameComplete, setIsGameComplete] = useState(false);
const [isGameStarted, setIsGameStarted] = useState(false);
// Initialize game
useEffect(() => {
initializeGame();
}, [difficulty]);
// Timer logic
useEffect(() => {
let interval: NodeJS.Timeout;
if (isGameStarted && !isGameComplete) {
interval = setInterval(() => {
setTimer(prev => prev + 1);
}, 1000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isGameStarted, isGameComplete]);
// Check for game completion
useEffect(() => {
if (matchedPairs.length === cards.length / 2 && cards.length > 0) {
setIsGameComplete(true);
setIsGameStarted(false);
}
}, [matchedPairs, cards.length]);
const initializeGame = useCallback(() => {
const cardCount = difficulty === 'easy' ? 6 : difficulty === 'medium' ? 8 : 10;
const newCards = generateCards(cardCount);
const shuffledCards = shuffleCards(newCards);
setCards(shuffledCards);
setOpenCards([]);
setMatchedPairs([]);
setMoves(0);
setTimer(0);
setIsGameComplete(false);
setIsGameStarted(false);
}, [difficulty]);
const handleCardClick = useCallback((cardIndex: number) => {
// Prevent clicking if card is already open or matched
if (openCards.includes(cardIndex) || cards[cardIndex].isMatched) {
return;
}
// Start game on first click
if (!isGameStarted) {
setIsGameStarted(true);
}
// Open the card
const newOpenCards = [...openCards, cardIndex];
setOpenCards(newOpenCards);
// If two cards are open, check for match
if (newOpenCards.length === 2) {
const [firstIndex, secondIndex] = newOpenCards;
const firstCard = cards[firstIndex];
const secondCard = cards[secondIndex];
setMoves(prev => prev + 1);
if (firstCard.type === secondCard.type) {
// Match found
setMatchedPairs(prev => [...prev, firstCard.type]);
setOpenCards([]);
} else {
// No match, close cards after delay
setTimeout(() => {
setOpenCards([]);
}, 1000);
}
}
}, [cards, openCards, isGameStarted]);
const handleRestart = useCallback(() => {
initializeGame();
}, [initializeGame]);
const handleDifficultyChange = useCallback((newDifficulty: 'easy' | 'medium' | 'hard') => {
// Update difficulty and restart game
// This would typically be handled by parent component
}, []);
return {
cards,
openCards,
matchedPairs,
moves,
timer,
isGameComplete,
isGameStarted,
handleCardClick,
handleRestart,
handleDifficultyChange
};
}Step 3: Card Component with Animations
// components/Card.tsx
import React from 'react';
import { Card as CardType } from '../types';
interface CardProps {
card: CardType;
isOpen: boolean;
isMatched: boolean;
isDisabled: boolean;
onClick: () => void;
}
const Card: React.FC<CardProps> = ({
card,
isOpen,
isMatched,
isDisabled,
onClick
}) => {
return (
<div
className={`
relative w-24 h-32 md:w-32 md:h-40 cursor-pointer
transform transition-all duration-300 ease-in-out
${isDisabled ? 'pointer-events-none' : 'hover:scale-105'}
${isMatched ? 'opacity-50' : ''}
`}
onClick={onClick}
role="button"
tabIndex={isDisabled ? -1 : 0}
aria-label={`Card ${card.type}`}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
<div
className={`
absolute inset-0 bg-white dark:bg-gray-800 rounded-lg shadow-lg
border-2 border-gray-200 dark:border-gray-600
transform transition-transform duration-300
${isOpen ? 'rotate-y-180' : ''}
`}
>
{/* Card Back */}
<div
className={`
absolute inset-0 flex items-center justify-center
bg-gradient-to-br from-blue-500 to-purple-600
rounded-lg
${isOpen ? 'opacity-0' : 'opacity-100'}
transition-opacity duration-300
`}
>
<div className="text-white text-2xl font-bold">?</div>
</div>
{/* Card Front */}
<div
className={`
absolute inset-0 flex items-center justify-center
bg-white dark:bg-gray-800 rounded-lg
${isOpen ? 'opacity-100' : 'opacity-0'}
transition-opacity duration-300
`}
>
<div className="text-center">
<div className="text-3xl mb-2">{card.image}</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
{card.type}
</div>
</div>
</div>
</div>
</div>
);
};Step 4: Game Board Component
// components/GameBoard.tsx
import React from 'react';
import { Card } from './Card';
import { Card as CardType } from '../types';
interface GameBoardProps {
cards: CardType[];
openCards: number[];
onCardClick: (index: number) => void;
}
const GameBoard: React.FC<GameBoardProps> = ({
cards,
openCards,
onCardClick
}) => {
const getGridCols = () => {
const cardCount = cards.length;
if (cardCount <= 12) return 'grid-cols-3 md:grid-cols-4';
if (cardCount <= 16) return 'grid-cols-4 md:grid-cols-4';
return 'grid-cols-5 md:grid-cols-5';
};
return (
<div className="mt-8">
<div className={`
grid gap-4 justify-items-center
${getGridCols()}
max-w-2xl mx-auto
`}>
{cards.map((card, index) => (
<Card
key={card.id}
card={card}
isOpen={openCards.includes(index)}
isMatched={card.isMatched}
isDisabled={openCards.length === 2}
onClick={() => onCardClick(index)}
/>
))}
</div>
</div>
);
};Step 5: Utility Functions
// utils/cardUtils.ts
import { Card } from '../types';
// Card data for different themes
const cardThemes = {
pokemon: ['Pikachu', 'Charizard', 'Blastoise', 'Venusaur', 'Gyarados', 'Dragonite'],
emoji: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'],
numbers: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
letters: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
};
export function generateCards(pairCount: number, theme: keyof typeof cardThemes = 'emoji'): Card[] {
const themes = cardThemes[theme];
const selectedThemes = themes.slice(0, pairCount);
const cards: Card[] = [];
selectedThemes.forEach((type, index) => {
// Create two cards for each type (pair)
for (let i = 0; i < 2; i++) {
cards.push({
id: `${type}-${index}-${i}`,
type,
image: type,
isFlipped: false,
isMatched: false,
isDisabled: false
});
}
});
return cards;
}
// Fisher-Yates shuffle algorithm
export function shuffleCards(cards: Card[]): Card[] {
const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
export function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}Step 6: Game Header and Controls
// components/GameHeader.tsx
import React from 'react';
import { Clock, Target, Trophy } from 'lucide-react';
interface GameHeaderProps {
moves: number;
timer: number;
isGameStarted: boolean;
bestScore: number;
bestTime: number;
}
const GameHeader: React.FC<GameHeaderProps> = ({
moves,
timer,
isGameStarted,
bestScore,
bestTime
}) => {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2 text-gray-600 dark:text-gray-400">
<Target className="w-5 h-5" />
<span className="text-sm font-medium">Moves</span>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{moves}
</div>
{bestScore > 0 && (
<div className="text-xs text-gray-500">
Best: {bestScore}
</div>
)}
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-2 text-gray-600 dark:text-gray-400">
<Clock className="w-5 h-5" />
<span className="text-sm font-medium">Time</span>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{formatTime(timer)}
</div>
{bestTime > 0 && (
<div className="text-xs text-gray-500">
Best: {formatTime(bestTime)}
</div>
)}
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-2 text-gray-600 dark:text-gray-400">
<Trophy className="w-5 h-5" />
<span className="text-sm font-medium">Score</span>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{calculateScore(moves, timer)}
</div>
</div>
<div className="text-center">
<div className="text-sm font-medium text-gray-600 dark:text-gray-400">
Status
</div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{isGameStarted ? 'Playing' : 'Ready'}
</div>
</div>
</div>
</div>
);
};
function calculateScore(moves: number, time: number): number {
// Simple scoring algorithm
const baseScore = 1000;
const movePenalty = moves * 10;
const timePenalty = time * 2;
return Math.max(0, baseScore - movePenalty - timePenalty);
}Step 7: Game Modal for Completion
// components/GameModal.tsx
import React from 'react';
import { Trophy, RefreshCw, Share } from 'lucide-react';
interface GameModalProps {
moves: number;
timer: number;
onRestart: () => void;
}
const GameModal: React.FC<GameModalProps> = ({
moves,
timer,
onRestart
}) => {
const score = calculateScore(moves, timer);
const formattedTime = formatTime(timer);
const handleShare = () => {
const text = `I completed the Memory Game in ${moves} moves and ${formattedTime}! Can you beat my score?`;
if (navigator.share) {
navigator.share({
title: 'Memory Game Score',
text
});
} else {
// Fallback to copying to clipboard
navigator.clipboard.writeText(text);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-md w-full mx-4">
<div className="text-center">
<div className="w-16 h-16 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center mx-auto mb-4">
<Trophy className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Congratulations!
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
You've completed the Memory Game!
</p>
<div className="space-y-3 mb-6">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Moves:</span>
<span className="font-semibold">{moves}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Time:</span>
<span className="font-semibold">{formattedTime}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Score:</span>
<span className="font-semibold text-green-600">{score}</span>
</div>
</div>
<div className="flex gap-3">
<button
onClick={onRestart}
className="flex-1 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Play Again
</button>
<button
onClick={handleShare}
className="flex-1 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors flex items-center justify-center gap-2"
>
<Share className="w-4 h-4" />
Share
</button>
</div>
</div>
</div>
</div>
);
};UI/UX Enhancements
Responsive Design
/* Tailwind classes for responsive memory game */
.game-container {
@apply min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800;
}
.card-grid {
@apply grid gap-4 justify-items-center max-w-2xl mx-auto;
}
@media (max-width: 640px) {
.card-grid {
@apply grid-cols-3 gap-2;
}
.card {
@apply w-20 h-28;
}
}Accessibility Features
// Enhanced accessibility with ARIA labels
const Card: React.FC<CardProps> = ({ card, isOpen, isMatched, isDisabled, onClick }) => {
return (
<div
role="button"
tabIndex={isDisabled ? -1 : 0}
aria-label={`Card ${card.type}${isMatched ? ' - Matched' : ''}`}
aria-pressed={isOpen}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
className="card"
>
{/* Card content */}
</div>
);
};Performance Optimizations
Memoization for Card Rendering
// Optimized Card component with React.memo
const Card = React.memo<CardProps>(({ card, isOpen, isMatched, isDisabled, onClick }) => {
const handleClick = useCallback(() => {
if (!isDisabled) {
onClick();
}
}, [isDisabled, onClick]);
return (
<div
className={`card ${isOpen ? 'flipped' : ''} ${isMatched ? 'matched' : ''}`}
onClick={handleClick}
>
{/* Card content */}
</div>
);
});
// Optimized game board with useMemo
const GameBoard: React.FC<GameBoardProps> = ({ cards, openCards, onCardClick }) => {
const renderedCards = useMemo(() => {
return cards.map((card, index) => (
<Card
key={card.id}
card={card}
isOpen={openCards.includes(index)}
isMatched={card.isMatched}
isDisabled={openCards.length === 2}
onClick={() => onCardClick(index)}
/>
));
}, [cards, openCards, onCardClick]);
return (
<div className="game-board">
{renderedCards}
</div>
);
};Efficient State Updates
// Optimized state updates with useCallback
const handleCardClick = useCallback((cardIndex: number) => {
if (openCards.includes(cardIndex) || cards[cardIndex].isMatched) {
return;
}
setOpenCards(prev => [...prev, cardIndex]);
if (openCards.length === 1) {
const [firstIndex] = openCards;
const firstCard = cards[firstIndex];
const secondCard = cards[cardIndex];
setMoves(prev => prev + 1);
if (firstCard.type === secondCard.type) {
setMatchedPairs(prev => [...prev, firstCard.type]);
setOpenCards([]);
} else {
setTimeout(() => {
setOpenCards([]);
}, 1000);
}
}
}, [cards, openCards]);Key Takeaways
What You've Learned
- Game Logic: Implementing complex game rules and state management
- Animations: CSS transitions and transform effects
- Performance: Memoization and efficient state updates
- User Experience: Responsive design and accessibility
- Code Architecture: Modular component structure and custom hooks
Common Pitfalls to Avoid
- ❌ Poor State Management: Avoid complex nested state updates
- ❌ No Animations: Missing smooth transitions and visual feedback
- ❌ Accessibility Issues: Forgetting keyboard navigation and ARIA labels
- ❌ Performance Problems: Not optimizing for large card sets
- ❌ Bad UX: No loading states or error handling
Next Steps
- Implement sound effects and audio feedback
- Add multiplayer functionality
- Create different card themes and customization
- Add analytics and game statistics
- Implement progressive web app features
This Memory Game demonstrates advanced React patterns and real-world game development concepts. The skills you've learned here are essential for building interactive applications and understanding complex state management scenarios!
Job Board Component with Hacker News API
Master machine coding by building a job board component that fetches and displays job postings from the Hacker News API with pagination, loading states, and responsive design.
Frontend Machine Coding: Build a Dynamic Modal Component
Learn how to build a flexible modal component with smooth animations, accessibility features, and configurable behavior using React