【Vue.js】まるばつゲーム作ってみた

#完成コード

```vuejs:janken.vue
<template>
  <div id="app">
    <table id="table">
      <tr v-for="(row,rowsIndex) in states" :key="row.id">
        <td
          v-for="(state,colsIndex) in row"
          @click="onSelect(rowsIndex, colsIndex)"
          :key="state.id"
        >
          <div class="maru" v-if="state==1">○</div>
          <div class="batu" v-if="state==2">×</div>
        </td>
      </tr>
    </table>
    <div class="player">
      <div class="player-maru" v-if="playerId==1">「○ プレイヤーさん、マスを選んでください」</div>
      <div class="player-batu" v-if="playerId==2">「× プレイヤーさん、マスを選んでください」</div>
    </div>
    <div class="winner" v-if="winnerText">
      <p v-text="winnerText"></p>
      <img src="../assets/winner.jpg" />
    </div>
  </div>
</template>

<script>
export default {
  el: "#app",
  data() {
    return {
      states: [],
      playerId: 1,
      winnerText: ""
    };
  },
  methods: {
    init: function() {
      this.states = [
        [-1, -1, -1],
        [-1, -1, -1],
        [-1, -1, -1]
      ];
    },
    onSelect: function(rowsIndex, colsIndex) {
      this.winnerText = "";

      if (this.states[rowsIndex][colsIndex] != -1) {
        alert("そのマスは、すでに選択されています！");
      } else {
        let states = JSON.parse(JSON.stringify(this.states)); //JSONに再度元に戻す
        states[rowsIndex][colsIndex] = this.playerId;
        this.states = states; //再度代入
        this.playerId = this.playerId == 1 ? 2 : 1; //三項演算子
        let winnerId = this.getWinnerId();

        if (winnerId != -1) {
          this.init();
          let playerIds = {
            1: "○",
            2: "×"
          };
          this.winnerText =
            playerIds[winnerId] + " さんの勝ちです。おめでとうございます！";
        } else if (this.isDraw()) {
          this.init();
          alert("引き分けです！");
        }
      }
    },
    getWinnerId: function() {
      for (let i = 0; i < 3; i++) {
        // 横の列
        let row = this.states[i];
        if (this.isStatesFilled(row)) {
          return row[0];
        }

        // 縦の列
        let col = [this.states[0][i], this.states[1][i], this.states[2][i]];
        if (this.isStatesFilled(col)) {
          return this.states[0][i];
        }
      }

      // ななめ
      let skew1 = [this.states[0][0], this.states[1][1], this.states[2][2]];
      if (this.isStatesFilled(skew1)) {
        return this.states[0][0];
      }

      let skew2 = [this.states[0][2], this.states[1][1], this.states[2][0]];
      if (this.isStatesFilled(skew2)) {
        return this.states[0][2];
      }

      return -1;
    },
    isStatesFilled: function(states) {
      return (
        states[0] != -1 && states[0] == states[1] && states[1] == states[2]
      );
    },
    isDraw: function() {
      for (let i in this.states) {
        let row = this.states[i];

        for (let j in row) {
          let state = row[j];

          if (state == -1) {
            return false;
          }
        }
      }

      return true;
    }
  },
  mounted: function() {
    this.init();
  }
};
</script>

<style lang="scss">
#table {
  margin: 0 auto;
  border-collapse: collapse;
  border: 3px solid #ccc;
  td {
    border: 1px solid #ccc;
    height: 110px;
    width: 110px;
    text-align: center;
    vertical-align: middle;
    font-size: 80px;
    cursor: pointer;
    .maru {
      color: #f00;
    }
    .batu {
      color: #00f;
    }
  }
}
.player {
  text-align: center;
  font-weight: bold;
  font-size: 16px;
  padding: 20px;
  &-maru {
    color: #f00;
  }
  &-batu {
    color: #00f;
  }
}

.winner {
  text-align: center;
  font-weight: bold;
  font-size: 30px;
  img {
    width: 220px;
    height: 220px;
  }
}
</style>
```

###レイアウトを作成

○、×を書いていくゲームのマス目を作ります。

```vuejs:janken.vue
<html>
<head>

    <style>

        #table {

            margin:0 auto;
            border-collapse: collapse;
            border: 3px solid #ccc;

        }

        #table td {

            border:1px solid #ccc;
            height: 80px;
            width: 80px;
            text-align: center;
            vertical-align: middle;
            font-size: 75px;
            cursor: pointer;

        }

    </style>

</head>
<body>
    <div id="app">
        <table id="table">
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
                <td></td>
            </tr>
        </table>
    </div>
    <!-- 省略 -->
</body>
</html>
```

![vue_marubatsu_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/5780239f-74e1-f8d9-b48d-037774ef18eb.png)

こちらで３×３のマスを作っているだけです。

#Vueに変数をつくる

まず、○×ゲームで必要な変数をつくっていきます。
必要なものは次の２点です。

