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, statistics

2. 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

  1. Game Logic: Implementing complex game rules and state management
  2. Animations: CSS transitions and transform effects
  3. Performance: Memoization and efficient state updates
  4. User Experience: Responsive design and accessibility
  5. 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!