JavaScript - 神経衰弱ゲームの作り方 Part 2 - カードをめくって揃える

JavaScript で作る神経衰弱ゲームのコードを完成させます。今回の Part 2 では、クリックでカードを2枚選ぶ方法と、数字が揃った場合と揃わなかった場合で処理を分ける方法を紹介します。

JavaScript - 神経衰弱ゲームの作り方 Part 2 - カードをめくって揃える
Photo by Kristyna Squared.one / Unsplash

カードを2枚めくって数字を揃えよう

Part 1 に引き続き、JavaScript で作る神経衰弱ゲームのコードを解説します。

  • Part 1 では、JavaScript で生成したカードに数字を割り振り、ゲームに使うカードを用意しました。
  • Part 2(この記事)では、カードをクリックしてめくれるようにし、数字が揃ったかどうかをチェックします。全部を揃え終わったあとも繰り返し遊べるようにして、ゲームを完成させます。
JavaScript - 神経衰弱ゲームの作り方 Part 1 - カードをシャッフルする
カードをめくって数字を揃える神経衰弱ゲームを作ります。今回の Part 1 では、カードを JavaScript で生成する方法と、配列で用意した数字をシャッフルする方法を紹介します。

この記事を読むと分かること

  • forEach()
  • addEventListener()
  • this キーワード
  • if…else 条件分岐
  • classList.contains()
  • classList.add()
  • classList.remove()
  • setTimeout()

サンプルプロジェクト

改めて、神経衰弱ゲームの動きを確認しておきましょう。

  • クリックでカードをめくる
  • 一度にめくれるのは2枚まで
  • 数字が揃ったら、そのカードはクリックに反応しなくなる
  • 数字が揃わなかったら、カードは伏せられる
メモリーゲームのサンプル
メモリーゲームのサンプル

Part 1 で解説したコード

以下は、神経衰弱ゲームの作り方 Part 1 で解説したコードです。今回は、ここに追加していくコードについて説明していきますよ。

// カードをシャッフルするコード

const numbers = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6];
const cardNumbers = numbers.length;
let cards = null;

// カードを生成する
function createCards() {
  const gameContainer = document.querySelector('.game-container');
  for (let i = 0; i < cardNumbers; i++) {
    const card = document.createElement('div');
    card.classList.add('game-card');
    gameContainer.appendChild(card);
  }
  cards = document.querySelectorAll('.game-card');
}

// 配列の数字をシャッフルする
function shuffleNumbers() {
  for (let i = cardNumbers - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [numbers[i], numbers[j]] = [numbers[j], numbers[i]];
  }
  // すべてのカードに数字を割り振る
  cards.forEach((card, i) => {
    card.textContent = numbers[i]
  });
}

createCards();
shuffleNumbers();

Part 1 でカードの準備ができたので、Part 2 ではカードをクリックしてめくれるようにしていきます。JavaScript で神経衰弱ゲームを作るときは、条件分岐の if 文を複数回使います。if 文の基本は以下の記事で解説しているので、あわせてご覧ください。

JavaScript - 条件分岐の基本 - if 文の書き方と true, false の意味
if 文は、「もし〇〇ならAをする、そうじゃなかったらBをする」のように、条件によって処理を分岐させるための文です。if 文を理解する際に必要になる、真理値(真偽値)の true と false についても、一緒に学んでいきましょう。

変数

最初に、変数を追加しましょう。

  • play:カードをめくれるかどうかを表す変数です。初めは true で、カードをめくれることを示します。(2枚目を選んだ後は、それ以上カードをめくれないようにするために false にします)
  • firstCardsecondCard:クリックしてめくった2枚のカードを格納する変数です。それぞれを null で初期化し、空っぽであることを示します。
  • matchedCount:揃った数を数える変数です。初めは 0 です。
const numbers = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6];
const cardNumbers = numbers.length;
let cards = null;

// カードを2枚までめくれるかどうか(trueまたはfalse)
let play = true;
// カード1枚目
let firstCard = null;
// カード2枚目
let secondCard = null;
// 揃った数
let matchedCount = 0;

クリックしてカードを2枚めくる - flipCard

変数を用意できたら、カードをクリックして2枚までめくれるようにする関数 flipCard を定義していきます。

