使用 React 构建纸牌记忆游戏

2025-06-10

使用 React 构建纸牌记忆游戏

我们经常会遇到一些小游戏,然后琢磨它到底有多复杂?我们能做出来吗?通常情况下,我们不会突破这个界限。然而,在这篇文章中,我们将构建一个简单的记忆游戏,它既易于上手,也易于开发。

纸牌记忆游戏是一款测试玩家记忆力的简单游戏。玩家需要在一副成对的纸牌中,连续轮次选择一对匹配的纸牌。当所有匹配的纸牌都选中时,玩家获胜。

它的简单 UI 可能看起来像这样:

我们最终演示的 UI 参考

让我们定义游戏规则

除非我们了解规则,否则我们无法制作游戏。因此,我们先来解释一下规则:

  • 我们需要一副洗好的牌。牌堆里每张牌都必须有一对。

  • 游戏必须翻转玩家点击的牌。每次最多显示两张牌。

  • 游戏会处理匹配和不匹配的卡牌。不匹配的卡牌会在短暂时间后翻回。匹配的卡牌会从牌堆中移除。

  • 每当玩家选择一对时,游戏就会增加当前的移动次数

  • 一旦找到所有配对,玩家就会看到带有分数的确认对话框。

  • 游戏提供了重启功能。

那么我们还在等什么...让我们开始吧。

我们首先定义卡片结构。对于卡片,我们创建一个具有 type 属性和图像源的对象。

{
   type: 'Pickachu',
   image: require('../images/Pickachu.png')
}
Enter fullscreen mode Exit fullscreen mode

下一步是洗牌。啊,是的,这是最重要的一步。如果我们不洗牌,那就不是真正的记忆游戏了。

1. 随机播放

我将使用Fisher-Yates 洗牌算法来洗牌数组。

// Fisher Yates Shuffle
function swap(array, i, j) {
   const temp = array[i];
   array[i] = array[j];
   array[j] = temp;
}
function shuffleCards(array) {
   const length = array.length;
   for (let i = length; i > 0; i--) {
      const randomIndex = Math.floor(Math.random() * i);
      const currentIndex = i - 1;
      swap(array, currIndex, randomIndex)
   }
   return array;
}
Enter fullscreen mode Exit fullscreen mode

2. 渲染纸牌的牌面

在这个例子中,我们使用了 12 张牌(6 对)。洗牌后,我们将它们渲染为 3x4 的网格。您可以选择将牌组拆分成 3 个数组,每个数组包含 4 个元素,并使用嵌套映射进行渲染,或者使用 CSS 弹性框或网格进行渲染。我将使用 CSS 网格进行渲染,因为一维数组更容易处理更新。


