C#でライフゲームを書いた

オブジェクト指向プログラミングをより深く理解するためにライフゲームの書いたのでQiitaの記法になれるのにちょうどよさそうなので記事にしてみます。
##ライフゲームとは
ライフゲームとは何ぞやという話は[Wikipedia:ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)でも見てください。
ライフゲームはルールが単純で、プログラミングの基本的な動作を試してみるにはうってつけのテーマです。

10年以上前にRubyで書いてWebUIで実装したことがあったけど、せっかくのRubyなのにオブジェクト指向とか全然関係ないコードだったので、C#でオブジェクト指向をちょっと意識して書いてみました。
##クラス設計
３つのクラスを作ることにしました。

- ライフゲームクラス（ロジック本体）
 - セルの縦横のサイズをフィールドで定義
 - セルの状態を保持する二次元配列をプロパティで定義
 - コンストラクタで縦横サイズを受け取り、フィールドを埋めて二次元配列を初期化
 - プロパティにセルの初期値を入れる
 - (x,y)座標の１つのセルの次の世代の生死を判定するメソッド
 - １つのセルの生死判定するメソッドを使って、二次元配列全体の次世代のセルの状態を二次元配列で返すメソッド
 - 二次元配列を展開して、生死状態を□と■で表示するメソッド
- `int[,]`の配列を`bool[,]`の配列に変換するスタティックメソッドのためのクラス
- 初期値の設定と入出力を制御するクラス(Main)

セルの状態を二次元配列で真偽値`bool`を格納します。

##ライフゲームクラス
メインのロジックを記述したクラスです。
三項演算子を多用しているので見づらいかもしれません。
今回はセルの状態を二次元配列に保存するので、インデクサーが使えるのかなと一応書いてみたのですが、結局使わなかったので削除しました。
プロパティに二次元配列を設定して、二次元配列を直接操作すれば足りるようです。

生死を判定する`DeadOrAlive`メソッドについて補足すると、たとえば(x,y)座標の生死を判定する場合、(x-1,y-1)から(x+1,y+1)までの9つのセルの生死をチェックしたうえで、自分の現在のセル(x,y)の状態によって次世代のセルの状態が決まります。この時、(x,y)が外枠に面している場合。たとえば(0,0)座標の場合は(-1,-1)になるため不都合が出るわけです。なので、外枠の場合は存在しない配列のインデックスを叩かないように前処理をしています。
`DeadOrAlive`メソッドは引数に単一の座標を与えて、その次世代の生死を真偽値で返すだけのものです。外部から呼び出されても使い道がないのでアクセス修飾子は`private`になっています。

この`DeadOrAlive`メソッドを使うのが、`GetNewGen`メソッドです。このメソッドを実行すると、プロパティで設定した現在のセルの状態を格納した二次元配列`Stat`のすべてのセルについて逐一`DeadOrAlive`メソッドを実行し次の世代のセルの状態を取得して、次世代のセルの状態を格納した二次元配列`bool[,]`を返します。

このメソッドを使う時に引数を何も与えないとプロパティの二次元配列`Stat`について評価して次世代を返しますが、メソッドを呼び出すときに任意の二次元配列を引数として与えたとき、その二次元配列について次世代のセルの状態を返すようにオーバーロードを定義しています。スタティックメソッドのように使うことができますますが、今回は使っていません。

`ViewStat`メソッドはプロパティの二次元配列`Stat`をConsoleに表示するためのメソッドです。フォームに表示したいときなどには使うことはないと思います。そういう意味ではこのメソッドがこのクラスに入っているのは美しくないかもしれません。
`ViewStat`メソッドもオーバーロードを書いておいたのでメソッド呼び出し時に任意の二次元配列を入れることで任意の配列の状態をコンソールに表示することができます。

