2019.09.17

マルバツゲームを作ってみよう~その3:クリックイベント編


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

今回は「クリックイベント編」ということで、画面をクリックしたときの動きをJavaScript で書いていきます!

今回実装する機能

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

  • マス目をクリックしたときに、マルの番であれば○が、バツの番であれば×が、マス目に記入される(マルバツ記入機能)。
  • マス目をクリックしたときに、メッセージが切り替わる(メッセージ切り替え機能)。
実装する機能

以下、順番に実装していきましょう。

マルバツ記入機能

まずはマルバツ記入機能から。

実装の考え方

マルバツゲームはターン制のゲームです。マルを記入したら次はバツの番、バツを記入したら次はマルの番…とターンが交互に切り替わります。

また、上でも述べた通り、マルの番にマス目をクリックすれば○が記入され、バツの番にマス目をクリックすれば×が記入されます。

つまり、「マルの番」と「バツの番」という状態を区別し、クリックするたびにこの状態(=ターン)が交互に切り替わるような仕組みをつくってやる必要があるということです。

「クリックするたびに切り替わる」と言えば、トグルボタンが思い浮かびますね💡
On/Off を切り替えるように、マルのときにクリックしたら次はバツになり、バツのときにクリックしたら次はマルになり…とすればターン制を表現できそうです。

ということで、マルバツ記入機能はトグルボタンの要領で実装したいと思います。

実装

なにはともあれ、まずは、クリックイベントの発火元である、マス目(squareクラスが付与された要素)を取得します。
取得してきたsquare要素たちをforEachで回して、その一つ一つにaddEventListener()処理を書いていく感じですね。

app.js
// square を取得
const squares = document.querySelectorAll('.square');
// Array に変換 ※IE11対策
const squaresArray = [].slice.call(squares);

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

  });
});

2行目で

const squaresArray = [].slice.call(squares);

としていますが、これは1行目で取得したNodeListArrayに変換する処理です。

querySelectorAll()メソッドは、配列(Array)ではなく、NodeListという、「配列風オブジェクト」を返します。
Chrome 等の多くのモダンブラウザでは、配列風オブジェクトでもforEachメソッドが使えるのですが、IE11 では使えません。
ですので、IE11 対策として、2行目で「配列風オブジェクト」を「配列」に変換しているというわけです。

外枠ができたところで、addEventListener()のコールバック関数の中にクリック時の処理を書いていきます。

app.js
// flagがfalseのときバツのターン、trueのときマルのターン
let flag = false;

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

    if (flag === true) {
      square.classList.add('js-maru-checked');
      flag = false;

    } else {
      square.classList.add('js-batsu-checked');
      flag = true;
    }
  });
});

フラグを用意して、処理をif で切り替えるのは、トグルボタンを実装するときの定番パターンですね。

flag === trueのときマルのターンで、そうでなければバツのターンです。
仕様ではバツが先攻となっているので、初期値はfalseです。

それぞれのターンで、classList.add()を使用して、前回作成したjs-maru-checkedjs-batsu-checkedをクリックされたマス目に付与しています。

これで「マルバツ記入機能」は完成!…ではありません!😅

このままだと、一度クリックしたマス目もまたクリックできてしまいます(下のCodePenで試してみてください)。

ということで、一度クリックしたマス目はクリックできないようにするための処理を加えます。

まずCSSのクラスを用意します。

app.css
.js-unclickable {
  pointer-events: none;
}

pointer-events: none;とすると、ホバーやクリックなどのマウスイベントを無効にできます。

このクラスをクリック時に一緒につけるようにすれば、完成です!

app.js
// flagがfalseのときバツのターン、trueのときマルのターン
let flag = false;

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

    if (flag === true) {
      square.classList.add('js-maru-checked');
      square.classList.add('js-unclickable');
      flag = false;

    } else {
      square.classList.add('js-batsu-checked');
      square.classList.add('js-unclickable');
      flag = true;
    }
  });
});

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

マス目をクリックすると、×→○→×…の順で、交互にマークが記入されます。

メッセージ切り替え機能

続いて、ゲームの進行に応じてメッセージを切り替える機能(メッセージ切り替え機能)を実装します。

実装の考え方

  1. メッセージ一つ一つにid を持たせる。

    ID メッセージ 表示されるタイミング
    batsu-turn ×のばん バツの番のときに表示される。
    maru-turn ○のばん マルの番のときに表示される。
    batsu-win ×の勝ち! バツが勝ったときに表示される。
    maru-win ○の勝ち! マルが勝ったときに表示される。
    draw 引き分け 引き分けになったときに表示される。
  2. 上で実装したマス目のクリックイベントの中に、特定のid のメッセージを表示させる(他のメッセージを隠す)処理を書く。

これでいけそうです。

実装

まず各メッセージにid を持たせます。

index.html
<div class="message-container">
  <ul class="message-list">
    <li id="maru-turn" class="js-hidden">
      <span class="maru"></span>のばん
    </li>
    <li id="batsu-turn">
      <span class="batsu">×</span>のばん
    </li>
    <li id="maru-win" class="js-hidden">
      <span class="maru"></span>の勝ち!
    </li>
    <li id="batsu-win" class="js-hidden">
      <span class="batsu">×</span>の勝ち!
    </li>
    <li id="draw" class="js-hidden">
      引き分け
    </li>
  </ul>
</div>

指定したid のメッセージからjs-hiddenを取り除き、それ以外のid のメッセージにjs-hiddenを付与するという処理を書いていきます。

まずmessage-listli要素を取得します。これは上でsquareを取得したのと同じですね。

app.js
const messages = document.querySelectorAll('.message-list li');
const messagesArray = [].slice.call(messages);

特定のメッセージのみ表示して、それ以外は隠したいので、例えば、「○のばん」(maru-turn) を表示させるコードは次のようになるでしょう。

messagesArray.forEach(function (message) {
  if (message.id === 'maru-turn') {
    message.classList.remove('js-hidden');
  } else {
    message.classList.add('js-hidden');
  }
});

maru-turn以外のメッセージも全部同じ処理で表示できるので、関数にまとめちゃいましょう。

app.js
// メッセージ切り替え関数
function setMessage(id) {
  messagesArray.forEach(function (message) {
    if (message.id === id) {
      message.classList.remove('js-hidden');
    } else {
      message.classList.add('js-hidden');
    }
  }); 
}

これでsetMessage(表示させたいメッセージのID)と書けば、メッセージを切り替えられるようになります。

以上を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');
      setMessage('batsu-turn');
      flag = false;

    } else {
      square.classList.add('js-batsu-checked');
      square.classList.add('js-unclickable');
      setMessage('maru-turn');
      flag = true;
    }
  });
});

これで完成です!

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

マス目をクリックするとメッセージが切り替わるようになりました。

でもちょっとメッセージが切り替わる時のガタつきが気になりますね。
「○」と「×」だけが入れ替わって、「のばん」の部分は固定されていてほしいです。

なので、ちょっとスタイルを加えます。

app.css
.symbol-holder {
  display: inline-block;
  width: 20px;
}
index.html
<li id="maru-turn">
  <span class="maru symbol-holder"></span>のばん
</li>

「○」と「×」で微妙に文字の幅が違うせいでガタついてしまうんですよね。
ということでspanに幅20px を持たせ、記号部分の幅が一定になるようにしました。

これでメッセージがスムーズに切り替わるようになったかと思います。

See the Pen tic-tac-toe3-3 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) -->