过节很无聊?还是用 JavaScript 写一个脑力小游戏吧!

0次阅读

共计 12582 个字符,预计需要花费 32 分钟才能阅读完成。

作者:疯狂的技术宅原文:https://medium.freecodecamp.o…

本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章

本教程使用了 HTML5,CSS3 和 JavaScript 的基本的技术。我们将讨论数据属性、定位、透视、转换、flexbox、事件处理、超时和三元组。你不需要在编程方面有太多的知识和经验就能看懂,不过还是需要知道 HTML,CSS 和 JS 都是什么。
????Demo: Memory Game Project

项目结构
先在终端中创建项目文件:
???? mkdir memory-game
???? cd memory-game
???? touch index.html styles.css scripts.js
???? mkdir img
HTML
初始化页面模版并链接 css 文件 js 文件.
<!– index.html –>

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>

<title>Memory Game</title>

<link rel=”stylesheet” href=”./styles.css”>
</head>
<body>
<script src=”./scripts.js”></script>
</body>
</html>
这个游戏有 12 张卡片。每张卡片中都包含一个名为 .memory-card 的容器 div,它包含两个 img 元素。一个代表卡片的正面 front-face,另一个个代表背面 back-face。

<div class=”memory-card”>
<img class=”front-face” src=”img/react.svg” alt=”React”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>
您可以在这里下载本项目的资源文件:Memory Game Repo。
这组卡片将被包装在一个 section 容器元素中。最终代码如下:
<!– index.html –>

<section class=”memory-game”>
<div class=”memory-card”>
<img class=”front-face” src=”img/react.svg” alt=”React”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/react.svg” alt=”React”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/angular.svg” alt=”Angular”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/angular.svg” alt=”Angular”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/ember.svg” alt=”Ember”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/ember.svg” alt=”Ember”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/vue.svg” alt=”Vue”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/vue.svg” alt=”Vue”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/backbone.svg” alt=”Backbone”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/backbone.svg” alt=”Backbone”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/aurelia.svg” alt=”Aurelia”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

<div class=”memory-card”>
<img class=”front-face” src=”img/aurelia.svg” alt=”Aurelia”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>
</section>
CSS
我们将使用一个简单但非常有用的配置,把它应用于所有项目:
/* styles.css */

* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
box-sizing: border-box 属性能使元素充满整个边框,所以我们就可以不用做一些数学计算了。
把 display:flex 设置给 body,并且把 margin:auto 应用到到 .memory-game 容器,这样可以使它将垂直水平居中。
.memory-game 是一个弹性容器,在默认情况下,里面的元素会缩小宽度来适应这个容器。通过把 flex-wrap 的值设置为 wrap,会根据弹性元素的大小进行自适应。
/* styles.css */

body {
height: 100vh;
display: flex;
background: #060AB2;
}

.memory-game {
width: 640px;
height: 640px;
margin: auto;
display: flex;
flex-wrap: wrap;
}
每个卡片的 width 和 height 都是用 CSS 的 calc() 函数进行计算的。下面我们需要制作一个三行四列的界面,并且把 width 设置为 25%,height 设置为 33.333%,还要再减去 10px 留足边距.
为了定位 .memory-card 子元素,还要添加属性 position: relative,这样我们就可以相对它进行子元素的绝对定位。
把 front-face and back-face 的 position 属性都设置为 absolute,这样就可以从原始位置移除元素,并使它们堆叠在一起。
这时页面模版看上去应该是这样:

我们还需要添加一个点击效果。每次元素被点击时都会触发 :active 伪类,它引发一个 0.2 秒的过渡:

翻转卡片
要在单击时翻转卡片,需要把一个 flip 类添加到元素。为此,让我们用 document.querySelectorAll 选择所有 memory-card 元素,然后使用 forEach 遍历它们并附加一个事件监听器。每当卡片被点击时,都会触发 flipCard 函数,其中 this 代表被单击的卡片。该函数访问元素的 classList 并切换到 flip 类:
// scripts.js
const cards = document.querySelectorAll(‘.memory-card’);

function flipCard() {
this.classList.toggle(‘flip’);
}