// クリックしてカードを2枚めくる
function flipCard() {

  // 1. すべてのカードにクリックイベントを追加する
  // 2. カードを2枚までめくれるかどうかを確認する
  // 3. カードがめくられていないことを確認してめくる
  // 4. めくったカードが1枚目か2枚目かで処理を分ける
  // 5. カードをクリックできないようにする

}

1. すべてのカードにクリックイベントを追加する

ユーザーのクリックにカードが反応するようにしましょう。すべてのカードにクリックイベントを追加にするために、forEach() メソッドを使用します。

  • cards:生成したすべてのカードを参照します。
  • forEach():すべてのカードに対して処理を行います。
  • card:処理の対象となっているカードを表す変数です。
function flipCard() {

  // 1. すべてのカードにクリックイベントを追加する
  cards.forEach(card => {

    // 2. カードを2枚までめくれるかどうかを確認する
    // 3. カードがめくられていないことを確認してめくる
    // 4. めくったカードが1枚目か2枚目かで処理を分ける
    // 5. カードをクリックできないようにする

  });
}
  • addEventListener() メソッドを使って、ユーザーがカードをクリックしときに関数が実行されるようにします。
    (関数は、アロー関数ではなく function キーワードを使用して書いていきます)
function flipCard() {

  // 1. すべてのカードにクリックイベントを追加する
  cards.forEach(card => {

    // クリックイベントを追加
    card.addEventListener('click', function() {

      // 2. カードを2枚までめくれるかどうかを確認する
      // 3. カードがめくられていないことを確認してめくる
      // 4. めくったカードが1枚目か2枚目かで処理を分ける
      // 5. カードをクリックできないようにする

    });
  });
}

続いて、クリックしたときに実行する処理です。3つの if 文でカードの状態をチェックしながら、1枚目、2枚目とカードをめくれるようにしていきますよ。

2. カードを2枚までめくれるかどうかを確認する

神経衰弱ゲームでは、2枚ずつカードをめくって数字を揃えていきます。クリックにすべてのカードが反応して次々にめくることができてしまうと、ゲームになりませんね。これを防ぐために、まずは、カードを2枚までめくれるかどうかの確認をしましょう。そのために使うのが、変数 play です。

  • 「変数 playtrue であれば」という条件を if 文に指定して、一度に2枚までめくれるようにします。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {

      // 2. カードを2枚までめくれるかどうかを確認する
      if(play) {

        // 3. カードがめくられていないことを確認してめくる
        // 4. めくったカードが1枚目か2枚目かで処理を分ける
        // 5. カードをクリックできないようにする

      }

    });
  });
}

3. カードがめくられていないことを確認してめくる

次に、めくれるのは伏せてあるカードだけで、既にめくってあるカードはクリックに反応しないようにします。伏せてあるかどうかは、そのカードに show クラスがついているかいないかで確認しますよ。使用するのは、classList.contains() メソッドです。

  • 「クリックしたカードに show クラスが含まれていないなら」という条件を if 文に指定して、まだめくっていないカードだけをめくれるようにします。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      if(play) {

        // 3. カードがめくられていないことを確認してめくる
        if(!this.classList.contains('show')) { 

          // 4. めくったカードが1枚目か2枚目かで処理を分ける
          // 5. カードをクリックできないようにする

        }

      }
    });
  });
}

イベントハンドラー内で使われている this は、イベントが発生して関数を実行している要素(クリックしたカード)を参照します。クリックしたときに実行する関数を function キーワードを使って書いているのは、この this を有効にするためです。(アロー関数では、この this は正しく動きません)

さて、if 文の条件を満たす場合は、クリックしたカードが伏せてあるということですね。classList.add() メソッドで、そのカードをめくった見た目に変更しましょう。

  • クリックしたカードに show クラスを追加します。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      if(play) {

        // 3. カードがめくられていないことを確認してめくる
        if(!this.classList.contains('show')) {
 
          // カードをめくる
          this.classList.add('show');

          // 4. めくったカードが1枚目か2枚目かで処理を分ける
          // 5. カードをクリックできないようにする

        }

      }
    });
  });
}

4. めくったカードが1枚目か2枚目かで処理を分ける

続いて、クリックしてめくったカードを変数に保存します。後ほど、2枚のカードの数字が揃ったかどうかを、この変数の値でチェックできるようにするためです。めくったカードが1枚目か2枚目かによって if…else 文で処理を分け、それぞれ変数 firstCardsecondCard に保存しましょう。

