30分鐘完成JavaScript中的記憶遊戲

經過在30分鐘內構建一個記憶遊戲來學習JS,CSS和HTML!
本教程介紹了一些基本的關於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-face(意爲背面)。vue

<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容器元素中。最終代碼結果是這樣的: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

咱們將使用一個簡單但很是有用的重置,適用於全部項目:編程

/* styles.css */

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
複製代碼

box-sizing: border-box屬性使元素充滿整個邊框,所以咱們能夠跳過數學計算。
經過設置display: flex到body和margin: auto到.memory-game容器,它將垂直地和水平地居中。 .memory-game也將是一個flex-container。默認狀況下,裏面的元素會縮小寬度來適應這個容器。經過將flex-wrap設置爲wrap,flex-items會根據彈性元素的大小進行自適應。bash

/* styles.css */

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

.memory-game {
  width: 640px;
  height: 640px;
  margin: auto;
  display: flex;
  flex-wrap: wrap;
}
複製代碼

每一個卡片的width(意爲寬度)和 height(意爲高度)都是用calc() CSS函數計算的。咱們將width設置爲25%,height設置爲33.333% ,並從margin(意爲邊距)中減去10px,來製做三行四張牌。
對於.memory-card子元素,咱們添加position: relative,這樣咱們就能夠相對它進行子元素的絕對定位。
把屬性front-face和back-face的屬性設置爲position: absolute,這樣就能夠從原始位置移除元素,並使它們堆疊在一塊兒。dom

/* styles.css */

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

.front-face,
.back-face {
  width: 100%;
  height: 100%;
  padding: 20px;
  position: absolute;
  border-radius: 5px;
  background: #1C7CCC;
}
複製代碼

這時頁面模版看上去應該是這樣:svg

讓咱們也添加一個點擊效果。:active僞類將在每次點擊元素時觸發。它引起一個 0.2秒的過渡:

.memory-card {
  width: calc(25% - 10px);
  height: calc(33.333% - 10px);
  margin: 5px;
  position: relative;
  transform-style: preserve-3d;
  box-shadow: 1px 1px 0 rgba(0, 0, 0, .3);
+ transform: scale(1);
}

+.memory-card:active {
+  transform: scale(0.97);
+  transition: transform .2s;
+}
複製代碼

翻轉卡片

要在單擊時翻轉卡片,咱們須要向元素添加類別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類把卡片旋轉了180度:

.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徽章。

爲了顯示它背面的圖像,讓咱們把backface-visibility: hidden應用到.front-face和.back-face。

.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方法來add(意爲添加):

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;
  });
}
view raw
複製代碼

爲了調用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));
複製代碼

看以後

點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注公衆號「新前端社區」,享受文章首發體驗!
每週重點攻克一個前端技術難點。

相關文章
相關標籤/搜索