export default function App({ uniqueCardsArray }) {
  const [cards, setCards] = useState(
    () => shuffleCards(uniqueCardsArray.concat(uniqueCardsArray))
  );

  const handleCardClick = (index) => {
    // We will handle it later
  };


  return (
    <div className="App">
      <header>
        <h3>Play the Flip card game</h3>
        <div>
          Select two cards with same content consequtively to make them vanish
        </div>
      </header>
      <div className="container">
        {cards.map((card, index) => {
          return (
            <Card
              key={index}
              card={card}
              index={index}
              onClick={handleCardClick}
            />
          );
        })}
      </div>
   </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
  .container {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: repeat(3, 1fr);
    justify-items: center;
    align-items: stretch;
    gap: 1rem;
  }
Enter fullscreen mode Exit fullscreen mode

3. 翻牌、评估比赛并计算步数

下一步是为用户提供翻转卡片并评估是否匹配的交互。为此,我们维护以下状态

  • openCards跟踪玩家已经翻过的牌

  • clearedCards跟踪已匹配且需要从牌堆中移除的牌

  • 移动来跟踪玩家的动作。

import { useEffect, useState, useRef } from "react";
import Card from "./card";
import uniqueElementsArray from './data';
import "./app.scss";

export default function App() {
  const [cards, setCards] = useState(
    () => shuffleCards(uniqueCardsArray.concat(uniqueCardsArray))
  );
  const [openCards, setOpenCards] = useState([]);
  const [clearedCards, setClearedCards] = useState({});
  const [moves, setMoves] = useState(0);
  const [showModal, setShowModal] = useState(false);
  const timeout = useRef(null);

  // Check if both the cards have same type. If they do, mark them inactive
  const evaluate = () => {
    const [first, second] = openCards;
    if (cards[first].type === cards[second].type) {
      setClearedCards((prev) => ({ ...prev, [cards[first].type]: true }));
      setOpenCards([]);
      return;
    }
    // Flip cards after a 500ms duration
    timeout.current = setTimeout(() => {
      setOpenCards([]);
    }, 500);
  };

  const handleCardClick = (index) => {
    // Have a maximum of 2 items in array at once.
    if (openCards.length === 1) {
      setOpenCards((prev) => [...prev, index]);
      // increase the moves once we opened a pair
      setMoves((moves) => moves + 1);
    } else {
      // If two cards are already open, we cancel timeout set for flipping cards back
      clearTimeout(timeout.current);
      setOpenCards([index]);
    }
  };

  useEffect(() => {
    if (openCards.length === 2) {
      setTimeout(evaluate, 500);
    }
  }, [openCards]);

  const checkIsFlipped = (index) => {
    return openCards.includes(index);
  };

  const checkIsInactive = (card) => {
    return Boolean(clearedCards[card.type]);
  };

  return (
    <div className="App">
      <header>
        <h3>Play the Flip card game</h3>
        <div>
          Select two cards with same content consequtively to make them vanish
        </div>
      </header>
      <div className="container">
        {cards.map((card, index) => {
          return (
            <Card
              key={index}
              card={card}
              index={index}
              isDisabled={shouldDisableAllCards}
              isInactive={checkIsInactive(card)}
              isFlipped={checkIsFlipped(index)}
              onClick={handleCardClick}
            />
          );
        })}
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

我们一次最多只能在openCards状态中保留两张卡片。由于我们使用的是静态数组,并且实际上并没有从原始 cards 数组中删除任何内容,因此我们只需将已打开卡片的索引存储在openCards状态中即可。根据openCards 和 clearedCards状态,我们分别将isFlippedisInactive属性传递给 Card 组件,然后组件将使用该属性添加相应的类。

不妨看看这篇精彩的博客,它解释了如何处理翻转卡片动画。
注意:由于我们在卡片中添加了翻转动画,因此我们会在几秒钟后评估匹配情况,以允许翻转过渡。

4. 检查游戏完成情况

每次评估匹配时,我们都会检查是否已找到所有配对。如果已找到,我们会向玩家显示完成模式。

  const checkCompletion = () => {
    // We are storing clearedCards as an object since its more efficient 
    //to search in an object instead of an array
    if (Object.keys(clearedCards).length === uniqueCardsArray.length) {
      setShowModal(true);
    }
  };
Enter fullscreen mode Exit fullscreen mode

5. 最后,我们的重启功能

嗯,重新开始很简单,我们只需重置我们的状态并重新洗牌。

<Button onClick={handleRestart} color="primary" variant="contained">
    Restart
</Button>
Enter fullscreen mode Exit fullscreen mode
  const handleRestart = () => {
    setClearedCards({});
    setOpenCards([]);
    setShowModal(false);
    setMoves(0);
    // set a shuffled deck of cards
    setCards(shuffleCards(uniqueCardsArray.concat(uniqueCardsArray)));
  };

Enter fullscreen mode Exit fullscreen mode

好极了!我们的基本记忆卡游戏就完成了。

您可以在下面找到用于演示的 CodeSandbox Playground

结论

我很高兴我们能走到这一步。我们创建了一副洗好的牌组,将其渲染到棋盘上,添加了翻转功能,并评估了匹配对的判定。我们可以扩展这个示例,添加计时器、玩家最佳得分,并支持更高数量的卡牌。

您可以查看此 Github 存储库以获取完整代码。

如果您喜欢这篇文章,请与您的朋友分享,如果您有任何建议或反馈,请随时添加评论或在 Twitter 上给我发私信

感谢您的阅读

鏂囩珷鏉ユ簮锛�https://dev.to/shubhamreacts/build-a-card-memory-game-with-react-23dj
PREV
如何在不使用 JavaScript 的情况下构建交互式网页
NEXT
如何提高英语水平:开发者指南