はじめに、めくったカードが1枚目の場合です。1枚目を表す変数 firstCardnull で初期化したので、この段階では変数の中身は空っぽ(false)です。

  • 変数 firstCardtrue ではないなら、
  • 変数 firstCard に、クリックしたカードを代入します。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      if(play) {
        if(!this.classList.contains('show')) {
          this.classList.add('show');

          // 4. めくったカードが1枚目か2枚目かで処理を分ける
          // 1枚目
          if(!firstCard) {
            firstCard = this;

          } else {
            // …

            // 5. カードをクリックできないようにする

          }
        }
      }
    });
  });
}

次は else の部分です。変数 firstCard に値が代入された後は if(!firstCard) の条件が満たされなくなるので、else 内の処理を実行します。

  • 変数 secondCard に、クリックしたカードを代入します。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      if(play) {
        if(!this.classList.contains('show')) {
          this.classList.add('show');

          // 4. めくったカードが1枚目か2枚目かで処理を分ける
          if(!firstCard) {
            firstCard = this;

          // 2枚目
          } else {
            secondCard = this;

            // 5. カードをクリックできないようにする

          }
        }
      }
    });
  });
}

5. カードをクリックできないようにする

カードを2枚めくり終わったら、それ以上カードをめくれないようにして次の段階の処理へ移ります。続けて、else の部分にコードを追加してください。

  • 変数 playfalse にします。
  • 数字が揃ったかどうかをチェックする関数 matchCards に移動します。
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      if(play) {
        if(!this.classList.contains('show')) {
          this.classList.add('show');
          if(!firstCard) {
            firstCard = this;
          } else {
            secondCard = this;

            // 5. カードをめくれないようにする
            play = false;
            // 数字が揃ったかどうかをチェックする
            matchCards();
          }
        }
      }
    });
  });
}

以上で、カードをクリックして2枚までめくれるようにする関数 flipCard を定義できました。

Part 1 で解説したコードと合わせて関数 flipCard を呼び出すと、カードを2枚めくることができますよ。(ただし、数字が揃ったかどうかをチェックする関数 matchCards をまだ定義していないので、現状ではエラー「ReferenceError: Can't find variable: matchCards」が発生します)

カードを2枚めくることができる(数字が揃ったかどうかの判定はまだできない)
カードを2枚めくることができる(数字が揃ったかどうかの判定はまだできない)

数字が揃ったかどうかをチェックする - matchCards

ここからは、2枚目のカードを選んだ後に実行する関数 matchCards を定義していきます。関数 matchCards では、カードの数字が揃ったかどうか、カードをすべて揃え終わったかどうかによって処理を分けます。

// 数字が揃ったかどうかをチェックする
function matchCards() {

  // 1. 数字が揃った場合
   // (1)すべてのカードを揃え終えた場合
   // (2)まだ揃っていないカードが残っている場合
  // 2. 数字が揃わなかった場合

}

1. 数字が揃った場合

めくった2枚のカード firstCardsecondCard の数字が「揃った場合」と「揃わなかった場合」を、if…else 文で条件分岐しましょう。

まずは、数字が揃った場合の処理を書いていきますよ。2枚のカードの数字を textContent プロパティで取得して比較し、数字が等しいことを if 文の条件とします。

  • 1枚目 firstCard と2枚目 secondCard の数字が等しいなら、
  • 揃った数を数える変数 matchedCount の値を1増やします。
function matchCards() {

  // 1. 数字が揃った場合
  if(firstCard.textContent === secondCard.textContent) {
    // 揃った数を1増やす
    matchedCount += 1;
    
    // (1)すべてのカードを揃え終えた場合
    // (2)まだ揃っていないカードが残っている場合
  
  // 2.数字が揃わなかった場合
  } else {
    // …
  }

}

数字が揃った場合は、さらに if…else 文で処理を分けます。「すべてのカードを揃え終えた場合」と「まだ揃っていないカードが残っている場合」です。

(1) すべてのカードを揃え終えた場合

すべてのカードを揃え終えたら、再びゲームを開始できるようにしましょう。

  • 揃え終わったかどうかは、変数 matchedCount の値で確認します。配列内の要素数 cardNumbers の半分(今回では6ペア)で揃え終わったということになります。
  • 揃え終わったあとしばらく待ってからゲームを再開できるようにするために、setTimeout() メソッドで3秒待ちます。
function matchCards() {
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;

    // (1)すべてのカードを揃え終えた場合
    // すべて揃ったことを確認する
    if(matchedCount === cardNumbers / 2) {
      // 3秒待つ
      setTimeout(() => {
        // …
      }, 3000);

    // (2)まだ揃っていないカードが残っている場合
    } else {
      // …
    }

  // 2. 数字が揃わなかった場合
  } else {
    // …
  }
}
  • カードを揃え終わったとき、すべてのカードはめくられている状態です。forEach() メソッドですべてのカードから show クラスを削除し、カードを伏せた見た目にします。クラスを削除するために使うのは、classList.remove() メソッドです。
  • 関数 shuffleNumbers を呼び出し、数字をシャッフルして割り振りし直します。