cards.forEach(card => card.addEventListener(‘click’, flipCard));
CSS 中的 flip 类会把卡片旋转 180deg:
.memory-card.flip {
transform: rotateY(180deg);
}
为了产生 3D 翻转效果,还需要将 perspective 属性添加到 .memory-game。这个属性用来设置对象与用户在 z 轴上的距离。值越小,透视效果越强。为了能达得最佳的效果,把它设置为 1000px:
.memory-game {
width: 640px;
height: 640px;
margin: auto;
display: flex;
flex-wrap: wrap;
+ perspective: 1000px;
}
接下来对 .memory-card 元素添加 transform-style:preserve-3d 属性,这样就把卡片置于在父节点中创建的 3D 空间中,而不是将其平铺在 z = 0 的平面上(transform-style)。
.memory-card {
width: calc(25% – 10px);
height: calc(33.333% – 10px);
margin: 5px;
position: relative;
box-shadow: 1px 1px 1px rgba(0,0,0,.3);
transform: scale(1);
+ transform-style: preserve-3d;
}
再把 transition 属性的值设置为 transform 就可以生成动态效果了:
.memory-card {
width: calc(25% – 10px);
height: calc(33.333% – 10px);
margin: 5px;
position: relative;
box-shadow: 1px 1px 1px rgba(0,0,0,.3);
transform: scale(1);
transform-style: preserve-3d;
+ transition: transform .5s;
}
耶!现在我们得到了带有 3D 翻转效果的卡片,不过为什么卡片的另一面没有出现?由于绝对定位的原因,现在 .front-face 和 .back-face 都堆叠在了一起。每个元素的 back face 都是它 front face 的镜像。属性 backface-visibility 默认为 visible,因此当我们翻转卡片时,得到的是背面的 JS 徽章。

为了显示它背面的图像,让我们在 .front-face 和 .back-face 中添加 backface-visibility:hidden。
.front-face,
.back-face {
width: 100%;
height: 100%;
padding: 20px;
position: absolute;
border-radius: 5px;
background: #1C7CCC;
+ backface-visibility: hidden;
}
如果我们刷新页面并翻转一张卡片,它就消失了!

由于我们将两个图像都藏在了背面,所以另一面没有任何东西。所以接下来需要再把 .front-face 翻转 180 度:
.front-face {
transform: rotateY(180deg);
}
效果终于出来了!

匹配卡片
完成翻转卡片的功能之后,接下来处理匹配的逻辑。
当点击第一张卡片时,需要等待另一张被翻转。变量 hasFlippedCard 和 flippedCard 用来管理翻转状态。如果没有卡片翻转,hasFlippedCard 的值为 true,flippedCard 被设置为点击的卡片。让我们切换到 toggle 方法:
const cards = document.querySelectorAll(‘.memory-card’);

+ let hasFlippedCard = false;
+ let firstCard, secondCard;

function flipCard() {
– this.classList.toggle(‘flip’);
+ this.classList.add(‘flip’);

+ if (!hasFlippedCard) {
+ hasFlippedCard = true;
+ firstCard = this;
+ }
}

cards.forEach(card => card.addEventListener(‘click’, flipCard));
现在,当用户点击第二张牌时,代码会进入 else 块,我们将检查它们是否匹配。为了做到这一点,需要能够识别每一张卡片。
每当我们想要向 HTML 元素添加额外信息时,就可以使用数据属性。通过使用以下语法:data-*, 这里的 * 可以是任何单词,它将被插入到元素的 dataset 属性中。所以接下来为每张卡片添加一个 data-framework:
<section class=”memory-game”>
+ <div class=”memory-card” data-framework=”react”>
<img class=”front-face” src=”img/react.svg” alt=”React”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”react”>
<img class=”front-face” src=”img/react.svg” alt=”React”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”angular”>
<img class=”front-face” src=”img/angular.svg” alt=”Angular”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”angular”>
<img class=”front-face” src=”img/angular.svg” alt=”Angular”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”ember”>
<img class=”front-face” src=”img/ember.svg” alt=”Ember”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”ember”>
<img class=”front-face” src=”img/ember.svg” alt=”Ember”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”vue”>
<img class=”front-face” src=”img/vue.svg” alt=”Vue”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”vue”>
<img class=”front-face” src=”img/vue.svg” alt=”Vue”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”backbone”>
<img class=”front-face” src=”img/backbone.svg” alt=”Backbone”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”backbone”>
<img class=”front-face” src=”img/backbone.svg” alt=”Backbone”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”aurelia”>
<img class=”front-face” src=”img/aurelia.svg” alt=”Aurelia”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>

