Excelに魔法をかけて懐かしのあのゲームにしてみた

# はじめに
　先日、Twitterで「Excelには事務の人がかけた魔法がかかっていて、数字を入れると結果が出てくるようになっている」というような内容のツイートを見かけました。
　それを見て、~~もう少しで魔法使いになる予定の~~私もExcelに何か魔法をかけたいと思ったのですが、如何せんネタがないという問題に直面しました。ただただ数値を計算させる機能を持ったExcelなら、先代の事務の魔法使いたちが既に何度となく作っていそうです。そのオーパーツは、今もそこかしこの企業で決算の時を待ち眠っていたりするのでしょう。
　そこまで考えて思い至りました。「そうか！あのオーパーツを作ってやろう！」と。

# 今回作ったもの

　ということで、今回、Excelで１日くらいかけて**マインスイーパ**を作りました。
![mone_sweeper.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259240/a43dfdea-b0b3-869f-966f-984f08ebff72.png)

　Windows7まで標準搭載されていたため、知らない人は少ないと思いますが、解説として一応[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)のリンクを張っておきます。

　~~Excelでマインスイーパって既に作ったことある人いそうですよね。~~

　一応[GitHub](https://github.com/obsidian-puyo/Excel_Mine_Sweeper)でダウンロードできます。xlsm形式なので警告文が出るかもしれません。
　怖い方はコードだけのテキストも別途ありますので、Excelのシートを画像のようにして、コードにテキストの内容をコピーして頂ければ同じように動くはずです。

---
　
　今回作ったマインスイーパですが、１日で簡単に作ったものであるため、以下の機能が実装されていません。
* 右クリックで旗を立てる
* 地雷以外のマスをすべて開くとクリアになる
* 最初の1クリックでは地雷を引かないようにする
* 地雷を引いた後、ほかのマスを開けないようにする

　まあでもこれらの機能がなくても最低限遊べるのではないかなと思います。

# 実装
　ここからしばらくはプログラミングに興味がない人はとばしてください。
　一応、コード内のコメントは多めに入れてあります。地雷数は15、マス数は10×10固定です。

### 地雷、数字の形成

``` vb

    Private Sub SetBomb()
        Dim i As Integer            ' 地雷数インクリメント
        Dim rand_row As Integer     ' 行選定用
        Dim rand_clm As Integer     ' 列選定用
    
        Dim idx_row As Integer      ' 行インクリメント
        Dim idx_clm As Integer      ' 列インクリメント
    
        ' 地雷の配置処理
        For i = 1 To bombs
            rand_row = Int(Rnd() * 10) + 2
            rand_clm = Int(Rnd() * 10) + 2
        
            If Cells(rand_row, rand_clm).Value = "" Then
                Cells(rand_row, rand_clm).Value = "●"
            Else
                ' 同じ場所を引いた場合再抽選のためデクリメント
                i = i - 1
            End If
        Next    'i
    
        ' 数字の配置処理
        For idx_row = 2 To 11
            For idx_clm = 2 To 11
                Dim cnt As Integer  ' 地雷数カウント
                Dim jdx_row As Integer
                Dim jdx_clm As Integer
            
                ' 地雷のセルは処理を抜ける
                If Cells(idx_row, idx_clm).Value = "●" Then
                    GoTo Continue1
                End If
            
                ' カウント数初期化
                cnt = 0
            
                ' カウント加算
                For jdx_row = (idx_row - 1) To (idx_row + 1)
                    For jdx_clm = (idx_clm - 1) To (idx_clm + 1)
                        If BombCount(jdx_row, jdx_clm) = True Then
                            cnt = cnt + 1
                        End If
                    Next    'jdx_clm
                Next        'jdx_row
            
                ' 数値を配置
                If cnt <> 0 Then
                    Cells(idx_row, idx_clm).Value = CStr(cnt)
                End If
        
    Continue1:          ' continueがないので仕方なくタグに飛ばす
            Next    'idx_clm
        Next        'idx_row
    
        ' セルの内容を非表示
        Range("B2", "K11").NumberFormatLocal = ";;;"

    End Sub
```
　地雷はRnd()を使用してランダムに配置しています。Rnd()の後の+2で爆弾が生成される範囲をゲームで使用するマス内に設定しています。この+2をしないと画像の黄色の位置に地雷が生成されてしまいます。
![mine_sweeper.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259240/a2b0a4b8-5366-6a6e-955c-1950b6edfcbb.png)

　地雷を生成したら次は数字を生成します。
　For文でマス内の**地雷が設置されている以外の**全てのセルに対して、周り8マスに地雷が何個あるか判定してその判定個数をそのまま値として与えてあります。
　ちなみに、この処理は地雷が設置されているマスを除外しないと地雷が数字で上書きされて消えてしまいます。処理飛ばそうとした時に、`Continue For`がなかったため、`Go To`でタグに飛ばさないといけなかったのが地味にめんどくさかったですね。

---

　ここまで処理を行って、配置状況を見えるようにしたものがこちら。
![mine_sweeper1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259240/2e71e25c-6c14-69ff-ddd5-678359a2455a.png)
　いい感じですね。

### マスを開いていく処理

　以下、SelectionChangeイベント内のマス内をクリックした時のコードです。蛇足ではありますが、ClickイベントではなくSelectionChangeイベントなのでキーボードの矢印キーでの移動でも開きます。複数セル選択時は処理を抜けるようにしてあります。

``` vb
    ' 選択セルが範囲内の場合
    If (Target.column >= 2 And Target.column <= 11) And _
       (Target.row >= 2 And Target.row <= 11) Then
        
        ' 爆弾のセルを引いた場合
        If Target.Value = "●" Then
            ' セルの内容を表示
            Target.Interior.Color = RGB(255, 255, 255)
            Target.NumberFormatLocal = ""
            Target.Font.Color = RGB(255, 0, 0)
            
            ' 他の爆弾の表示処理
            Dim idx_row As Integer      ' 行インクリメント
            Dim idx_clm As Integer      ' 列インクリメント
            
            For idx_row = 2 To 11
                For idx_clm = 2 To 11
                    ' セルが爆弾の場合
                    If Cells(idx_row, idx_clm).Value = "●" Then
                        Cells(idx_row, idx_clm).NumberFormatLocal = ""
                    End If
                Next        'idx_clm
            Next            'idx_row
            
        Else
            ' それ以外
            Call ClickFunction(Target.row, Target.column)
        End If
    End If
 ```

　地雷のセルをクリックした場合、文字色を赤色にして他の地雷も表示するようにしています。
　それ以外のセル(空白セル、数字セル)をクリックした場合は、ClickFunctionというメソッドに押したセルの行数と列数を引数として渡して遷移させています。

``` vb
    Private Sub ClickFunction(ByVal p_row As Integer, ByVal p_clm As Integer)

        If Cells(p_row, p_clm).Interior.Color = RGB(255, 255, 255) Then
            Exit Sub
        End If

        If (p_row >= 2 And p_row <= 11) And _
           (p_clm >= 2 And p_clm <= 11) Then
       
            ' セルの内容を表示
            Cells(p_row, p_clm).Interior.Color = RGB(255, 255, 255)
            Cells(p_row, p_clm).NumberFormatLocal = ""
            
            If Cells(p_row, p_clm).Value = "" Then
                ' 空白のセルの場合、周りのセルを表示
                Call ClickFunction(p_row - 1, p_clm - 1)    ' 左上
                Call ClickFunction(p_row - 1, p_clm)        ' 上
                Call ClickFunction(p_row - 1, p_clm + 1)    ' 右上
                Call ClickFunction(p_row, p_clm - 1)        ' 左
                Call ClickFunction(p_row, p_clm + 1)        ' 右
                Call ClickFunction(p_row + 1, p_clm - 1)    ' 左下
                Call ClickFunction(p_row + 1, p_clm)        ' 下
                Call ClickFunction(p_row + 1, p_clm + 1)    ' 右下
                Exit Sub
            Else
                ' 数字のセルの場合、処理終了
                Exit Sub
            End If
        Else
            Exit Sub
        End If
    End Sub
```
　冒頭で、既に開いたことがるセルの場合、処理を抜けるようにしています。最初にコードを書いた時にこの記述が抜けていて、空白セルが二つ以上並んだセルをクリックした時にループ処理になってしまっていました。ちなみに、既に開いたことがあるかどうかの判定は背景色で行っています。

　その後の処理は、クリックしたセルの表示状態にして、クリックしたセルが空白のセルだった場合、周り8マスについて同じ処理をするようにしています。

---

　処理的にはこれくらいです。コメント入れても170行程度ですね。
　Excelのマクロはあまり組んだことがないので、周りくどいコードの書き方になっているところがあるかもしれません。

　実際に遊んでみるとこんな感じ。
![mine_sweeper2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1259240/e117fe94-2cf7-d75e-dce9-17340cb0e6d5.png)

　旗がない以外は結構ストレスなく遊べる気がします。

# 今後の改修
　今後、実装されていない機能を実装する場合について考えてみました。
### 右クリックで旗を立てる
　今回の作りとして、爆弾や数字を初期配置でセルの値として設定し、表示形式で見えなくして、クリック時に表示形式を変えて表示しているだけのため、旗の実装は難しい気がします。

### 地雷以外のマスをすべて開くとクリアになる
　思いつく限り、クリックごとに
* 残りのマス数が地雷設置数以下で文字色が赤色のマスがないか判定
* 10×10マスに未開封で爆弾以外のマスがないか判定

のどちらかでいけないかなあと思います。

　機会があれば作ってみたいですね。

### 最初の1クリックでは地雷を引かないようにする
　地雷の初期配置処理を今回はリセットボタン押下時にしていますが、最初の１回目クリック時に変更すれば行けるのではないかと思います。クリックしたセルに地雷が生成された場合、地雷の抽選を再抽選すれば大丈夫そうですね。

### 地雷を引いた後、ほかのマスを開けないようにする
　調べてみたのですが、ExcelのセルにLockedというプロパティがあるので地雷を引いた時とクリア時にTrueにすれば行けそうですね。

### 地雷数、マス数を変更できるようにする
　地雷数は定数として最初に宣言しているので変数にすればいけるでしょう。地雷数としてどこかに入力項目を作って、リセット時にそこを参照するようにすれば設定できそうです。
　マス数はも同様に入力項目を作って参照するようにすれば作れますが、現在コード中でマス数は始点終点をリテラルで与えているので作り替えが必要になります。リセットボタンも現在はシート中の特定のセルになっていますが、マス数の範囲が変わるとリセットボタンが呑み込まれてしまう恐れがあるため、別途ボタンを作る必要が出てきそうですね。

# さいごに
　Excelに魔法をかけるとマインスイーパにもなるということがわかりましたね。
　……業務中に遊んじゃいけないよ！~~業務中に作ったんだけどね。~~