- マス目の状態（３×３の配列）
- 選択できるプレイヤーのID

３×３の配列の中身は次のとおりです。

- -1　・・・　まだ選択されてない
- 1　・・・　○が選択されている
- 2　・・・　×が選択されている

また、プレイヤーIDは

- 1　・・・　○のプレイヤー
- 2　・・・　×のプレイヤー

そして、これらをVueの変数にするとこうなります。

```javascript:janken.vue
new Vue({
    el: '#app',
    data: {
        states: [
            [-1, -1, -1],
            [-1, -1, -1],
            [-1, -1, -1]
        ],
        playerId: 1 // ○のプレイヤーから開始
    }
})
```

#変数をバインディングする
設定した変数をHTML内にバインディングしていきます。

まずはマス目部分です。

```vuejs:janken.vue
<table id="table">
    <tr v-for="row in states">
        <td v-for="state in row">
            <div style="color:#f00;" v-if="state==1">○</div>
            <div style="color:#00f;" v-if="state==2">×</div>
        </td>
    </tr>
</table>
```
![vue_marubatsu_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/5445bdda-9c2f-3ef1-a73b-82a2ec0bd974.png)

ここでやっていることは、先ほどつくったdata変数のstatesをv-forを使ってループさせ、
３×３のマスを作ると同時に、マス目の状態によって○、×、空白を切り替えています。

###プレイヤー選択機能を追加

playerIdは、現在マスを選ぶことができるプレイヤーのIDなので、
このIDを使って次の２つの表示を切り替えるようにしてみます。

- ○ プレイヤーさん、マスを選んでください
- × プレイヤーさん、マスを選んでください

```vuejs:janken.vue
<table id="table">
    <!-- 省略 -->
</table>

<div style="text-align:center;">
    <div style="color:#f00;" v-if="playerId==1">「○ プレイヤーさん、マスを選んでください」</div>
    <div style="color:#00f;" v-if="playerId==2">「× プレイヤーさん、マスを選んでください」</div>
</div>
```
![vue_marubatsu_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/99a4fff0-da8d-13b1-f703-68ec3f2036e5.png)


#マス目が選択されたときのコードを作成

実際にマス目が選択されたときのコードをつくっていきます。

まずはマス目になる<td>タグにクリックイベントでonSelect()が実行されるようにします。

```vuejs:janken.vue
<table id="table">
    <tr v-for="(row,rowsIndex) in states">
        <td v-for="(state,colsIndex) in row" @click="onSelect(rowsIndex, colsIndex)">
            <!-- 省略 -->
        </td>
    </tr>
</table>
```

選択されたマスがどこなのかが分かるように、v-forにはそれぞれrowsIndexとcolsIndexを追加して
onSelect()へ送っています。

```javascript:janken.vue
new Vue({
    // 省略
    methods: {
        onSelect: function(rowsIndex, colsIndex) {

            if(this.states[rowsIndex][colsIndex] != -1) {

                alert('そのマスは、すでに選択されています！');

            } else {

                var states = JSON.parse(JSON.stringify(this.states))
                states[rowsIndex][colsIndex] = this.playerId;
                this.states = states;
                this.playerId = (this.playerId == 1) ? 2 : 1;

            }

        }
    }
})
```

この中で、まず選択されたマスの状態を確認し、
もし-1でなければ（つまり、すでに選択済みなら）エラーを表示するようにしています。

そして、ここが重要なのですが、**Vueの配列を更新するためには変数自体を書き換える**必要があります。

```
this.states[rowsIndex][colsIndex] = this.playerId;
```

この場合、バインディングされた部分は新しく書き換えられることはありません。

そのため、まず元々のデータを一旦JSONにして再度元に戻すことで配列をコピー。
そして、選択されたマス目のデータを更新し、最後にthis.states全体を書き換えるという方法をとっています。

また、選択が済んだあとは、プレイヤーを交代させるためにplayerIdを変更するようにしています。

ゲームが完了したかどうかをチェックする
これでマス目の選択部分は完了しましたが、肝心のゲームに勝ったかどうかをチェックする機能がついていませんので、この部分を作っていきます。

```javascript:janken.vue
onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        var winnerId = this.getWinnerId();

        if(winnerId != -1) {

            this.states = [
                [-1, -1, -1],
                [-1, -1, -1],
                [-1, -1, -1]
            ];
            playerIds = {
                1: '○',
                2: '×'
            };
            alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます！');

        }

    }

},
getWinnerId: function() {

    for(var i = 0; i < 3 ; i++){

        // 横の列
        var row = this.states[i];
        if(this.isStatesFilled(row)) { return row[0];}

        // 縦の列
        var col = [
            this.states[0][i],
            this.states[1][i],
            this.states[2][i]
        ];
        if(this.isStatesFilled(col)) { return this.states[0][i]; }

    }

    // ななめ
    var skew1 = [
        this.states[0][0],
        this.states[1][1],
        this.states[2][2]
    ];
    if(this.isStatesFilled(skew1)) { return this.states[0][0]; }

    var skew2 = [
        this.states[0][2],
        this.states[1][1],
        this.states[2][0]
    ];
    if(this.isStatesFilled(skew2)) { return this.states[0][2]; }

    return -1;

},
isStatesFilled: function(states) {

    return(
        states[0] != -1 &&
        states[0] == states[1] &&
        states[1] == states[2]
    );

}
```

