2019.09.17

マルバツゲームを作ってみよう~その4:勝敗判定ロジック編


「マルバツゲームを作ってみよう」の連載第4回目です。

今回は「勝敗判定ロジック編」ということで、ゲームの勝敗や引き分けを判定するロジックを JavaScript で書いていきます!

今回実装する機能

今回実装するのは、以下の機能です。

  • ゲームの勝敗を判定する(勝敗判定)
  • 9マス全て埋まった時点で勝敗がついていない場合は「引き分け」にする(引き分け判定)

少々複雑な処理になりますが、頑張って実装していきましょう😁

勝敗判定

実装の考え方

いつ判定するか

ロジックの中身に入る前に、この「勝敗判定」はいつ行われるのか、考えてみましょう。

私たち人間がマルバツゲームを行っている際にはあまり意識しないかもしれませんが、ゲームの流れをフローチャートに落とし込むと以下のようになっているはずです。

フローチャート

自分のマークを書き込んだ後、自分が勝っているかどうか判定し、自分が勝っていれば自分の勝ち、勝っていなければ相手のターン、という流れです。

つまり、「勝敗判定」は、マス目をクリックしてマークを記入するたびに行われます。

※上図で「ターンカウンター」や「残ターン数」という用語が出てきていますが、こちらについては、第三項「引き分けの場合について」を参照してください。

どのように判定するのか

色々な考え方があるとは思いますが、今回は次のように考えました。

マルバツゲームでは、タテ・ヨコ・ナナメのいずれか一列(この記事では「ライン」と呼ぶことにします)を先に自分のマークで埋めた方が勝ちとなります。
ラインは全部で8列あります(タテ3列、ヨコ3列、ナナメ2列)。

もっとスマートなやり方があるのかもしれませんが、今回はたった8列しかありませんので、マス目をクリックするたびに、8つのライン全部をチェックすることにしました。

8つのラインをチェックし、そのうち全てのマス目が自分のマークで埋まっているようなラインが1列でもあれば自分の勝ち。そうでなければ相手のターン。というようにロジックを組み立てることができそうです。

また、マス目にIDを振れば、ラインは「マス目のIDの配列」として表現できそうです。

マス目と列

上の図でいうと、Line1 は、['1-1', '1-2', '1-3']と表せます。

実装

まずマス目に ID を振ります。

index.html
<div class="squares-container">
  <div class="squares-box">
    <div class="square" id="1-1"></div>
    <div class="square" id="1-2"></div>
    <div class="square" id="1-3"></div>
    <div class="square" id="2-1"></div>
    <div class="square" id="2-2"></div>
    <div class="square" id="2-3"></div>
    <div class="square" id="3-1"></div>
    <div class="square" id="3-2"></div>
    <div class="square" id="3-3"></div>
  </div>
</div>

マス目自体は第三回目で実装したコードで取得済みです(squaresArray)。
これらのマス目を篩い分けて8つのラインにします。

例えば、Line1 は、id が、1-1、1-2、1-3 のマス目をsquaresArrayの中から篩い分けて生成します。
filter()メソッドを使って次のように書けるでしょう。

const line1 = squaresArray.filter(function(e) {
  return (e.id === '1-1' || e.id === '1-2' || e.id === '1-3');
});

全てのラインに共通の処理なので、上手いこと関数化したいですね。
このような感じでしょうか。

app.js
function filterById(targetArray, idArray) {
  return targetArray.filter(function(e) {
    return (e.id === idArray[0] || e.id === idArray[1] || e.id === idArray[2]);
  });
}

元となる配列と ID の配列を渡すと、元となる配列から当該 ID のアイテムを抜き出してくれるという関数です。

これで

const line1 = filterById(squaresArray, ['1-1', '1-2', '1-3']);

のように書くことができます!

他のラインも同様に作ります。

app.js
const line1 = filterById(squaresArray, ['1-1', '1-2', '1-3']);
const line2 = filterById(squaresArray, ['2-1', '2-2', '2-3']);
const line3 = filterById(squaresArray, ['3-1', '3-2', '3-3']);
const line4 = filterById(squaresArray, ['1-1', '2-1', '3-1']);
const line5 = filterById(squaresArray, ['1-2', '2-2', '3-2']);
const line6 = filterById(squaresArray, ['1-3', '2-3', '3-3']);
const line7 = filterById(squaresArray, ['1-1', '2-2', '3-3']);
const line8 = filterById(squaresArray, ['1-3', '2-2', '3-1']);

ようやく下準備ができました。これからいよいよ勝敗判定のロジックを組んでいきます。

上でも書きましたが、「8つのラインをチェックし、そのうち全てのマス目が自分のマークで埋まっているようなラインが1列でもあれば自分の勝ち。」です。