function matchCards() {
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;

    // (1)すべてのカードを揃え終えた場合
    if(matchedCount === cardNumbers / 2) {
      setTimeout(() => {

        // すべてのカードを伏せる
        cards.forEach(card => {
          card.classList.remove('show');
        });
        // 数字をシャッフルしてすべてのカードに割り振る
        shuffleNumbers();

      }, 3000);

    // (2)まだ揃っていないカードが残っている場合
    } else {
      // …
    }

  // 2. 数字が揃わなかった場合
  } else {
    // …
  }
}
  • 変数 matchedCount0 にして、揃った数をリセットします。
  • 関数 readyToFlip を呼び出します。これは、カードをめくれるよう準備する関数で、このあと定義します。
function matchCards() {
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;

    // (1)すべてのカードを揃え終えた場合
    if(matchedCount === cardNumbers / 2) {
      setTimeout(() => {
        cards.forEach(card => {
          card.classList.remove('show');
        });
        shuffleNumbers();

        // 揃った数をリセットする
        matchedCount = 0;
        // カードをめくれるよう準備する
        readyToFlip();

      }, 3000);

    // (2)まだ揃っていないカードが残っている場合
    } else {
      // …
    }

  // 2. 数字が揃わなかった場合
  } else {
    // …
  }
}

以上で、すべてのカードを揃え終えたあとゲームを再開できるようにするコードができました。

(2)まだ揃っていないカードが残っている場合

めくったカードの数字が揃ったとしても、まだ揃っていないカードが残っている場合はゲームを続行します。

  • カードをめくれるよう準備する関数 readyToFlip を呼び出します。
function matchCards() {
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;
    if(matchedCount === cardNumbers / 2) {
      setTimeout(() => {
        cards.forEach(card => {
          card.classList.remove('show');
        });
        shuffleNumbers();
        matchedCount = 0;
        readyToFlip();
      }, 3000);

    // (2)まだ揃っていないカードが残っている場合
    } else {
      //カードをめくれるよう準備する
      readyToFlip();
    }

  // 2. 数字が揃わなかった場合
  } else {
    // …
  }
}

以上で、数字が揃った場合のコードができました。

2. 数字が揃わなかった場合

今度は、数字が揃わなかった場合です。めくったカードを裏返してゲームを続行できるようにしましょう。

  • しばらく待ってから次のカードをめくれるようにするために、setTimeout() メソッドで 1.5 秒待ちます。
  • めくった2枚のカードから show クラスを削除して、カードを伏せた見た目にします。
  • カードをめくれるよう準備する関数 readyToFlip を呼び出します。
function matchCards() {
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;
    if(matchedCount === cardNumbers / 2) {
      setTimeout(() => {
        cards.forEach(card => {
          card.classList.remove('show');
        });
        shuffleNumbers();
        matchedCount = 0;
        readyToFlip();
      }, 3000);
    } else {
      readyToFlip();
    }

  // 2. 数字が揃わなかった場合
  } else {
    // 1.5秒待つ
    setTimeout(() => {
      
      // めくったカードを伏せる
      firstCard.classList.remove('show');
      secondCard.classList.remove('show');
      
      // カードをめくれるよう準備する
      readyToFlip();
      
    }, 1500); 
  }
}

以上で、数字が揃わなかった場合のコードができました。関数 matchCards の定義はここまでです。

カードをめくれるよう準備する - readyToFlip

さて、ここからは、上で説明した関数 matchCards の中にも出てきた readyToFlip を定義していきますよ。readyToFlip は、カードをめくれるよう準備する関数です。

  • 変数 firstCardsecondCardnull にし、カードが1枚も選ばれていない状態にします。
  • カードをめくれるかどうかを示す変数 playtrue にして、カードを2枚までめくれるようにします。
