時々唸るファンの音

色々と勉強してみたいと思ってる人の備忘録。

2019-12-27:神経衰弱を作ってみる その3

こんにちは~~!

どんな記事にしようと悩んでたら、一日が終わってしまいました!
(あとデバック作業が思いのほか大変だった)

 

まずは…完成した神経衰弱ゲームを見てもらおうと思います!
fan-s-sinkeisuijaku.netlify.com
こんな感じのゲームになりました!

 
ソースコードとかの記事を書くのはじめてだから、

上手に書けないかもしれないのですが…温かい目で見てくれると嬉しいです!

全部のソースコードは解説できないとは思うのですが、頑張って書きます!!!

※かんたんモードを例に書いていこうと思います!
※できるだけ見やすくなるようにしてるので、実際のソースコードと抜粋してたり、順番が異なります。

解説できそうな部分のソースコードを解説してみる。

ルール説明画面→難易度選択

 const okBtn = document.getElementById('okBtn');
const rule = document.getElementById('rule');
const levelChoice = document.getElementById('levelChoice');

okBtn.addEventListener('click', () => {
    rule.style.display = 'none';
    levelChoice.style.display = 'block';
  });

OKボタン押したら、ルール画面を消して、難易度選択画面を出す作業をしてます。まずはここから書き始めました。

難易度選択画面→ゲーム画面

  const easyBtn = document.getElementById('easyBtn');
  const normalBtn = document.getElementById('normalBtn');
  const hardBtn = document.getElementById('hardBtn');
  const startBtn = document.getElementById('startBtn');

  let level = 1;

  easyBtn.addEventListener('click', () => {
    easyBtn.style.background = '#F4B2BA';
    easyBtn.style.border = '2px solid black';
    startBtn.style.pointerEvents = 'auto';
    errorMsg.style.display = 'none';
    level = 1;
    if (normalBtn.style.background = '#F4B2BA') {
      normalBtn.style.background = '#BDBDBD';
      normalBtn.style.border = '1px solid black';

    }
    if (hardBtn.style.background = '#F4B2BA') {
      hardBtn.style.background = '#BDBDBD';
      hardBtn.style.border = '1px solid black';
    }
  });

EASYボタンを選択したら、
EASYの色がピンクに、枠線を太くしています。
STARTボタンが選択できるよ!とわかりやすくなるように、pointer-events = "auto";にして、選択できるようになってることをわかりやすくしています。
そして「選んでね!」の文言を消してます。
level = 1 ;
つまり、簡単モードです。ってことを変数に入れてます。

以下、if文は、NORMAL,HARDを選択されたときは、
デフォルトの状態に戻すようにしています。

