美文网首页前端先锋前端先锋
用 JavaScript 写一个脑力小游戏

用 JavaScript 写一个脑力小游戏

作者: 前端先锋 | 来源:发表于2019-02-26 11:44 被阅读6次

    翻译:疯狂的技术宅
    原文:https://medium.freecodecamp.org/vanilla-javascript-tutorial-build-a-memory-game-in-30-minutes-e542c4447eae

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

    项目结构

    先在终端中创建项目文件:

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

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

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

    front-face and back-faceposition属性都设置为 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);
    }
    

    效果终于出来了!

    匹配卡片

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

    当点击第一张卡片时,需要等待另一张被翻转。 变量 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();
    

    锁定

    现在已经完成了匹配逻辑,接着为了避免同时转动两组卡片,还需要锁定它们,否则翻转将会被失败。

    先声明一个 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;
    

    变量 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
    关注微信公众号,每天推送最新前端趋势和技术文章

    微信扫一扫关注公众号

    相关文章

      网友评论

        本文标题:用 JavaScript 写一个脑力小游戏

        本文链接:https://www.haomeiwen.com/subject/hkqayqtx.html