// カードをめくれるよう準備する
function readyToFlip() {
  firstCard = null;
  secondCard = null;
  play = true;
}

完成コード

Part 1 で解説したコードと合わせて、神経衰弱ゲームのコードが完成です。

//************************
// 神経衰弱ゲームのコード
//************************

// 使用する数字
const numbers = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6];
// 数字の数
const cardNumbers = numbers.length;
// 初めはカードは無し
let cards = null;
// 2枚までめくれるかどうか(trueまたはfalse)
let play = true;
// カード1枚目、カード2枚目
let firstCard = null;
let secondCard = null;
// 揃った数
let matchedCount = 0;

// カードを生成する
function createCards() {
  const gameContainer = document.querySelector('.game-container');
  //----- 配列の要素と同じ数だけ生成 -----
  for (let i = 0; i < cardNumbers; i++) {
    const card = document.createElement('div');
    card.classList.add('game-card');
    gameContainer.appendChild(card);
  }
  //----- 生成したすべてのカードを変数に格納 -----
  cards = document.querySelectorAll('.game-card');
}

// 配列の数字をシャッフルする
function shuffleNumbers() {
  //----- Fisher-Yates アルゴリズム ----------
  for (let i = cardNumbers - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [numbers[i], numbers[j]] = [numbers[j], numbers[i]];
  }
  //----- すべてのカードに数字を割り振る ----------
  cards.forEach((card, i) => {
    card.textContent = numbers[i]
  });
}

// クリックしてカードを2枚めくる
function flipCard() {
  cards.forEach(card => {
    card.addEventListener('click', function() {
      //----- カードを2枚までめくれるなら ----------
      if(play) {
        //----- カードが伏せてあるなら ----------
        if(!this.classList.contains('show')) {
          this.classList.add('show');
          //----- 1枚目または2枚目 ----------
          if(!firstCard) {
            firstCard = this;
          } else {
            secondCard = this;
            play = false;
            //----- 数字が揃ったかどうかをチェックする -----
            matchCards();
          }
        }
      }
    });
  });
}

// 数字が揃ったかどうかをチェックする
function matchCards() {
  //----- 数字が揃った場合 ----------
  if(firstCard.textContent === secondCard.textContent) {
    matchedCount += 1;
    //----- すべてのカードを揃え終えた場合 ----------
    if(matchedCount === cardNumbers / 2) {
      setTimeout(() => {
        cards.forEach(card => {
          card.classList.remove('show');
        });
        shuffleNumbers();
        matchedCount = 0;
        readyToFlip();
      }, 3000);
    //----- まだ揃っていないカードが残っている場合 ----------
    } else {
      readyToFlip();
    }
  //----- 数字が揃わなかった場合 ----------
  } else {
    setTimeout(() => {
      firstCard.classList.remove('show');
      secondCard.classList.remove('show');
      readyToFlip();
    }, 1500); 
  }
}

// カードをめくれるよう準備する
function readyToFlip() {
  firstCard = null;
  secondCard = null;
  play = true;
}

createCards();
shuffleNumbers();
flipCard();

See the Pen JavaScript - Memory Game by Pyxofy (@pyxofy) on CodePen.


まとめ

今回は、JavaScript で神経衰弱ゲームを作る方法の Part 2 として、カードをクリックして2枚ずつめくり、数字が揃ったかどうかをチェックするコードを紹介しました。

カードをめくったり伏せたりするために、classList.add()classList.remove() でクラスの付け外しをし、カードの見た目を切り替えました。また、一度にめくれるのは2枚までとしたり、数字が揃った場合と揃わなかった場合の処理を分けるために、if 文で条件分岐しました。配列に保存する数字の数を増やせばカードの数も増えるので、お好きなカードの枚数で神経衰弱ゲームを作ってみてくださいね。

最後まで読んでいただき、ありがとうございます。この記事をシェアしてくれると嬉しいです!

SNSで Pyxofy とつながりましょう! LinkedInThreadsBlueskyMastodon X (Twitter) @pyxofyFacebook

関連記事

JavaScript - Pyxofy
プログラミング言語のJavaScriptについて、初心者向けに解説しています。
CSS Art
Articles for creating CSS Art.
CSS Animation
Articles for creating CSS Animation.
Scratch - Pyxofy
Scratch 3.0の使い方を、プログラミング初心者や子どもにも分かりやすく紹介しています。