STARTボタンが押されたとき

  const startBtn = document.getElementById('startBtn');
  const backBtn = document.getElementById('backBtn');
  const mainGameArea = document.getElementById('mainGameArea');
  const levelChoice = document.getElementById('levelChoice');
  const cardsArea = document.getElementById('cardsArea');
  const mainGameArea = document.getElementById('mainGameArea');



  startBtn.addEventListener('click', () => {
    //全般設定:難易度設定ボタン出現させます。
    backBtn.style.display = 'block';

    // 簡単モードの場合
    if (level === 1) {
      // カードランダム生成をしている部分
      cardShuffleEasy();
      // カードの数字をdivに入れながら生成。
      for (let i = 0; i < 4; i++) {
        createDiv(cardsNumEasy[i]);
      }
      // メインゲームエリアを表示、レベル選択画面を消す。
      mainGameArea.style.display = 'block';
      levelChoice.style.display = 'none';
      // カード選択エリアの白背景のheight,margin-topを設定。
      cardsArea.style.height = '150px';
      cardsArea.style.marginTop = '50px';
      // ピンク背景のアレを調整
      mainGameArea.style.height = '300px';
      mainGameArea.style.marginTop = '-150px';
      // 難易度設定に戻るボタンの調整
      backBtn.style.marginTop = '170px';


      // ゲーム部分
      gameStart();
  }

//中略

}

startボタンが押されたときに走る部分を書いています。
まず、難易度選択画面に戻るボタンを出現させてます。
(抜粋してるので簡単モードの場合だけ書いてるのですが、
cardShuffleEasy(); カードをシャッフルするための関数を走らせて、

for (let i = 0; i < 4; i++) {
        createDiv(cardsNumEasy[i]);
      }

簡単モードなので4枚、シャッフルしたカードのdivを生成してます。
その下の部分は、ゲーム画面の調整をしています。

そして
gameStart(); gameがスタートしますよ!という関数を走らせてます。

カードをシャッフルして、カードを並べる部分

cardShuffleEasy();
  const cardsNumEasy = [1, 1, 2, 2]

  function cardShuffleEasy() {

    for (let j = cardsNumEasy.length - 1; j >= 0; j--) {
      let rand = Math.floor(Math.random() * (j + 1));
      [cardsNumEasy[j], cardsNumEasy[rand]] = [cardsNumEasy[rand], cardsNumEasy[j],]
    }
  }

配列の中に1と2のカードを用意して、フィッシャー–イェーツのシャッフルというアルゴリズムでカードをシャッフルさせる関数です。

createDiv(cardText);
  function createDiv(cardText) {

    const outsideCards = document.createElement('div');
    const div = document.createElement('div');

    outsideCards.className = ('outsideCard');
    div.className = ('card');
    div.textContent = cardText;

    cardsArea.appendChild(outsideCards);
    outsideCards.appendChild(div);
  }

カードを作る関数です。
上でシャッフルした数字を引数に入れて、その数字をcardの文字にしています。
それを先ほど上で紹介したfor文で指定の数だけカードを生成しています。

ゲームの中身!

  const timer = document.getElementById('timer');
  const finalResultTimer = document.getElementById('finalResultTimer');
  const classOnOffCheck = document.getElementsByClassName('card');
  const classCardAreas = document.getElementsByClassName('outsideCard');
  const resultComment = document.getElementById('result-comment');

  let gameCounter = 0;
  let firstNum = 0;
  let secondNum = 0;

  function gameStart() {
    // タイマー開始です。
    startTime = Date.now();
    countUp();
    updateTimeText();
    for (let i = 0; i < classCardAreas.length; i++) {
      classOnOffCheck[i].classList.add('yet-card');
      classCardAreas[i].addEventListener('click', () => {
        // 1枚目の処理です。
        if (gameCounter === 0) {
          gameCounter++;

          classOnOffCheck[i].classList.add('card-show');
          firstNum = classOnOffCheck[i].textContent;
          classCardAreas[i].style.pointerEvents = 'none';

        } else if (gameCounter === 1) {
          // 2枚目の処理
          gameCounter++;
          classOnOffCheck[i].classList.add('card-show');
          secondNum = classOnOffCheck[i].textContent;
          for (let j = 0; j < classCardAreas.length; j++) {
            classCardAreas[j].style.pointerEvents = 'none';
          }
            checkMiniGameResult(firstNum, secondNum);
        }
      });

     // リセットします。
      firstNum = 0;
      secondNum = 0;

      if (level === 1) {
        resultComment.textContent = "EASY"
      } else if (level === 2) {
        resultComment.textContent = "NORMAL"
      } else if (level === 3) {
        resultComment.textContent = "HARD"
      }
    }
  }

タイマー周りの事はとりあえず置いておいて、
場にあるすべてのカードにCSSのクラス yet-cardを追加します。(まだのカードだよって言いたい)

配列の[i]番目のカードをクリックしたら以下の処理を走らせます。

gameCounter=0の場合(1枚目のカードをめくったとき)
gameCounterを1回足して、
CSSのクラス card-showを追加します。(これでカードの数字が見えるようになります。)
変数のfirstNumには[i]番目のtextContentが入るんだよ!!!って書いてます。
一度押されたカードを再び押せないように、pointer-events="none";にしてます!

gameCounter=1の場合(2枚目のカードをめくったとき)
gameCounterを1回足して、
同じくカードの中身が見えるCSSクラスを追加しています。
変数secondNumに[i]番目のtextContentが入りますよ!って書いてます。

次のfor文で、全部のカードを押せないようにしています!
3枚目絶対にめくらせないマンです。

そして…
checkMiniGameResult(firstNum, secondNum);を走らせます。※以下解説

checkMiniGameResult();が終わったゲームのリセット作業をします。(一応)
ついでに、ここで選んだレベルによって最終結果で表示する難易度を設定しています。

checkMiniGameResult(n1, n2);

ここでは1枚目と2枚目が合ってるかどうかの判定と、あっていた時に行う処理、違ったときに行う処理を書いています。
(一度書いた定数は省略する)

  function checkMiniGameResult(n1, n2) {

    for (let j = 0; j < classCardAreas.length; j++) {
      classCardAreas[j].style.pointerEvents = 'none';
    }
    // もし、数字が一致してたら以下の処理をする。
    if (firstNum === secondNum) {
      for (let i = 0; i < classOnOffCheck.length; ++i) {
        if (classOnOffCheck[i].classList.contains('card-show')) {
          classOnOffCheck[i].classList.remove('card-show');
          classOnOffCheck[i].classList.remove('yet-card');
          classOnOffCheck[i].classList.add('keep-show-card');
        }
      }
      gameCounter = 0;
      // 一致してなかったら以下の処理をする。
    } else if (firstNum !== secondNum) {
      // 1秒後に、一時表示クラスを外してウラ表示にする。
      setTimeout(() => {
        for (let i = 0; i < classOnOffCheck.length; ++i) {
          if (classOnOffCheck[i].classList.contains('card-show')) {
            classOnOffCheck[i].classList.remove('card-show');
              }
        }
        // ミスカウンターを+1します。
        missCounter++;
        gameCounter = 0;
      }, 1000);
    }
    for (let k = 0; k < classCardAreas.length; k++) {
      classCardAreas[k].style.pointerEvents = 'auto';
    }

……(続)
 

ここの処理長いので分割して解説します。

引数n1 n2にはfirstNum secondNumがそれぞれ入ります。

絶対にカードを開かせないマンという強い意志を感じるpointer-events="none";
もしかしたらどっちかはいらないかもしれないですね…(遠い目)

1枚目、2枚目が一致してた時は
card-showが含まれているものにだけ処理を走らせます。
→card-show,yet-cardクラスを外します。
yet-cardが外れる=表になったカードって意味です!
→keep-show-cardクラスを付けます。→オモテにしたままです!って意味です。

そして、ゲームカウンターをリセットます。

1枚目、2枚目が一致してない時は1秒後にcard-showクラスが付いているものだけに以下の処理を走らせます。
card-showを外してね!→裏返してね。
そしてmissCounterを+1します。

最後に、カードを再び選択できるような状態にします。
(すべてにかけてますが、cssのkeep-show-cardにはpointer-Events=none;がついてるので選択できないようにしてます!)

続き

  const backBtn = document.getElementById('backBtn');

  let yetOpenCard


    // すでに開いているカードは0枚です。(初期値)

  yetOpenCard = 0;

    // yet-cardクラスがあるかどうかカードの数だけ確認します。
    for (let i = 0; i < classOnOffCheck.length; i++) {
      if (classOnOffCheck[i].classList.contains('yet-card')) {
        yetOpenCard++;
      }
    }
    // 0枚だった場合は最終結果をチェックします。
    if (yetOpenCard === 0) {
      backBtn.style.display = 'none';
      // タイマーを止めてます。
      clearTimeout(timerId);
      timeToadd += Date.now() - startTime;
      checkFinalResult();
    }
……(続)

ここで、startボタンを押した時に発動する、CSSのyet-cardが役に立ちます!
カードの数だけ、変数:yetOpenCardの数を数えます。
yet-cardが0枚(すべてのカードが表になっている状態)の時だけ処理を走らせてます。

そして、checkFinalResult();へ…

続き

checkFinalResult();
  const finalResult = document.getElementById('finalResult');
  const missResult = document.getElementById('missResult');

  let missCounter = 0;

    // 最終結果を確認しています。
    // ---------------------------------

    // 関数:最終結果確認(上で使用しています)
    function checkFinalResult() {

      mainGameArea.style.display = 'none';
      finalResult.style.display = 'block';
      // ミスのリザルトを表示!
      missResult.textContent = `おてつきは${missCounter}回でした!`;

      // いわゆる、リセットボタンです。
      replay();
      resetGame();
    }
    // ----------------------------
    // 関数:リセットボタン(上で使用しています)
    function replay() {

      // リプレイボタンを押した時の動き。
      replayBtn.addEventListener('click', () => {
        // タイマーをリセットしています。
        elapsedTime = 0;
        timeToadd = 0;
        // missCounterをリセットします。
        missCounter = 0;


        // 画面を難易度選択にしています。
        finalResult.style.display = 'none';
        levelChoice.style.display = 'block';
      });
    }
  }
*** resetGame();

ゲームエリアの表示を非表示にして、最終結果の画面に移行させます。
ミスの数を数えてあるので、それを表示してます◎
リプレイボタンを押した時には、いろいろリセットして、
終結果画面から難易度選択画面に移行させてます。

最後!

  // 関数:ゲーム終了時に流す、リセットするやつ。
  function resetGame() {
    const removeCards = document.querySelectorAll('div.outsideCard');
    for (let i = 0; i < removeCards.length; i++) {
      cardsArea.removeChild(removeCards[i]);
    }
  }

div.でかつCSSのクラスがoutsideCardのものを定義して、
その個数分だけ、cardsAreaから上のものを取り除いています。

まとめ

はじめてソースコードの説明をしたので、もしかしたらうまく説明できてなかったり、そもそも説明が間違ってたりするかもしれません。どうかそこはご容赦ください。

流れとしては、
最初にdivを生成て、配列の中をシャッフルする、それを頭から順番につっこんでいく。
(これでカードがバラバラになる)
一時的に表になっている状態のカードをCSSのクラスで判定して、
一致していれば、そのまま開け続けるクラスを付ける。
不一致であれば、一時的に表にするクラスを外すことで裏にしています。

最終的な判断は、最初につけたyet-card(まだ開いてないカードと言いたい)クラスで判定しています。

あとは一般的なタイマー機能を適宜つけ、ミスの回数も数えられるようにした感じです。

ここのロジックの部分は自分で考えたのですが、
うまく実現できなかったところは主人に助言をもらいました。

改めて、ゲームのリンクはこちらです。
fan-s-sinkeisuijaku.netlify.com

このゲームの全ソースコードはここで見られます。
github.com

感想

むずかしかったです。とにかくむずかしかった。
自分の中での解決する手段のパターンが少なすぎて、なかなかやりたいことの実現ができず、悔し涙を流しながら作ったゲームです。
でも、無事にできてよかったです。

おそらくきれいなコードじゃないけど、一生懸命作った大好きな神経衰弱です◎

遊んでくれた方、本当にありがとう。
感想をくれた方、本当に本当にありがとうございました!

また、何か作れたらお知らせさせてください!

長くなりました。
ここまで読んでくれてありがとうございますー!