+ <div class=”memory-card” data-framework=”aurelia”>
<img class=”front-face” src=”img/aurelia.svg” alt=”Aurelia”>
<img class=”back-face” src=”img/js-badge.svg” alt=”Memory Card”>
</div>
</section>
这下就可以通过访问两个卡片的数据集来检查匹配了。下面将匹配逻辑提取到它自己的方法 checkForMatch(),并将 hasFlippedCard 设置为 false。如果匹配的话,则调用 disableCards() 并分离两个卡上的事件侦听器,以防止再次翻转。否则 unflipCards() 会将两张卡都恢复成超过 1500 毫秒的超时,从而删除 .flip 类:
把代码组合起来:
const cards = document.querySelectorAll(‘.memory-card’);

let hasFlippedCard = false;
let firstCard, secondCard;

function flipCard() {
this.classList.add(‘flip’);

if (!hasFlippedCard) {
hasFlippedCard = true;
firstCard = this;
+ return;
+ }
+
+ secondCard = this;
+ hasFlippedCard = false;
+
+ checkForMatch();
+ }
+
+ function checkForMatch() {
+ if (firstCard.dataset.framework === secondCard.dataset.framework) {
+ disableCards();
+ return;
+ }
+
+ unflipCards();
+ }
+
+ function disableCards() {
+ firstCard.removeEventListener(‘click’, flipCard);
+ secondCard.removeEventListener(‘click’, flipCard);
+ }
+
+ function unflipCards() {
+ setTimeout(() => {
+ firstCard.classList.remove(‘flip’);
+ secondCard.classList.remove(‘flip’);
+ }, 1500);
+ }

cards.forEach(card => card.addEventListener(‘click’, flipCard));
更优雅的进行条件匹配的方法是用三元运算符,它由三部分组成:第一部分是要判断的条件,如果条件符合就执行第二部分的代码,否则执行第三部分:
– if (firstCard.dataset.name === secondCard.dataset.name) {
– disableCards();
– return;
– }

– unflipCards();

+ let isMatch = firstCard.dataset.name === secondCard.dataset.name;
+ isMatch ? disableCards() : unflipCards();
锁定
现在已经完成了匹配逻辑,接着为了避免同时转动两组卡片,还需要锁定它们,否则翻转将会被失败。

先声明一个 lockBoard 变量。当玩家点击第二张牌时,lockBoard 将设置为 true,条件 if (lockBoard) return; 在卡被隐藏或匹配之前会阻止其他卡片翻转:
const cards = document.querySelectorAll(‘.memory-card’);

let hasFlippedCard = false;
+ let lockBoard = false;
let firstCard, secondCard;

function flipCard() {
+ if (lockBoard) return;
this.classList.add(‘flip’);

if (!hasFlippedCard) {
hasFlippedCard = true;
firstCard = this;
return;
}

secondCard = this;
hasFlippedCard = false;

checkForMatch();
}

function checkForMatch() {
let isMatch = firstCard.dataset.name === secondCard.dataset.name;
isMatch ? disableCards() : unflipCards();
}

function disableCards() {
firstCard.removeEventListener(‘click’, flipCard);
secondCard.removeEventListener(‘click’, flipCard);
}

function unflipCards() {
+ lockBoard = true;

setTimeout(() => {
firstCard.classList.remove(‘flip’);
secondCard.classList.remove(‘flip’);

+ lockBoard = false;
}, 1500);
}

cards.forEach(card => card.addEventListener(‘click’, flipCard));
点击同一个卡片
仍然是玩家可以在同一张卡上点击两次的情况。如果匹配条件判断为 true,从该卡上删除事件侦听器。