~~~CSharp
 class LifeGame
    {
        private int _rowSize;
        private int _colSize;
        /// <summary>
        /// コンストラクタ
        /// セルの大きさを定義して、二次元配列(Stat)を初期化する
        /// <param name="val1">行数(row)</param>
        /// <param name="val2">列数(col)</param>
        public LifeGame(int val1, int val2)
        {
            _rowSize = val1;
            _colSize = val2;
            Stat = new bool[val1, val2];    // セルの状態を格納する二次元配列を初期化
        }

        /// <summary>
        /// 現在のセルの二次元配列のプロパティ
        /// </summary>
        public bool[,] Stat { get; set; }

        /// <summary>
        /// 生死を判定するメソッド
        /// </summary>
        /// <param name="v1">行番号</param>
        /// <param name="v2">列番号</param>
        /// <returns></returns>
        private bool DeadOrAlive(int v1, int v2)
        {
            /*
             * 座標(v1,v2)の隣り合う8個のセルと自分のセルの生死状態を確認する
             * セルの外枠部分は隣り合うセルの数が違うので、前処理する。
             */
            var life = 0;
            int row1 = (v1 == 0 ? 0 : v1 - 1);
            int row2 = (v1 == _rowSize - 1 ? _rowSize - 1 : v1 + 1);
            int col1 = (v2 == 0 ? 0 : v2 - 1);
            int col2 = (v2 == _colSize - 1 ? _colSize - 1 : v2 + 1);

            for (int i = row1; i <= row2; i++)
            {
                for (int j = col1; j <= col2; j++)
                {
                    if (Stat[i, j])    // 真偽値が入っているのでそのままif文に入れられる
                    {
                        life++;
                    }
                }
            }
            /*
             * セルが生きている場合
             */
            if (Stat[v1, v2])
            {
                if (!(life < 3 || life > 4))  // 自分の「生」の分が含まれているので+1して評価
                {
                    return true;            // 2または3なら生存
                }
                else
                {
                    return false;           // 1以下なら過疎で死滅、4以上でも過密で死滅
                }
            }
            /*
             * セルが死んでいる場合
             */
            else
            {
                if (life == 3)
                {
                    return true;            // 3なら誕生
                }
                else
                {
                    return false;           // それ以外は変わらない
                }
            }
        }
        /// <summary>
        /// オーバーロードを使ってGetNewGenメソッドに
        /// 任意の二次元配列が与えられたとき、その二次元配列を1世代進める
        /// 今回は使ってない
        /// </summary>
        /// <param name="">ｎ世代のセルの状態</param>
        /// <returns>ｎ＋１世代のセルの状態</returns>
        public bool[,] GetNewGen(bool[,] val)
        {
            int rowLength = val.GetLength(0);
            int colLength = val.GetLength(1);
            bool[,] bl = new bool[rowLength, colLength];
            for (int i = 0; i < rowLength; i++)
            {
                for (int j = 0; j < colLength; j++)
                {
                    bl[i, j] = DeadOrAlive(i, j);
                }
            }
            return bl;
        }
        /// <summary>
        /// プロパティで定義したStatの中身を1世代進めて
        /// 次の世代のセルの二次元配列を返す
        /// 引数が与えられない場合の処理
        /// </summary>
        /// <returns>ｎ＋１世代のセルの状態</returns>
        public bool[,] GetNewGen()
        {
            bool[,] bl = new bool[_rowSize, _colSize];
            for (int i = 0; i < _rowSize; i++)
            {
                for (int j = 0; j < _colSize; j++)
                {
                    bl[i, j] = DeadOrAlive(i, j);
                }
            }
            return bl;
        }
        /// <summary>
        /// bool[,]で与えられたデータを表示するメソッド
        /// スタティックメソッドとして使う
        /// </summary>
        /// <param name="val"></param>
        public static void ViewStat(bool[,] val)
        {
            int rowLength = val.GetLength(0);
            int colLength = val.GetLength(1);
            for (int i = 0; i < rowLength; i++)
            {
                for (int j = 0; j < colLength; j++)
                {
                    Console.Write((val[i, j] ? "■" : "□"));
                }
                Console.WriteLine();
            }
        }
        /// <summary>
        /// Statの中身を表示するためのメソッド
        /// 上のスタティックメソッドをオーバーロードしている
        /// </summary>
        public void ViewStat()
        {
            for (int i = 0; i < _rowSize; i++)
            {
                for (int j = 0; j < _colSize; j++)
                {
                    Console.Write((Stat[i, j] ? "■" : "□"));
                }
                Console.WriteLine();
            }
        }
    }
