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

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

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


clipboard.png

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

项目结构

先在终端中建立项目文件:html

🌹 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-facevue

clipboard.png

<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 Repojava

这组卡片将被包装在一个 section 容器元素中。 最终代码以下:react

<!-- 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

咱们将使用一个简单但很是有用的配置,把它应用于全部项目:git

/* styles.css */

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

box-sizing: border-box 属性能使元素充满整个边框,因此咱们就能够不用作一些数学计算了。es6

display:flex 设置给 body ,而且把 margin:auto应用到到 .memory-game 容器,这样可使它将垂直水平居中。github

.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;
}

每一个卡片的 widthheight 都是用 CSS 的 calc()函数进行计算的。 下面咱们须要制做一个三行四列的界面,而且把 width 设置为 25%height 设置为 33.333% ,还要再减去 10px 留足边距.

为了定位 .memory-card 子元素,还要添加属性 position: relative ,这样咱们就能够相对它进行子元素的绝对定位。

front-face and back-faceposition属性都设置为 absolute ,这样就能够从原始位置移除元素,并使它们堆叠在一块儿。

这时页面模版看上去应该是这样:

clipboard.png

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

clipboard.png

翻转卡片

要在单击时翻转卡片,须要把一个 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 徽章。

clipboard.png

为了显示它背面的图像,让咱们在 .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;
}

若是咱们刷新页面并翻转一张卡片,它就消失了!

clipboard.png

因为咱们将两个图像都藏在了背面,因此另外一面没有任何东西。 因此接下来须要再把 .front-face 翻转180度:

.front-face {
  transform: rotateY(180deg);
}

效果终于出来了!

clipboard.png

匹配卡片

完成翻转卡片的功能以后,接下来处理匹配的逻辑。

当点击第一张卡片时,须要等待另外一张被翻转。 变量 hasFlippedCardflippedCard 用来管理翻转状态。 若是没有卡片翻转,hasFlippedCard 的值为 trueflippedCard 被设置为点击的卡片。 让咱们切换到 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();

锁定

如今已经完成了匹配逻辑,接着为了不同时转动两组卡片,还须要锁定它们,不然翻转将会被失败。

clipboard.png

先声明一个 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,从该卡上删除事件侦听器。

clipboard.png

为了防止这种状况,须要检查当前点击的卡片是否等于firstCard,若是是确定的则返回。

if (this === firstCard) return;

变量 firstCardsecondCard 须要在每一轮以后被重置,因此让咱们将它提取到一个新方法 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欢迎关注,天天都给你推送新鲜的前端技术文章