こんにちは、きうちです。
今回は「ライフゲーム」を作ってみたいと思います。
ライフゲームについての詳細は 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世代進むと元の形に戻る感じ。
★ ★ ★
といったところで、今回はここまで。
次回は、もう少し複雑なパターンをやってみたいと思います。
では!
タグ: lifegame, C#, programming, hobby