为了防止这种情况,需要检查当前点击的卡片是否等于 firstCard,如果是肯定的则返回。
if (this === firstCard) return;
变量 firstCard 和 secondCard 需要在每一轮之后被重置,所以让我们将它提取到一个新方法 resetBoard() 中,再其中写上 hasFlippedCard = false; 和 lockBoard = false。es6 的解构赋值功能 [var1, var2] = [‘value1’, ‘value2’] 允许我们把代码写得超短:
function resetBoard() {
[hasFlippedCard, lockBoard] = [false, false];
[firstCard, secondCard] = [null, null];
}
接着调用新方法 disableCards() 和 unflipCards():
const cards = document.querySelectorAll(‘.memory-card’);

let hasFlippedCard = false;
let lockBoard = false;
let firstCard, secondCard;

function flipCard() {
if (lockBoard) return;
+ if (this === firstCard) return;

this.classList.add(‘flip’);

if (!hasFlippedCard) {
hasFlippedCard = true;
firstCard = this;
return;
}

secondCard = this;
– hasFlippedCard = false;

checkForMatch();
}

function checkForMatch() {
let isMatch = firstCard.dataset.name === secondCard.dataset.name;
isMatch ? disableCards() : unflipCards();
}

function disableCards() {
firstCard.removeEventListener(‘click’, flipCard);
secondCard.removeEventListener(‘click’, flipCard);

+ resetBoard();
}

function unflipCards() {
lockBoard = true;

setTimeout(() => {
firstCard.classList.remove(‘flip’);
secondCard.classList.remove(‘flip’);

– lockBoard = false;
+ resetBoard();
}, 1500);
}

+ function resetBoard() {
+ [hasFlippedCard, lockBoard] = [false, false];
+ [firstCard, secondCard] = [null, null];
+ }

cards.forEach(card => card.addEventListener(‘click’, flipCard));
洗牌
我们的游戏看起来相当不错,但是如果不能洗牌就没有乐趣,所以现在处理这个功能。
当 display: flex 在容器上被声明时,flex-items 会按照组和源的顺序进行排序。每个组由 order 属性定义,该属性包含正整数或负整数。默认情况下,每个 flex-item 都将其 order 属性设置为 0,这意味着它们都属于同一个组,并将按源的顺序排列。如果有多个组,则首先按组升序顺序排列。
游戏中有 12 张牌,因此我们将迭代它们,生成 0 到 12 之间的随机数并将其分配给 flex-item order 属性:
function shuffle() {
cards.forEach(card => {
let ramdomPos = Math.floor(Math.random() * 12);
card.style.order = ramdomPos;
});
}
为了调用 shuffle 函数,让它成为一个立即调用函数表达式(IIFE),这意味着它将在声明后立即执行。脚本应如下所示:
const cards = document.querySelectorAll(‘.memory-card’);

let hasFlippedCard = false;
let lockBoard = false;
let firstCard, secondCard;

function flipCard() {
if (lockBoard) return;
if (this === firstCard) return;

this.classList.add(‘flip’);

if (!hasFlippedCard) {
hasFlippedCard = true;
firstCard = this;
return;
}

secondCard = this;
lockBoard = true;

checkForMatch();
}

function checkForMatch() {
let isMatch = firstCard.dataset.name === secondCard.dataset.name;
isMatch ? disableCards() : unflipCards();
}

function disableCards() {
firstCard.removeEventListener(‘click’, flipCard);
secondCard.removeEventListener(‘click’, flipCard);

resetBoard();
}

function unflipCards() {
setTimeout(() => {
firstCard.classList.remove(‘flip’);
secondCard.classList.remove(‘flip’);

resetBoard();
}, 1500);
}

function resetBoard() {
[hasFlippedCard, lockBoard] = [false, false];
[firstCard, secondCard] = [null, null];
}

+ (function shuffle() {
+ cards.forEach(card => {
+ let ramdomPos = Math.floor(Math.random() * 12);
+ card.style.order = ramdomPos;
+ });
+ })();

cards.forEach(card => card.addEventListener(‘click’, flipCard));
终于完成了!
您还可以在油管找到视频演示:???? Code Sketch Channel.

本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章

正文完
 0