getWinnerId()の中では、statesの中身をisStatesFilled()を使って次の勝利条件をチェックし、
さらに勝ったプレイヤーのIDを取得しています。

- -1を含まず（まだ選択されていないため）
- 全ての数字が同じ値である

![vue_marubatsu_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1246814/d0f4a6be-dd0c-80f4-650c-c911e12cbe1d.png)


そして、もしこの勝利条件を満たす場合は1もしくは2の勝ったプレイヤーIDを返し、アラートを表示。
さらにstatesを初期値へ戻します。

#引き分けの場合をチェックする
○×ゲームは、引き分けの場合もありますので、勝利条件を満たしていない場合は
isDraw()で引き分けをチェックするようにします。

```java:janken.vue
onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        if(winnerId != -1) {

            // 省略

        } else if(this.isDraw()) {

            this.states = [
                [-1, -1, -1],
                [-1, -1, -1],
                [-1, -1, -1]
            ];
            alert('引き分けです！');

        }

    }

},

// 省略

isDraw: function() {

    for(var i in this.states) {

        var row = this.states[i];

        for(var j in row) {

            var state = row[j];

            if(state == -1) {

                return false;

            }

        }

    }

    return true;

}
```

isDraw()の中ではstatesの中身をすべてチェックして-1が存在していたら（つまりまだゲームは終わっていない）falseを返し、そうでなければゲームは終了しているのに勝敗が決まっていない（＝引き分け）ということでtrueを返します。

そして、もし引き分けの場合はアラートを起動して完了です。


#リファクタリング
今回紹介したコードの中には複数回同じコードが出てくる場面があります。
statesを初期化する部分です。

```javascript:janken.vue
this.states = [
    [-1, -1, -1],
    [-1, -1, -1],
    [-1, -1, -1]
];
```

同じコードはできるだけ一回書くだけにしておくほうがいいのでinit()というメソッドを作ってこの中でstatesを初期化するようにリファクタリングしてみましょう。

```javascript:janken.vue
methods: {
    init: function() {

        this.states = [
            [-1, -1, -1],
            [-1, -1, -1],
            [-1, -1, -1]
        ];

    },

    // 省略
```

そして、init()を呼び出す場所は先ほどのonSelect()の中ですね。

```javascript:janken.vue
onSelect: function(rowsIndex, colsIndex) {

    if(this.states[rowsIndex][colsIndex] != -1) {

        // 省略

    } else {

        // 省略

        if(winnerId != -1) {

            this.init();
            playerIds = {
                1: '○',
                2: '×'
            };
            alert(playerIds[winnerId] +' さんの勝ちです。おめでとうございます！');

        } else if(this.isDraw()) {

            this.init();
            alert('引き分けです！');

        }

    }

},
```

そして、もう一つあります。
データの初期値です。

今まで直接値を設定していましたが、空白の配列へ変更します。

```javascript:janken.vue
data: {
    states: [], // 初期値はmountedの中で設定
    // 省略
},
```

そして、ページが表示されたらすぐに実行されるmountedの中でinit()を実行すると可読性もあがりますし、コードの共通化ができて保守もしやすいでしょう。

```javascript:janken.vue
mounted: function() {

    this.init();

}
```

#勝った時に画像を表示する
先ほどのコードでは、○×どちらかのプレイヤーが勝ったときにアラートを表示するだけでしたが、ちょっとこれだけでは味気ないので勝ったら画像を表示するようにしてみます。（ただし、対戦中は表示しません）

まずは変数部分です。
winnerTextという変数を追加し、どちらかが勝ったときのメッセージをここへ格納します。

```javascript:janken.vue
data: {

    // 省略

    winnerText: ''
},
```

そして、レイアウト部分です。

```javascript:janken.vue
<div v-if="winnerText" style="text-align:center;">
    <h2 v-text="winnerText"></h2>
    <img src="/images/congrats.png">
</div>
```

もしwinnerTextの中にメッセージが入っていれば、そのテキストと画像を表示するようにします。

そして、winnerTextはマスが選択される度に初期化しますが、もしどちらかが勝った場合だけテキストを格納します。（こうすることで対戦中にテキストと画像が表示されることはなくなります）

```javascript:janken.vue
onSelect: function(rowsIndex, colsIndex) {

    this.winnerText = '';

    // 省略

        if(winnerId != -1) {

            // 省略

            this.winnerText = playerIds[winnerId] +' さんの勝ちです。おめでとうございます！';

        }

    // 省略

}
```