使用 React 构建纸牌记忆游戏
我们经常会遇到一些小游戏,然后琢磨它到底有多复杂?我们能做出来吗?通常情况下,我们不会突破这个界限。然而,在这篇文章中,我们将构建一个简单的记忆游戏,它既易于上手,也易于开发。
纸牌记忆游戏是一款测试玩家记忆力的简单游戏。玩家需要在一副成对的纸牌中,连续轮次选择一对匹配的纸牌。当所有匹配的纸牌都选中时,玩家获胜。
它的简单 UI 可能看起来像这样:
让我们定义游戏规则
除非我们了解规则,否则我们无法制作游戏。因此,我们先来解释一下规则:
-
我们需要一副洗好的牌。牌堆里每张牌都必须有一对。
-
游戏必须翻转玩家点击的牌。每次最多显示两张牌。
-
游戏会处理匹配和不匹配的卡牌。不匹配的卡牌会在短暂时间后翻回。匹配的卡牌会从牌堆中移除。
-
每当玩家选择一对时,游戏就会增加当前的移动次数
-
一旦找到所有配对,玩家就会看到带有分数的确认对话框。
-
游戏提供了重启功能。
那么我们还在等什么...让我们开始吧。
我们首先定义卡片结构。对于卡片,我们创建一个具有 type 属性和图像源的对象。
{
type: 'Pickachu',
image: require('../images/Pickachu.png')
}
下一步是洗牌。啊,是的,这是最重要的一步。如果我们不洗牌,那就不是真正的记忆游戏了。
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;
}
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>
)
}
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
justify-items: center;
align-items: stretch;
gap: 1rem;
}
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>
);
}
我们一次最多只能在openCards状态中保留两张卡片。由于我们使用的是静态数组,并且实际上并没有从原始 cards 数组中删除任何内容,因此我们只需将已打开卡片的索引存储在openCards状态中即可。根据openCards 和 clearedCards状态,我们分别将isFlipped或isInactive属性传递给 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);
}
};
5. 最后,我们的重启功能
嗯,重新开始很简单,我们只需重置我们的状态并重新洗牌。
<Button onClick={handleRestart} color="primary" variant="contained">
Restart
</Button>
const handleRestart = () => {
setClearedCards({});
setOpenCards([]);
setShowModal(false);
setMoves(0);
// set a shuffled deck of cards
setCards(shuffleCards(uniqueCardsArray.concat(uniqueCardsArray)));
};
好极了!我们的基本记忆卡游戏就完成了。
您可以在下面找到用于演示的 CodeSandbox Playground
结论
我很高兴我们能走到这一步。我们创建了一副洗好的牌组,将其渲染到棋盘上,添加了翻转功能,并评估了匹配对的判定。我们可以扩展这个示例,添加计时器、玩家最佳得分,并支持更高数量的卡牌。
您可以查看此 Github 存储库以获取完整代码。
如果您喜欢这篇文章,请与您的朋友分享,如果您有任何建议或反馈,请随时添加评论或在 Twitter 上给我发私信。
感谢您的阅读
鏂囩珷鏉ユ簮锛�https://dev.to/shubhamreacts/build-a-card-memory-game-with-react-23dj