~~~

##`int[,]`を`bool[,]`に変換するクラス
セルの状態を保持する二次元配列は生は`true`、死は`false`で格納しているのですが、初期値を設定するときにいちいちtrue/falseをたくさん書くのは面倒なので、初期値を0/1で設定してこれをtrue/falseに変換するスタティックメソッドを作りました。
ここでも三項演算子が使われています。

~~~CSharp
    class IntToBool
    {
        public static bool[,] Conv(int[,] val)   // int[,]を受け取り、bool[,]を返す
        {
           /*
            * int[,]で二次元配列のサイズを調べてbool[,]を初期化
            */
            int rowLength = val.GetLength(0);
            int colLength = val.GetLength(1);
            bool[,] bl = new bool[rowLength,colLength];

            for (int i = 0; i < rowLength; i++)
            {
                for (int j = 0; j < colLength; j++)
                {
                    if (val[i, j] == 0 || val[i, j] == 1)
                    {
                        bl[i, j] = (val[i, j] == 1 ? true : false);
                    }
                    else
                    {
                        throw new ArgumentException();  // 0/1以外が入っていたら例外を発生させる
                    }
                }
            }
            return bl;
        }
    }
~~~

##初期値を設定して入出力を制御
Main()部分になります。
初期値をハードコードして、単純にConsoleに表示するものになります。
無限ループになっているので、終了するときは`Ctrl + C`で止めます。

~~~CSharp
    class Program
    {
        static void Main(string[] args)
        {
           /*
            * 初期値を設定する
            */
            var row = 10;        // セルの行数
            var col = 10;        // セルの列数
            var gen = 0;         // 世代数
            var init = new int[row, col]
            {
                {0,0,0,0,0,0,0,0,0,0 },
                {0,0,0,0,0,0,0,1,1,1 },
                {0,0,1,0,0,0,0,0,0,0 },
                {0,1,0,0,0,0,0,0,0,0 },
                {0,1,1,1,0,0,0,0,0,0 },
                {0,0,0,0,0,0,0,0,0,0 },
                {0,0,1,1,0,1,1,0,0,0 },
                {0,0,0,1,0,1,1,0,0,0 },
                {1,1,1,0,0,0,0,1,1,0 },
                {1,0,0,0,0,0,0,1,1,0 }
            };                  // セルの初期値　0:死　1:生

            var s = new LifeGame(row, col);
            s.Stat = IntToBool.Conv(init);     // プロパティのStatにbool[,]を入れる　内部的にはboolで扱うのでintをboolに変換する
            
            Console.ReadKey();
            /*
             * １．画面をクリアして
             * ２．「ｎ世代目」を表示
             * ３．現在のStatの状況をコンソールに表示
             * ４．1秒間(1000ms)待機
             * ５．世代を１つ進めてStatを洗い替える
             * ６．世代カウンタを１つ進める
             */
            while (true)
            {
                Console.Clear();
                Console.WriteLine($"{gen}世代目");
                s.ViewStat();
                System.Threading.Thread.Sleep(1000);
                s.Stat = s.GetNewGen();
                gen++;
            }
        }
    }
~~~

##おわりに
このプログラムを書くことによって、以下のテクニックを学ぶことができました。

- 制御構造（for文、while文、if文）
- 三項演算子
- 二次元配列
オブジェクト指向にフォーカスすると、以下のテクニックが使われています。
- クラス
- フィールド
- プロパティ
- コンストラクタ
- メソッド
- スタティックメソッド
- メソッドのオーバーロード

今後の課題としては、Formプログラミングに発展させたりできたらなと思っています。