今日はブログで書くネタがなかったので、C#でライフゲームを実装してみました。
コードは末尾に記載します。
※コンソールのカーソルを非表示にし忘れていました。コードでは修正済みです。
ライフゲームとは1970年に考案された
古典的かつシンプルな人工生命シミュレーションプログラムです。
ライフゲーム – Wikipedia
格子状に配置したセルに、生存と死亡の2つの状態を割り当て、各セルについて、
「現在のセルの状態及び周囲8セルの状態から次の状態を求めて、状態を更新する(※)」
を繰り返すことで、生命の生と死のようなサイクルをシミュレーションします。
※状態遷移は以下
・死亡かつ周囲生存3セル→生存(”誕生”)
・生存かつ周囲生存1セル以下→死亡(”過疎”)
・生存かつ周囲生存2セルor3セル→生存(”生存”)
・生存かつ周囲生存4セル以上→死亡(”過密”)
基本的にはどこかで変化が生じなくなりシミュレーションが止まるのですが、
中には変化をし続ける初期セルのパターンもあり、
現在でも愛好家により新しいパターンが発見されているようです。
近年の進化したAIに比べると随分とシンプルですが、単調過ぎず見ていて面白いですよね。
(若い頃に知って感動した故にそう感じるのかもしれませんが。。。)
10年ほど前、学部3年生の頃にゼミの課題で実装したことがありましたが、
(そのゼミは4年に上がる前に教授が他所に移ってしまったため消滅しました。。。)
当時はプログラミングを講義で習って1年ちょっとの段階だったのもあり、
動作はしたもののそれは酷いコードを書いていた覚えがあります。
(言語はほぼCの機能しか使用していない、なんちゃってC++で、描画はOpenGLで行っていました)
当時に比べると洗練されたプログラムを書けるようになったと思います。
継続は力なりということにして締めさせていただきます。
以上、老いを感じる、思い出語りを含んだ記事更新でした。
// Program.cs
using LifeGame;
new Controller().Simulate(20, 0);
// Lifegame.cs
using System.Diagnostics;
using System.Text;
namespace LifeGame;
/// <summary> モデルクラス </summary>
internal class Model {
/// <summary> グリッド一辺サイズ </summary>
public int Size { get; private set; } = 0;
/// <summary> 現在の各セルの状態 </summary>
public bool[,] Cells { get; private set; } = new bool[0, 0];
/// <summary> 更新用のセル状態バッファ(兼以前の各セルの状態) </summary>
private bool[,] _cells { get; set; } = new bool[0, 0];
/// <summary> 初期化します。 </summary>
public void Init(int size, int seed) {
Size = size;
Cells = new bool[Size, Size];
_cells = new bool[Size, Size];
var rnd = new Random(seed);
for (var x = 0; x < Size; x++) {
for (var y = 0; y < Size; y++) {
// 乱数で初期状態設定
Cells[x, y] = rnd.Next(0, 100) % 2 == 1;
}
}
}
/// <summary> 終端の状態か。 </summary>
public bool IsTerminal() {
for (var x = 0; x < Size; x++) {
for (var y = 0; y < Size; y++) {
var diff = Cells[x, y] != _cells[x, y];
if (diff)
return false; // 差分あれば非終端
}
}
return true; // 差分なしで終端
}
/// <summary> 更新します。 </summary>
public void Update() {
for (var x = 0; x < Size; x++) {
for (var y = 0; y < Size; y++) {
_cells[x, y] = GetNextState(x, y); // 次状態取得
}
}
(Cells, _cells) = (_cells, Cells); // 更新した状態と現在の状態を交換
}
/// <summary> 次の状態を取得します。 </summary>
private bool GetNextState(int x, int y) {
var isAlive = Cells[x, y];
var countAroundAlive = CountAroundAliveCells(x, y);
return (isAlive, countAroundAlive) switch {
(false, 3) => true, // 誕生
(false, _) => false, // (死亡維持)
(true, <= 1) => false, // 過疎
(true, 2 or 3) => true, // 生存
(true, >= 4) => false, // 過密
};
}
/// <summary> グリッド走査用 x座標オフセット </summary>
private static int[] Dx { get; } = { -1, 0, 1, -1, 1, -1, 0, 1 };
/// <summary> グリッド走査用 y座標オフセット </summary>
private static int[] Dy { get; } = { -1, -1, -1, 0, 0, 1, 1, 1 };
/// <summary> 指定した中心セルの周囲の生存セルを数えます。 </summary>
private int CountAroundAliveCells(int cx, int cy) {
var count = 0;
foreach (var (dx, dy) in Dx.Zip(Dy)) {
// 中心座標+オフセットを正規化して座標算出
var (x, y) = (ValidateIndex(cx + dx), ValidateIndex(cy + dy));
if (Cells[x, y])
count++; // 生存ならカウントアップ
}
return count;
}
/// <summary> セルのインデックスを正規化します。端と端が繋がるように。 </summary>
private int ValidateIndex(int index) => index switch {
< 0 => Size - 1, // 左の外側なら右端に
_ when Size <= index => 0, // 右の外側なら左端に
_ => index // 範囲内はそのまま
};
}
/// <summary> ビュークラス </summary>
internal class View {
/// <summary> 初期化します。 </summary>
public void Init(Model model) {
Console.Clear();
Console.CursorVisible = false;
Console.ForegroundColor = ConsoleColor.Green;
Draw(model);
}
/// <summary> 終了します。 </summary>
public void Term() {
Console.ForegroundColor = ConsoleColor.Gray;
Console.CursorVisible = true;
Console.WriteLine("Done.");
}
/// <summary> 描画します。 </summary>
public void Draw(Model model) {
Console.SetCursorPosition(0, 0); // コンソール上書きのためカーソルを左上に
var sb = new StringBuilder();
for (var x = 0; x < model.Size; x++) {
for (var y = 0; y < model.Size; y++) {
var state = model.Cells[x, y] ? "■" : "_"; // 状態を文字化
sb.Append(state);
}
sb.AppendLine();
}
Console.Write(sb.ToString()); // 出力
}
}
/// <summary> 制御クラス </summary>
internal class Controller {
/// <summary> モデル </summary>
private Model Model { get; } = new();
/// <summary> ビュー </summary>
private View View { get; } = new();
/// <summary> シミュレーションを実行します。 </summary>
public void Simulate(int size, int seed) {
var sw = new Stopwatch();
Model.Init(size, seed);
View.Init(Model);
const int frameMs = 50; // 1フレーム50ms (20FPS)
while (!Model.IsTerminal()) {
// 更新所要時間を1フレーム時間から引いた上で待機
Thread.Sleep(Math.Max(1, frameMs - (int)sw.ElapsedMilliseconds));
sw.Restart();
Model.Update(); // モデル更新
View.Draw(Model); // 描画更新
}
View.Term();
}
}