こんにちは、きうちです。
今回は「ライフゲーム」を作ってみたいと思います。
ライフゲームについての詳細は 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