创建一个简单的记忆翻牌游戏

创建一个简单的记忆翻牌游戏,包含以下功能:

  1. 6对卡片随机排列
  2. 点击翻牌效果和匹配音效
  3. 键盘操作支持(Tab聚焦/Enter选择)
  4. 屏幕阅读器友好提示
  5. 胜利动画和音效
XML 复制代码
import { useState, useEffect, useCallback } from 'react';
import { Volume2, VolumeX, Trophy } from 'lucide-react';

const MemoryGame = () => {
  const [cards, setCards] = useState([]);
  const [flipped, setFlipped] = useState([]);
  const [matched, setMatched] = useState([]);
  const [moves, setMoves] = useState(0);
  const [isSoundOn, setIsSoundOn] = useState(true);
  const [gameWon, setGameWon] = useState(false);

  // 初始化卡片数据
  const initGame = useCallback(() => {
    const emojis = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊'];
    const cardPairs = [...emojis, ...emojis];
    const shuffled = cardPairs
      .sort(() => Math.random() - 0.5)
      .map((emoji, index) => ({ id: index, emoji, flipped: false }));
    
    setCards(shuffled);
    setFlipped([]);
    setMatched([]);
    setMoves(0);
    setGameWon(false);
  }, []);

  useEffect(() => {
    initGame();
  }, [initGame]);

  // 播放音效
  const playSound = (soundType) => {
    if (!isSoundOn) return;
    
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
    const oscillator = audioContext.createOscillator();
    const gainNode = audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);
    
    if (soundType === 'flip') {
      oscillator.type = 'sine';
      oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
      gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
    } else if (soundType === 'match') {
      oscillator.type = 'triangle';
      oscillator.frequency.setValueAtTime(660, audioContext.currentTime);
      gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
    } else if (soundType === 'win') {
      oscillator.type = 'sawtooth';
      oscillator.frequency.setValueAtTime(880, audioContext.currentTime);
      gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 1);
    }
    
    oscillator.start();
    oscillator.stop(audioContext.currentTime + (soundType === 'win' ? 1 : 0.5));
  };

  // 处理卡片点击
  const handleCardClick = (id) => {
    if (flipped.length >= 2 || flipped.includes(id) || matched.includes(id) || gameWon) {
      return;
    }
    
    const newFlipped = [...flipped, id];
    setFlipped(newFlipped);
    playSound('flip');
    
    if (newFlipped.length === 2) {
      setMoves(moves + 1);
      const [firstId, secondId] = newFlipped;
      const firstCard = cards.find(card => card.id === firstId);
      const secondCard = cards.find(card => card.id === secondId);
      
      if (firstCard.emoji === secondCard.emoji) {
        setMatched([...matched, firstId, secondId]);
        playSound('match');
        
        // 检查是否胜利
        if (matched.length + 2 === cards.length) {
          setTimeout(() => {
            setGameWon(true);
            playSound('win');
          }, 500);
        }
      } else {
        setTimeout(() => {
          setFlipped([]);
        }, 1000);
      }
    }
  };

  // 键盘导航
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (gameWon) return;
      
      const focusableCards = document.querySelectorAll('.card:not([aria-disabled="true"])');
      const currentFocus = document.activeElement;
      const currentIndex = Array.from(focusableCards).indexOf(currentFocus);
      
      if (e.key === 'Tab') {
        // 默认Tab行为已由浏览器处理
      } else if (e.key === 'Enter' && currentFocus.classList.contains('card')) {
        const id = parseInt(currentFocus.getAttribute('data-id'));
        handleCardClick(id);
      } else if (e.key === 'ArrowRight') {
        e.preventDefault();
        if (currentIndex < focusableCards.length - 1) {
          focusableCards[currentIndex + 1].focus();
        } else {
          focusableCards[0].focus();
        }
      } else if (e.key === 'ArrowLeft') {
        e.preventDefault();
        if (currentIndex > 0) {
          focusableCards[currentIndex - 1].focus();
        } else {
          focusableCards[focusableCards.length - 1].focus();
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [flipped, matched, gameWon, cards]);

  return (
    <div className="min-h-screen bg-gradient-to-br from-indigo-100 to-purple-100 flex flex-col items-center justify-center p-4">
      <div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-2xl">
        <div className="flex justify-between items-center mb-6">
          <h1 className="text-3xl font-bold text-indigo-800">记忆翻牌游戏</h1>
          
          <div className="flex items-center gap-4">
            <div className="bg-indigo-100 px-4 py-2 rounded-full flex items-center gap-2">
              <span className="font-medium">步数:</span>
              <span className="font-bold">{moves}</span>
            </div>
            
            <button
              onClick={() => setIsSoundOn(!isSoundOn)}
              className={`p-2 rounded-full ${isSoundOn ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'}`}
              aria-label={isSoundOn ? '关闭声音' : '开启声音'}
            >
              {isSoundOn ? <Volume2 size={20} /> : <VolumeX size={20} />}
            </button>
          </div>
        </div>
        
        {gameWon && (
          <div className="mb-6 p-4 bg-gradient-to-r from-green-100 to-emerald-100 rounded-xl border border-green-200 flex items-center justify-center gap-2 animate-bounce">
            <Trophy className="text-yellow-500" size={24} />
            <span className="text-xl font-bold text-green-800">恭喜获胜!</span>
          </div>
        )}
        
        <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
          {cards.map((card) => {
            const isFlipped = flipped.includes(card.id) || matched.includes(card.id);
            const isDisabled = matched.includes(card.id) || gameWon;
            
            return (
              <button
                key={card.id}
                data-id={card.id}
                onClick={() => handleCardClick(card.id)}
                disabled={isDisabled}
                className={`card relative aspect-square rounded-xl transition-all duration-300 transform ${
                  isFlipped 
                    ? 'bg-white rotate-y-180 scale-105 shadow-md' 
                    : 'bg-gradient-to-br from-indigo-500 to-purple-600 rotate-y-0 hover:scale-105'
                } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
                tabIndex={isDisabled ? -1 : 0}
                aria-disabled={isDisabled}
                aria-label={isFlipped ? `已翻开的卡片,图案是${card.emoji}` : '未翻开的卡片'}
              >
                <div className={`absolute inset-0 flex items-center justify-center transition-transform duration-500 transform-style-preserve-3d ${
                  isFlipped ? 'rotate-y-180' : ''
                }`}>
                  <div className="back-face absolute inset-0 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
                    <span className="text-white text-4xl">?</span>
                  </div>
                  <div className="front-face absolute inset-0 bg-white rounded-xl flex items-center justify-center rotate-y-180">
                    <span className="text-4xl">{card.emoji}</span>
                  </div>
                </div>
              </button>
            );
          })}
        </div>
        
        <div className="mt-8 text-center">
          <button
            onClick={initGame}
            className="px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-full font-medium hover:from-indigo-600 hover:to-purple-700 transition-all shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
          >
            重新开始
          </button>
        
          <div className="mt-4 text-sm text-gray-600">
            <p className="mb-1">操作说明:</p>
            <ul className="list-disc list-inside text-left">
              <li>使用鼠标点击或键盘方向键导航</li>
              <li>按Enter键翻开卡片</li>
              <li>找到所有匹配的卡片对</li>
            </ul>
          </div>
        </div>
      </div>
      
      <style jsx>{`
        .transform-style-preserve-3d {
          transform-style: preserve-3d;
        }
        .rotate-y-0 {
          transform: rotateY(0deg);
        }
        .rotate-y-180 {
          transform: rotateY(180deg);
        }
        .card:focus-visible {
          outline: 3px solid #4f46e5;
          outline-offset: 2px;
        }
      `}</style>
    </div>
  );
};

export default MemoryGame;
相关推荐
兔儿资源2 小时前
游戏包站GM手游
游戏
春卷同学2 小时前
基于Electron开发的跨平台鸿蒙PC剪刀石头布游戏应用
游戏·electron·harmonyos
萌萌的提莫队长2 小时前
Unity试玩广告-PlayWork-麻将消消乐游戏
游戏
春卷同学3 小时前
基于Electron开发的跨平台鸿蒙PC找不同游戏应用
javascript·游戏·electron
Minecraft红客3 小时前
HTMLminecraft版本
游戏·3d·html·娱乐
春卷同学3 小时前
基于Electron开发的跨平台鸿蒙PC经典吃豆人游戏
游戏·electron·harmonyos
春卷同学3 小时前
赛车游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
春卷同学4 小时前
跑酷游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
春卷同学4 小时前
水上摩托游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos