ライフゲームをVBAで作ってみよう！

これは「[自由研究室 AIRS-Lab](https://www.airs-lab.jp)」の [Advent Calendar](https://qiita.com/advent-calendar/2021/airs-lab)、13日目の記事です。

# 背景
AIRS-Lab の主催者の我妻さんがセル・オートマトンの[記事](https://qiita.com/yuky_az/items/17fff285101ea365982b)を書かれていて、その記事の中で Python で実装されたライフゲームを紹介されてましたので、ここではそれに関連して（私らしく）ライフゲームをVBAで書いていこうと思います。

# ライフゲーム
詳しくは [我妻さんの記事](https://qiita.com/yuky_az/items/17fff285101ea365982b)を見ていただければと思いますが、やっていることは単純で、自分の周りにどれだけ生きているセルがあるかを合計して、その結果で自セルの生死を決めていきます。

# VBA で ライフゲーム
今回は VBA でライフゲームを作っていきます。それほど難しいアルゴリズムではないので、VBA でも充分実装可能ですし、エクセルならではの可視化手法も使えるのでむしろ Python より分かりやすいかも？
こんな感じのものを作っていきますよー。

<iframe width="560" height="315" src="https://www.youtube.com/embed/moMRxKqpx_k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## ワークシート
まずはワークシートの設定をしましょう。

### シートの設定
全セルが格子状になるようにセルの高さと幅を調整しましょう。私の環境では、30ピクセルずつにしています。各セルの値によって生死を表現します。セルの値が 1 の時は「生きている」状態、0 の時は「死んでいる」状態 を表し、「生きている」時は「黒」、「死んでいる」時は「白」で表現することとします。条件付き書式設定を使って表現しますが、条件付き書式設定は VBA のコードで行うこととします。
なお、今回は 50 x 50 セルを一つの「世界」とし、上は下に、左端は右端に接続しているような世界を想定しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276318/04f3fb36-3b74-363d-2501-5f2823c1803d.png" width="64%">

### ワークシートのオブジェクト名
先ほど各種設定をしたワークシートのオブジェクト名を変更しておきましょう。今回は「wsWorld」にしておきました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276318/b29f69ab-cfe7-025c-b5b8-499b7766a00e.png" width="40%">

## VBA
ライフゲームで活動する生命の状態を VBA でコーディングしていきましょう。

### 標準モジュールを追加
標準モジュールを追加しコードを書いていきます。標準モジュールのオブジェクト名は特に変更しなくて構いませんが、今回は「mdlWorld」にしています。

### オプション設定
では、コードを書いていきましょう。
ひとつはいわずもがな、変数の宣言を強制（Option Explicit）しておきます。
あとは、セルとのやり取りを念頭に、配列の開始インデックスを1始まり（Option Base 1）にしておきましょう。

```vb
Option Explicit
Option Base 1
```

### メインプロシージャ
メインのプロシージャはこんな感じです。二次元配列の world を定義し、配列の各要素を 1 か 0 にすることで、「生きている」か「死んでいる」かを表します。大まかな流れなどはコメントを参考にしてください。なお、各サブプロシージャはこの後で見ていきます。

```vb:Click_Start
Public Sub Click_Start()
    Dim r As Long                   '行カウンタ
    Dim c As Long                   '列カウンタ
    Dim msgResult As Long           'メッセージボックスの戻り値
    
    Dim world() As Long             '「世界」用配列
    Const WORLD_H As Long = 50      '「世界」の高さ
    Const WORLD_W As Long = 50      '「世界」の幅

    Dim step As Long                'ステップカウンタ
    Const MAX_STEPS As Long = 300   '最大ステップ数
    
    '「世界」の大きさを定義（2次元配列）
    ReDim world(WORLD_W, WORLD_H)
    
    '「世界」と「生命」状態を初期化
    Call initializeWorld(world)
    'ワークシート上に反映
    Call plotWorld(world)
    
    '開始確認
    msgResult = MsgBox("start ?", vbOKCancel)
    If msgResult = vbCancel Then
        Exit Sub
    End If
    
    For step = 0 To MAX_STEPS
    
        '「世界」を演算（アップデート）
        Call updateWorld(world)
        'ワークシート上に反映
        Call plotWorld(world)
    
        '念のため DoEvents で Windows にコントロールを戻す
        DoEvents
        Application.StatusBar = step
    
    Next
    
    Application.StatusBar = False
    
End Sub
```

### 初期化
初期化処理です。ここでは、条件付き書式設定の設定と、world の各要素の 0 or 1 の設定を行います。
条件付き書式設定は、world の大きさが変えても対応できるように、初期化の都度再設定するようにしています。
各要素の 0 or 1 の設定は、ランダム値が 0.5 未満なら 0、それ以外なら 1 にしています。この 0.5 を変えることで、初期状態の「生きている」セル数を（ある程度）コントロールできます。ここは IF 文で書いてもよいです（面倒だったので IIF で書いてます…）。

```vb:initializeWorld
'初期化（条件付き書式設定と「生きている」セルの初期設定）
Private Sub initializeWorld(ByRef world() As Long)
    Dim r As Long   '行カウンタ
    Dim c As Long   '列カウンタ
    
    With wsWorld
        .Cells.ClearContents
    
        '条件付き書式設定
        'セルの値が 0 のときは、セルの色と文字の色を白にする
        'セルの値が 1 のときは、セルの色と文字の色を黒にする
        With .Range(.Cells(LBound(world, 1), LBound(world, 2)), .Cells(UBound(world, 1), UBound(world, 2)))
            .FormatConditions.Delete
            
            .FormatConditions.Add Type:=xlCellValue, Operator:=xlEqual, Formula1:="0"
            .FormatConditions(1).Interior.Color = RGB(255, 255, 255)
            .FormatConditions(1).Font.Color = RGB(255, 255, 255)
            
            .FormatConditions.Add Type:=xlCellValue, Operator:=xlEqual, Formula1:="1"
            .FormatConditions(2).Interior.Color = RGB(0, 0, 0)
            .FormatConditions(2).Font.Color = RGB(0, 0, 0)
            
        End With
    
    End With
    
    Randomize
    
    For r = LBound(world, 1) To UBound(world, 1)
        For c = LBound(world, 2) To UBound(world, 2)
            'ランダムで0.5未満なら0, それ以外なら1
            world(r, c) = IIf(Rnd < 0.5, 0, 1)
        Next
    Next
    
End Sub
```

### 「世界」を演算して更新
「世界」の状態を演算して更新します。具体的には、各セルについて、そのセルの周囲の「生きている」セル数をカウントして、自セルの状態と周囲の状態をもとに、自セルの状態を更新（「生きている」か「死んでいる」かに）します。各セルの計算をしながらセルの状態を更新していくと、セルの状態がどんどん変わっていってしまう（次のセルの更新前に自セルの状態が変化してしまう）ため、バッファを用意して更新後の状態を格納していきます。最後にバッファから更新後の状態を全コピーします。

```vb:updateWorld
'「世界」を演算して更新
Private Sub updateWorld(ByRef world() As Long)
    Dim r As Long               '行カウンタ
    Dim c As Long               '列カウンタ
    Dim bufWorld() As Long      'バッファ世界
    
    Dim up_r As Long            '上の行番号
    Dim down_r As Long          '下の行番号
    Dim left_c As Long          '左の行番号
    Dim right_c As Long         '右の行番号
    
    Dim self As Long            '自セルの状態
    Dim life_count As Long      '周囲の「生きている」セルの数
    
    'バッファ世界を生成
    ReDim bufWorld(UBound(world, 1), UBound(world, 2))
    
    For r = LBound(world, 1) To UBound(world, 1)
        For c = LBound(world, 2) To UBound(world, 2)

            '自セルの状態を取得する
            self = world(r, c)
            
            '自セルの上下左右の座標を求める
            '「世界」から外れる場合は反対側の座標をセットする
            up_r = IIf(r = LBound(world, 1), UBound(world, 1), r - 1)
            down_r = IIf(r = UBound(world, 1), 1, r + 1)
            left_c = IIf(c = LBound(world, 2), UBound(world, 2), c - 1)
            right_c = IIf(c = UBound(world, 2), 1, c + 1)
            
            '周囲の「生きている」セル数を計算する
            life_count = world(up_r, left_c) + world(up_r, c) + world(up_r, right_c) _
                       + world(r, left_c) + world(r, right_c) _
                       + world(down_r, left_c) + world(down_r, c) + world(down_r, right_c)
            
            If self = 0 And life_count = 3 Then
            '自セルが死んでいてまわりの3セルが生きていれば誕生
                self = 1
            ElseIf self = 1 And (life_count = 2 Or life_count = 3) Then
            '自セルが生きていてまわりの2セルもしくは3セルが生きていれば現状維持（生きたまま）
                self = 1
            Else
            'それ以外は死亡
                self = 0
            End If
            
            '自セルの状態をバッファ世界に書き込む
            bufWorld(r, c) = self
            
        Next
    Next
    
    world = bufWorld
    
End Sub
```

#### 周囲のセルの座標を計算
あるセルを基準にすると上下左右の行インデックス、列インデックスはこのようになります。なお、自セルが「世界」の境界である場合、例えば、一番上（1行目、つまり r=1）にあるとき、r-1 とすると 0 になり、インデックスの範囲外となってしまいます（Option Base 1 としていることを思い出してください）。この「世界」は上下、左右が連結されているので、その場合、一番下の行が自セルの「上の」行として扱われます。`up_r = IIf(r = LBound(world, 1), UBound(world, 1), r - 1)` でそれを表現（計算）しています。`down_r`, `left_c`, `right_c` も同様です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276318/e76c20b9-7862-e109-9358-dac198ecfe65.png" width="64%">

#### 周囲の状態を計算
そうして求まった座標（行インデックス、列インデックス）を利用して、自セルの周囲の「生きている」セルをカウントします。「生きている」セルは値が 1 になっていて、「死んでいる」セルは値が 0 になっているので、単純に加算すれば「生きている」セルの数が求まります。以下のコードがその部分です。

```vb
'周囲の「生きている」セル数を計算する
life_count = world(up_r, left_c) + world(up_r, c) + world(up_r, right_c) _
           + world(r, left_c) + world(r, right_c) _
           + world(down_r, left_c) + world(down_r, c) + world(down_r, right_c)
```

#### 生死の判定
自セルの状態と上で求めた周囲のセルの状態をもとに、自セルの状態を判定します。
ロジックは以下の通りです。
　・自セルが「死んで」いて、周囲に「生きている」セルが 3つある場合、自セルは「生きている」状態になる。（誕生）
　・自セルが「生きて」いて、周囲に「生きている」セルが 2つか3つある場合、自セルは現状維持。（生きたまま）
　・それ以外の場合、自セルは「死ん」だ状態になる。（死亡）

```vb
If self = 0 And life_count = 3 Then
'自セルが死んでいてまわりの3セルが生きていれば誕生
    self = 1
ElseIf self = 1 And (life_count = 2 Or life_count = 3) Then
'自セルが生きていてまわりの2セルもしくは3セルが生きていれば現状維持（生きたまま）
    self = 1
Else
'それ以外は死亡
    self = 0
End If
```

#### 状態を更新
自セルの状態が更新されたら、バッファ世界に書き込みます。（`bufWorld(r, c) = self`）
全セルの状態更新が完了したら、バッファ世界から「世界」に状態をコピーします。（`world = bufWorld`）

### シートに書き出す
最後に world の値をシートに書き出します。シートに書き出されると、条件付き書式設定により、「生きている」セルは「黒」、「死んでいる」セルは「白」に着色されます。なお、セルの範囲は world のサイズから求めています。

```vb:plotWorld
'worldの中身をセルに書き出す
Private Sub plotWorld(ByRef world() As Long)

    With wsWorld
        .Range(.Cells(LBound(world, 1), LBound(world, 2)), .Cells(UBound(world, 1), UBound(world, 2))).Value = world
    End With

End Sub
```

## 完成
これで完成です。ボタンを作成して Click_Start() を登録してもよいですし、そのまま実行してもよいでしょう。
WORLD_H, WORLD_W を変更して「世界」の大きさを変えたり、MAX_STEPS で最大ステップ数を変更してどのようになるか観察してもよいかもしれませんね。