Vue.js でマインスイーパっぽいものを作ってみた

# 🥚初めに

コーディングの練習のために、個人的に慣れ親しんでいる Vue.js で単純な実装のゲームとされるマインスイーパを作ってみました。

マインスイーパの仕様をきちんと調べたわけではないため、動きに若干不安がありますが、そのあたりは漸次改善していく予定です。

# 🐣本題

## 環境

||バージョン|
|:--|:--|
|Node|v12.11.1|
|npm|6.11.3|
|vue-cli|4.0.5

## 成果物

それっぽい動きしています。

![minesweeper.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519262/d00dc61d-4800-f287-7ba3-2bbeb3ba6c12.gif)


[ソースコード（github）](https://github.com/qianer-fengtian/vue-minesweeper)

## 実装のポイント

### マインスイーパーの各マスを作る

```html
          <template v-for="(cell, yi) in row">
            <td
              v-if="started"
              :key="yi"
              class="cell cell--started"
              :class="{ 'cell--opened': cell.isOpen }"
              @click="open(xi, yi)"
              @click.right.prevent="mark(xi, yi)"
            >
              <span v-if="cell.isOpen">
                {{ cell.isBomb ? "💣" : cell.bombCount === 0 ? "" : cell.bombCount }}
              </span>
              <span v-if="cell.isMark">
                🚩
              </span>
            </td>
            <td
              v-if="!started"
              class="cell cell--unstarted"
              :key="yi"
            >
              &nbsp;
            </td>
```

マインスイーパーの盤面自体は table 要素で作っています。
地雷が配置される各マスは、td 要素で表現しています。
v-if によりフラグを切り替えることで、以下の状態を切り替えています。

- そもそもゲームが開始されているかどうか
- マスが開かれているかどうか
- 旗が立てられているかどうか

また以下のように、マスをクリックすることで、マスを開いたり旗を立てたりする処理を発火させるようにしています。

- 左クリック ... マスを開く処理を発火する
- 右クリック ... 旗を立てる処理を発火する

### 地雷を配置する

```javascript

    /**
     * 地雷を配置します.
     */
    _layMines() {
      // ランダムな整数値を返す
      const getRandomInt = max => {
        return Math.floor(Math.random() * Math.floor(max));
      }
      for (let i = 0; i < this.numOfBomb; i++) {
        const x = getRandomInt(this.maxX);
        const y = getRandomInt(this.maxY);
        // 既に地雷が置かれている場合、別のセルに置くようにする
        if (this.area[x][y].isBomb) {
          i--;
          continue;
        }
        this.area[x][y].isBomb = true;
        // 地雷に隣接するセルの周囲の地雷数を増やす
        // 上のセル
        if (x-1 >= 0) {
          this.area[x-1][y].bombCount++;
          if (y-1 >= 0) this.area[x-1][y-1].bombCount++;
          if (y+1 < this.maxY) this.area[x-1][y+1].bombCount++;
        }
        // 横のセル
        if (y-1 >= 0) this.area[x][y-1].bombCount++;
        if (y+1 < this.maxY) this.area[x][y+1].bombCount++;
        // 下のセル
        if (x+1 < this.maxX) {
          this.area[x+1][y].bombCount++;
          if (y-1 >= 0) this.area[x+1][y-1].bombCount++;
          if (y+1 < this.maxY) this.area[x+1][y+1].bombCount++;
        }
      }      
    }
  }
```

設定された地雷の数だけ繰り返して地雷を盤面に設置しています。
ランダムな x 行目・ y 列目を取得し、その x 行 y 列のマスにボムであるフラグを立てます。
フラグを立てた後、周囲8マスの「周囲に地雷がある数」を +1 させています。

### マスを開く

```javascript

    /**
     * セルを開きます.
     * @param {Number} x 何行目.
     * @param {Number} y 何列目.
     */
    open(x, y) {
      // 💣だったら終了
      if (this.area[x][y].isBomb) {
        alert("Bomb!!!");
        for (let i = 0; i < this.maxX; i++) {
          for (let j = 0; j < this.maxY; j++) {
            this.area[i][j].isOpen = true;
          }
        }
      }
      this.area[x][y].isOpen = true;
      if (this.area[x][y].bombCount !== 0) return;
      // 周囲に地雷がない空きセルを開く
      // 左のセル
      if (x-1 >= 0 && this.area[x-1][y].autoOpenable()) {
        this.open(x-1, y);
      }
      // 右のセル
      if (x+1 <= this.maxX-1 && this.area[x+1][y].autoOpenable()) {
        this.open(x+1, y);
      }
      // 上のセル
      if (y-1 >= 0 && this.area[x][y-1].autoOpenable()) {
        this.open(x, y-1);
      }
      // 下のセル
      if (y+1 <= this.maxY-1 && this.area[x][y+1].autoOpenable()) {
        this.open(x, y+1);
      }
    },
```

マスを開く処理では、単にクリックされたマスを開くとともに、「周囲に一つも地雷がない」隣接するマスを再帰的に開いていく処理を行っています。

具体的には、以下のような再帰処理を行っています。
1回目に実行される(A)では、クリックされたマスに対して判定処理を行います。

1. (A)マスの上下左右を開くことができるか判定する。
  2. 開くことができる場合
      1. マスを開く。
      2. 開いたマスに対して、(A)の処理を行う。
  2. 開くことができない場合
      1. 終了。

マスを開くことができるか判定する処理は、以下の通りです。

```javascript
  /**
   * 自動で開くことができるか判定して返します.
   * @return {Boolean}
   */
  autoOpenable() {
    return !this.isOpen && !this.isBomb;
  }
```

### 旗を立てる

```javascript
    /**
     * セルに旗を立てたり、立てなかったりします.
     * @param {Number} x 何行目.
     * @param {Number} y 何列目.
     */
    mark(x, y) {
      if (!this.area[x][y].isOpen) {
        this.area[x][y].isMark = !this.area[x][y].isMark;
      }
    },
```

旗を立てる処理はごく単純で、マスがまだ開かれていない場合、旗を立てるか立てないかを切り替えるようにしています。

# 🐥おわりに

今回はマインスイーパ（？）を作りましたが、今後はテトリスやぷよぷよも作ってみたいと思います。
Vue.js で作るのもいいですし、生のJSやSVG、canvasで作るのも面白そうです。

# 参考文献

- [マインスイーパ - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%A4%E3%83%BC%E3%83%91)