ライフゲームを作ってみる(前編)

こんにちは、きうちです。

今回は「ライフゲーム」を作ってみたいと思います。

ライフゲームについての詳細は Wikipediaの説明 などをご覧いただければと思いますが、ここで簡単に説明すると

  • 碁盤のようなフィールド
  • 各マス目を「セル」と呼称
  • セルの色の有無(ON/OFF)がセルの生死を表現
  • 時間の経過の概念あり
  • 時間の経過とともに、ルールにのっとってセルがONになったりOFFになったりする

というものです。

初期配置(ONのセルの配置具合)によって、さまざまな動きがみられて面白いです。
1970年頃から存在するもので、Webを探せばサンプルプログラムとかいろいろ出てくると思いますし、
あるいはChatGPTなどの生成AIに作ってもらうこともできると思いますが…

ここではあえて自作してみます。

言語は例によってC#にします。
Windowsフォームアプリとして作成します。

フォームのメンバ変数として、フィールドのサイズを定義する定数 WIDTH,HEIGHT を定義し、サイズをそれぞれ50にします。
また、フィールドの内容を保持するための変数 field を int 型の2次元配列として定義します。

またフォームのサイズは 640×480 にして、ほどよい位置にサイズ400×400のPictureBoxを配置します。PictureBoxの背景は黒にします。

フォームのコンストラクタで field変数の内容をクリア(全要素0にする)した上でいくつかのセルの要素値を1にします。
上記処理は別メソッド(ClearField, PresetField)にしておきます。

PictureBoxにPaintイベントを設定します。
内容は、fieldの各要素のインデックスに対応する位置に、要素値が1の場合は白い四角を描く、というものにします。
インデックスについては行がy座標に対応し、列がx座標に対応するものとします。
1セル8×8ピクセルで描くものとします。

public partial class Form1 : Form
{
    private const int WIDTH = 50;
    private const int HEIGHT = 50;

    private int[,] field = new int[HEIGHT, WIDTH];

    public Form1()
    {
        InitializeComponent();

        ClearField(field);

        PresetField(field);

        pictureBox1.Refresh();
    }

    private void ClearField(int[,] f)
    {
        for (int i = 0; i < HEIGHT; i++)
        {
            for (int j = 0; j < WIDTH; j++)
            {
                f[i, j] = 0;
            }
        }
    }

    private void PresetField(int[,] f)
    {
        f[5, 9] = 1;
        f[6, 9] = 1;
        f[5, 10] = 1;
        f[6, 10] = 1;
    }

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        e.Graphics.Clear(Color.Black);

        for (int i = 0; i < HEIGHT; i++)
        {
            for (int j = 0; j < WIDTH; j++)
            {
                if (field[i, j] == 1)
                {
                    e.Graphics.FillRectangle(Brushes.White, j * 8, i * 8, 8, 8);
                }
            }
        }
    }
}

これで実行してみると、図のように表示されるはずです。

ここで、タイマーを1つ追加します。名前はtimNextGenerationとし、Intervalを1000(=1秒)、Enabledをtrueに(=起動と同時に発動するように)しておいて、Tickイベントで処理メソッドを呼ぶようにします。

    private void timNextGeneration_Tick(object sender, EventArgs e)
    {
        DoNextGeneration();
    }

    private void DoNextGeneration()
    {
        int[,] field2 = new int[HEIGHT, WIDTH];
        ClearField(field2);

        for (int i = 0; i < HEIGHT; i++)
        {
            for (int j = 0; j < WIDTH; j++)
            {
                int count = CountNeighbor(i, j);
                if (field[i, j] == 0 && count == 3
                    || field[i, j] == 1 && count >= 2 && count <= 3) field2[i, j] = 1;
                else field2[i, j] = 0;
            }
        }

        for (int i = 0; i < HEIGHT; i++)
        {
            for (int j = 0; j < WIDTH; j++)
            {
                field[i, j] = field2[i, j];
            }
        }

        pictureBox1.Refresh();
    }

    private int CountNeighbor(int i, int j)
    {
        int count = 0;

        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                if (x == 0 && y == 0) continue;
                int x1 = j + x;
                int y1 = i + y;
                if (y1 >= 0 && y1 < HEIGHT && x1 >= 0 && x1 < WIDTH && field[y1, x1] > 0) count++;
            }
        }

        return count;
    }

なんかメソッド名が微妙だなー…次世代「する」て…
でも GenerateNextGeneration だとなんかくどいし、まあ、いいことにします(笑)

DoNextGeneration でやっていることは、現世代の各要素について現在の生否、そして周辺の生きているセルの数に応じ、次世代の生否を決めるというものです。以下(Wikipediaより引用)のルールに従って判定しています。

誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

周辺のセルを数えるためのメソッドとして、CountNeigborを定義しています。なお、数える周辺セルの座標がマイナスまたは最大値(49)をオーバーしている場合はそのセルは生きていないものとして扱います。

次世代データができたらそれをメンバ変数の field に格納し、PictureBoxをRefresh(再描画)させます。

この状態で実行してみると…ほら、セルが動き出しました!

…白いセルがだんだん少なくなっていって最後に消えましたね。

ちょっとさみしいので…
今度は、いろいろなパターンを置いてみましょう。
PresetField メソッドを次のように変えます。

private void PresetField(int[,] f)
{
    // === 固定物体 ===

    // ブロック
    f[5, 9] = 1;
    f[6, 9] = 1;
    f[5, 10] = 1;
    f[6, 10] = 1;

    // 蜂の巣
    f[5, 14] = 1;
    f[4, 15] = 1;
    f[4, 16] = 1;
    f[6, 15] = 1;
    f[6, 16] = 1;
    f[5, 17] = 1;

    // ボート
    f[5, 21] = 1;
    f[5, 22] = 1;
    f[6, 21] = 1;
    f[7, 22] = 1;
    f[6, 23] = 1;

    // 船
    f[5, 27] = 1;
    f[5, 28] = 1;
    f[6, 28] = 1;
    f[7, 27] = 1;
    f[7, 26] = 1;
    f[6, 26] = 1;

    // 池
    f[5, 32] = 1;
    f[5, 33] = 1;
    f[6, 34] = 1;
    f[7, 34] = 1;
    f[8, 33] = 1;
    f[8, 32] = 1;
    f[7, 31] = 1;
    f[6, 31] = 1;

    // === 振動子 ===

    // ブリンカー
    f[10, 10] = 1;
    f[11, 10] = 1;
    f[12, 10] = 1;

    // ヒキガエル
    f[10, 15] = 1;
    f[11, 15] = 1;
    f[12, 15] = 1;
    f[11, 16] = 1;
    f[12, 16] = 1;
    f[13, 16] = 1;

    // ビーコン
    f[10, 20] = 1;
    f[10, 21] = 1;
    f[11, 20] = 1;
    f[11, 21] = 1;
    f[12, 22] = 1;
    f[12, 23] = 1;
    f[13, 22] = 1;
    f[13, 23] = 1;

    // 時計
    f[11, 27] = 1;
    f[11, 28] = 1;
    f[10, 29] = 1;
    f[13, 28] = 1;
    f[12, 29] = 1;
    f[12, 30] = 1;
}

「固定物体」5種と「振動子」4種を置くようにしてみました。実行すると、こんな感じ。

固定物体は初期状態からずっと変化がないです。
振動子は2世代進むと元の形に戻る感じ。

★ ★ ★

といったところで、今回はここまで。
次回は、もう少し複雑なパターンをやってみたいと思います。

では!

タグ: , , ,