用 JavaScript 寫一個卡片小遊戲

小遊戲使用了HTML5,CSS3和JavaScript的基本的技術。

將討論數據屬性、定位、透視、轉換、flexbox、事件處理、超時和三元組。

你不需要在編程方面有太多的知識和經驗就能看懂,不過還是需要知道HTML,CSS和JS都是什麼。

用 JavaScript 寫一個卡片小遊戲

項目結構
先在終端中創建項目文件:

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

用 JavaScript 寫一個卡片小遊戲

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

這組卡片將被包裝在一個 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 ,這樣就可以從原始位置移除元素,並使它們堆疊在一起。

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

用 JavaScript 寫一個卡片小遊戲

我們還需要添加一個點擊效果。 每次元素被點擊時都會觸發 :active 僞類,它引發一個 0.2秒的過渡:

用 JavaScript 寫一個卡片小遊戲

翻轉卡片

要在單擊時翻轉卡片,需要把一個 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 徽章。

![(http://upload-images.jianshu.io/upload_images/13133049-4521ac8b957bb1be.gif?imageMogr2/auto-orient/strip)

爲了顯示它背面的圖像,讓我們在 .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;
}

如果我們刷新頁面並翻轉一張卡片,它就消失了!

用 JavaScript 寫一個卡片小遊戲

由於我們將兩個圖像都藏在了背面,所以另一面沒有任何東西。 所以接下來需要再把 .front-face 翻轉180度:

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

效果出來了!

用 JavaScript 寫一個卡片小遊戲

匹配卡片

完成翻轉卡片的功能之後,接下來處理匹配的邏輯。

當點擊第一張卡片時,需要等待另一張被翻轉。 變量 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();

鎖定

現在已經完成了匹配邏輯,接着爲了避免同時轉動兩組卡片,還需要鎖定它們,否則翻轉將會被失敗。

用 JavaScript 寫一個卡片小遊戲

先聲明一個 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,從該卡上刪除事件偵聽器。

用 JavaScript 寫一個卡片小遊戲

爲了防止這種情況,需要檢查當前點擊的卡片是否等於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));

點擊同一個卡片

仍然是玩家可以在同一張卡上點擊兩次的情況。 如果匹配條件判斷爲 true,從該卡上刪除事件偵聽器。

用 JavaScript 寫一個卡片小遊戲

爲了防止這種情況,需要檢查當前點擊的卡片是否等於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));

完成了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章