以上を分解すると、

  1. あるラインについて、全てのマス目が自分のマークで埋まっているかどうかの判定
  2. 8つのラインのうち、上記条件を満たすラインが1列でもあるかどうかの判定

という2重の判定になっていることがわかることかと思います。

まず一つ目の判定について。

例えば、Line1 のマス目が全てマルかどうかは、次のコードで判定できます。

const subResult = line1.every(function (square) {
  return square.classList.contains('js-maru-checked');
});
return subResult; 

every()メソッドは、テスト関数に全ての要素がパスした場合にtrueを返すメソッドです。
上のコードは、line1 の全ての square がjs-maru-checkedクラスを持っていた場合にtrueを返します。

次に二つ目の判定について。

二つ目の判定では、上記判定を全てのラインについて行い、パスするラインが1列でもあるかどうかを判定します。

コードにすると次のようになります。

const lineArray = [line1, line2, line3, line4, line5, line6, line7, line8];

const result = lineArray.some(function (line) { 
  
  // 一つ目の判定
  const subResult = line.every(function (square) {
    return square.classList.contains('js-maru-checked');
  });
  return subResult;

});
return result;

some()メソッドは、テスト関数に少なくとも1つの要素がパスした場合にtrueを返すメソッドです(every()とは対照的ですね)。 上のコードは、一つ目の判定にパスするラインが1列でもあった場合にtrueを返します。

以上をまとめて、勝敗判定関数を作ります。
それがこちらです。

app.js
// 勝敗判定
function isWinner(symbol) { 
  const result = lineArray.some(function (line) {
    const subResult = line.every(function (square) {
      if (symbol === 'maru') {
        return square.classList.contains('js-maru-checked');
      } else 
      if (symbol === 'batsu') {
        return square.classList.contains('js-batsu-checked');
      }
    });
    return subResult;
  });
  return result;
}

引数に'maru''batsu'を受け取って条件分岐することにより、マル・バツ両方の勝敗を判定できるようにしました。

例によってsquare.addEventListener()の中に組み込みます。

app.js
// マス目をクリックしたときにイベント発火
squaresArray.forEach(function (square) {
  square.addEventListener('click', function () {

    if (flag === true) {
      square.classList.add('js-maru-checked');
      square.classList.add('js-unclickable');
      
      // マル勝利判定
      if (isWinner('maru')) {
        setMessage('maru-win');
        return;
      }
      
      setMessage('batsu-turn');
      flag = false;

    } else {
      // 省略。バツも同様
    }
  });
});

マル印を記入した後、マルが勝ったかどうか判定し、勝った場合は「○の勝ち!」のメッセージをセットしてリターン、そうでない場合は、「×のばん」のメッセージをセットして、ターンを切り替えます。

勝敗判定ロジックが完成しました!

See the Pen tic-tac-toe4-1 by Shinichi Kurita (@kuri-ta) on CodePen.

引き分け判定

実装の考え方

私たち人間がマルバツゲームをする際には、ターン数よりも残りのマス目の数を気にするかもしれませんね。
残りのマス目が 0 になったらゲーム終了、その時点で勝敗がついていなければ引き分け、というふうに考えることでしょう。

ですがマルバツゲームの場合、1 ターンに記入できるマス目の数は 1 つまでと決められているので、残りのターン数=残りのマス目の数となります。

残りのマス目を数えるよりもターン数をカウントしていく方が簡単ですので、今回はターン数に着目して引き分け判定を実装します。

初期値 9(マス目が全部で9マスあるため)のカウンターを用意して、ターンが移るたびに -1 していき、勝敗がつかない状態で0 になったら引き分け、というふうにすればいけそうですね😊

実装

まずターン数をカウントするための変数を用意します。

app.js
let counter = 9;

マス目をクリックするたびに、1ずつカウントダウンしていきます。そして勝敗がつかないままcounterが0になった場合、「引き分け」になります。

以上の処理をsquares.addEventListener()内の一番最後の方に書きます。

app.js
// マス目をクリックしたときにイベント発火
squaresArray.forEach(function (square) {
  square.addEventListener('click', function () {

    if (flag === true) {
      // 省略
    } else {
      // 省略
    }

    counter--;
    // 引き分け
    if (counter === 0) {
      setMessage('draw');
    }

  });
});

これで引き分けも判定できるようになりました!

See the Pen tic-tac-toe4-2 by Shinichi Kurita (@kuri-ta) on CodePen.


以上、勝敗判定と引き分け判定機能が実装できました🥳
だいぶゲームらしくなってきましたね。

残す機能もあとわずかです。
次回はゲーム終了後の処理と、ゲーム初期化処理を実装します!

次回もお楽しみに!

連載記事


<!-- Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com --><!-- License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->