いまさらC#でライフゲームを書いてみた
どう書く?orgにて、C#で既に書いてらっしゃる方が多数おられますが、
どうもコードを短くすることに重点を置いて書いているような感じで、正直なんだか読みにくいなーと思い・・・。
冗長だけどわかりやすく、オブジェクト指向っぽくライフゲームを書いてみることにしました*1。
※ライフゲームについての説明は、ライフゲーム - Wikipediaを参照ください。
Cellクラス
まず、細胞ひとつひとつを表すCellクラスを作ることにします。IsAliveによって、細胞が生きているかどうかがわかるようにします。
using System; namespace LifeGameLibrary { /// <summary> /// 細胞クラス /// </summary> [Serializable] public class Cell { /// <summary> /// コンストラクタ /// </summary> public Cell() { this.Age = 0; } /// <summary> /// コンストラクタ /// </summary> public Cell(bool alive) { this.Age = 0; this.IsAlive = alive; } /// <summary> /// 生死 /// </summary> public bool IsAlive { get; set; } /// <summary> /// 細胞年齢 /// </summary> public long Age { get; set; } } }
CellMapクラス
続きまして、細胞ひとつひとつの座標位置を表すマップを作ります。まぁいろいろやってます、コードを読んでください。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LifeGameLibrary { /// <summary> /// 細胞マップ /// </summary> [Serializable] public class CellMap : ICloneable { private Cell[,] map; /// <summary> /// コンストラクタ /// </summary> /// <param name="width">マップの幅</param> /// <param name="height">マップの高さ</param> public CellMap(int width, int height) { // 世代を初期化 this.Generation = 0; this.map = new Cell[width, height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { this.map[x, y] = new Cell(); } } } /// <summary> /// コンストラクタ /// </summary> /// <param name="mapString">マップ文字列</param> public CellMap(IEnumerable<string> mapString,char alive) { var mapInfo = from map in mapString where map.Length == mapString.Max(x => x.Length) let count = mapString.Count() select new { column = mapString.Max(x => x.Length), row = count }; foreach (var t in mapInfo) { this.map = new Cell[t.column, t.row]; this.ColumnCount = t.column; this.RowCount = t.row; var r = -1; foreach (var row in mapString) { r++; //改行で分割して文字配列にしてセット var record = row.ToCharArray(); for (int x = 0; x < t.column; x++) { this.map[x, r] = new Cell(record[x] == alive); } } } } /// <summary> /// 列数 /// </summary> public int ColumnCount { get; set; } /// <summary> /// 行数 /// </summary> public int RowCount { get; set; } /// <summary> /// 指定した座標位置の細胞 /// </summary> /// <param name="x">x座標</param> /// <param name="y">y座標</param> /// <returns>細胞</returns> public Cell this[int x, int y] { get { return this.map[x, y]; } set { this.map[x, y] = value; } } /// <summary> /// マップの幅 /// </summary> public int Width { get { return this.map.GetLength(0); } } /// <summary> /// マップの高さ /// </summary> public int Height { get { return this.map.GetLength(1); } } /// <summary> /// マップの世代 /// </summary> public long Generation { get; set; } /// <summary> /// 細胞マップを文字列で取得します。 /// </summary> /// <param name="alive">生存を表す文字</param> /// <param name="dead">死亡を表す文字</param> /// <returns></returns> public string GetCellMapString(char alive,char dead) { var sb = new StringBuilder(); for (int y = 0; y < this.RowCount; y++) { for (int x = 0; x < this.ColumnCount; x++){ sb.Append(this.map[x, y].IsAlive ? alive : dead ); } sb.Append((this.RowCount - 1 == y) ? "" : "\r\n"); } return sb.ToString(); } /// <summary> /// ディープコピークローンを取得します。 /// </summary> /// <returns></returns> public CellMap DeepCopyClone() { return (CellMap)this.Clone(); } #region ICloneable メンバ public object Clone() { return Serialization.DeepCopyClone<CellMap>(this); } #endregion } }
ILivingConditionインターフェイス
続きまして、細胞の生存条件を表すインターフェイスを定義します。using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LifeGameLibrary { /// <summary> /// 生存条件インターフェイス /// </summary> public interface ILivingCondition { /// <summary> /// 細胞マップの各細胞について、次世代の生存確認を行います。 /// </summary> /// <param name="cellMap"></param> void ConfirmationAlive(CellMap cellMap); /// <summary> /// 隣接する細胞たちを取得します。 /// </summary> /// <param name="cellMap">細胞マップ</param> /// <param name="x">X座標</param> /// <param name="y">Y座標</param> /// <returns>隣接する細胞たち</returns> IEnumerable<Cell> GetNeighbors(CellMap cellMap, int x, int y); } }
AbstractLivingConditionクラス
先ほど作成したILivingConditionインターフェイスを実装した、生存条件抽象クラスを作成します。コードのとおりです。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LifeGameLibrary { /// <summary> /// 生存条件抽象クラス /// </summary> public abstract class AbstractLivingCondition : ILivingCondition { /// <summary> /// 対象の細胞のうち、生存している細胞の数を取得します。 /// </summary> /// <param name="cells">対象細胞</param> /// <returns>生きている細胞の数</returns> public virtual int GetNumberOfCellsToBeAlive(IEnumerable<Cell> cells) { int count = 0; foreach (Cell cell in cells) { if (cell.IsAlive) { count++; } } return count; } #region ILivingCondition メンバ /// <summary> /// 次の世代の生存確認を行います。 /// </summary> /// <param name="cellMap"></param> public abstract void ConfirmationAlive(CellMap cellMap); /// <summary> /// 隣接する細胞のイテレータを提供 /// </summary> /// <param name="cellMap">細胞マップ</param> /// <param name="x">x座標</param> /// <param name="y">y座標</param> /// <returns>隣接する細胞</returns> public virtual IEnumerable<Cell> GetNeighbors(CellMap cellMap, int x, int y) { int w = cellMap.Width; int h = cellMap.Height; foreach (int xn in new int[] { x - 1, x + 1, x }) { foreach (int yn in new int[] { y - 1, y + 1, y }) { if ((xn != x) || (yn != y)) { if ((xn >= 0) && (xn < w) && (yn >= 0) && (yn < h)) { yield return cellMap[xn, yn]; } } } } yield break; } #endregion } }
StandardLivingConditionクラス
生存条件抽象クラスから派生し、ライフゲームの標準的な生存条件クラスを作成します。ここで初めて、ConfirmationAliveメソッドが実装されています。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LifeGameLibrary { /// <summary> /// ライフゲームの標準的な生存条件 /// </summary> public class StandardLivingCondition : AbstractLivingCondition { private IEnumerable<int> NumberOfNeighborsToAlive{get;set;} private IEnumerable<int> NumberOfNeighborsToBeBorn{get;set;} /// <summary> /// 条件:23/3 /// </summary> public static readonly StandardLivingCondition Original = new StandardLivingCondition(new int[] { 2, 3 }, new int[] { 3 }); /// <summary> /// 条件:23/36 /// </summary> public static readonly StandardLivingCondition HighLife = new StandardLivingCondition(new int[] { 2, 3 }, new int[] { 3, 6 }); #region コンストラクタ /// <summary> /// コンストラクタ /// </summary> /// <param name="numberOfNeighborsToAlive"></param> /// <param name="numberOfNeighborsToBeBorn"></param> public StandardLivingCondition(IEnumerable<int> numberOfNeighborsToAlive, IEnumerable<int> numberOfNeighborsToBeBorn) { this.NumberOfNeighborsToAlive = from num in numberOfNeighborsToAlive orderby num select num; this.NumberOfNeighborsToBeBorn = from num in numberOfNeighborsToBeBorn orderby num select num; } #endregion /// <summary> /// 細胞マップの各細胞について、次世代の生存確認を行います。 /// </summary> /// <param name="cellMap">細胞マップ</param> public override void ConfirmationAlive(CellMap cellMap) { var oldCellMap = cellMap.DeepCopyClone(); for (int x = 0; x < cellMap.Width; x++) { for (int y = 0; y < cellMap.Height; y++) { var oldCell = oldCellMap[x, y]; var newCell = cellMap[x, y]; var neighbours = this.GetNeighbors(oldCellMap, x, y); int aliveNeighborsCount = this.GetNumberOfCellsToBeAlive(neighbours); if (oldCell.IsAlive) { newCell.IsAlive = Array.BinarySearch(this.NumberOfNeighborsToAlive.ToArray(), aliveNeighborsCount) >= 0; newCell.Age = newCell.IsAlive ? ++newCell.Age : 0; continue; } newCell.IsAlive = Array.BinarySearch(this.NumberOfNeighborsToBeBorn.ToArray(), aliveNeighborsCount) >= 0; } } } } }
LifeGameクラス
ライフゲームを管理するクラスです。マップ、生存条件、マップの描画などをつかさどります。
using System; using System.Collections.Generic; namespace LifeGameLibrary { /// <summary> /// ライフゲーム /// </summary> public class LifeGame { #region コンストラクタ /// <summary> /// コンストラクタ /// </summary> /// <param name="lc">生存条件</param> /// <param name="width">マップの幅</param> /// <param name="height">マップの高さ</param> /// <param name="drw">描画するやつ</param> public LifeGame(ILivingCondition lc, int width, int height, ICellMapDrower drw) : this(lc, new CellMap(width, height), drw) { } /// <summary> /// コンストラクタ /// </summary> /// <param name="lc">生存条件</param> /// <param name="cm">細胞マップ</param> /// <param name="drw">描画するやつ</param> public LifeGame(ILivingCondition lc, CellMap cm, ICellMapDrower drw) { this.livingCondition = lc; this.CellMap = cm; this.cellMapDrawer = drw; } #endregion #region プロパティ /// <summary> /// 細胞マップを取得します。 /// </summary> public CellMap CellMap { get; set; } /// <summary> /// 生存条件を取得します。 /// </summary> private ILivingCondition livingCondition; public ILivingCondition LivingCondition { get { return this.livingCondition; } } private ICellMapDrower cellMapDrawer; public ICellMapDrower CellMapDrawer { get { return this.cellMapDrawer; } } #endregion #region メソッド /// <summary> /// 世代イテレータを取得 /// </summary> /// <returns></returns> public IEnumerable<CellMap> GetCellMapGeneration() { while (true) { this.CellMap.Generation++; this.LivingCondition.ConfirmationAlive(this.CellMap); this.CellMapDrawer.Draw(this.CellMap); yield return this.CellMap; } } /// <summary> /// 細胞マップをランダム生成して設定します。 /// </summary> public void SetRamdomCellMap() { var rnd = new Random(); int w = this.CellMap.Width; int h = this.CellMap.Height; for(int x = 0; x < w; x++){ for(int y = 0; y < h; y++){ this.CellMap[x, y].IsAlive = (Math.Floor((double)rnd.Next(2)) == 1); } } } /// <summary> /// 細胞マップを描画します。 /// </summary> /// <returns></returns> public void Draw() { this.cellMapDrawer.Draw(this.CellMap); } #endregion } }
Serializationクラス
CellMapのクローンを作成するために、ジェネリクスなディープコピーのみが実装されています。このクラスに限った話ではありませんが、クラス名は適当なのでつっ込まないでください。
using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace LifeGameLibrary { public static class Serialization { /// <summary> /// 対象オブジェクトのディープコピークローンを取得します。 /// </summary> /// <typeparam name="T">対象オブジェクトの型</typeparam> /// <param name="target">コピー対象オブジェクトを指定します。</param> /// <returns>ディープコピーのクローンを返します。</returns> /// <remarks>このメソッでディープコピーするには、対象クラスがシリアライズ可能である必要があります。</remarks> public static T DeepCopyClone<T>(T target) { object clone = null; using (MemoryStream stream = new MemoryStream()) { //対象オブジェクトをシリアライズ BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, target); stream.Position = 0; //シリアライズデータをデシリアライズ clone = formatter.Deserialize(stream); } return (T)clone; } } }
ICellMapDrowerインターフェイス
細胞マップを描画するためのインターフェイス。こいつ見てて、クラス設計がなんか微妙な感じなことに気付きましたが、
もっかい考えるの面倒なのでこのままにしました。
namespace LifeGameLibrary { /// <summary> /// 細胞マップを描画するやつのインターフェイス /// </summary> public interface ICellMapDrower { void Draw(CellMap cellMap); } }
ConsoleDrawerクラス
こいつが細胞マップをコンソール出力してくれます。using System; namespace LifeGameLibrary { /// <summary> /// コンソール出力 /// </summary> public class ConsoleDrawer : ICellMapDrower { public void Draw(CellMap cellMap) { Console.Clear(); Console.WriteLine(cellMap.GetCellMapString('■','□')); Console.WriteLine("世代:{0}", cellMap.Generation); } } }
Templateクラス
ライフゲームで広く知られているパターンのテンプレを定義しています。Wikipediaからまんま持ってきました。これは別になくてもよいですね。
using System.Collections.Generic; namespace LifeGameLibrary.Pattern { /// <summary> /// ライフゲームのパターンテンプレ /// </summary> public static class Template { #region 固定型 /// <summary> /// ブロック /// </summary> /// <returns></returns> public static IEnumerable<string> GetBlock() { yield return "□□□□"; yield return "□■■□"; yield return "□■■□"; yield return "□□□□"; yield break; } /// <summary> /// 蜂の巣 /// </summary> /// <returns></returns> public static IEnumerable<string> GetBeehive() { yield return "□■□"; yield return "■□■"; yield return "■□■"; yield return "□■□"; yield break; } /// <summary> /// ボート /// </summary> /// <returns></returns> public static IEnumerable<string> GetBoat() { yield return "□■□"; yield return "■□■"; yield return "□■■"; yield break; } /// <summary> /// 船 /// </summary> /// <returns></returns> public static IEnumerable<string> GetShip() { yield return "■■□"; yield return "■□■"; yield return "□■■"; yield break; } /// <summary> /// 池 /// </summary> /// <returns></returns> public static IEnumerable<string> GetPool() { yield return "□■■□"; yield return "■□□■"; yield return "■□□■"; yield return "□■■□"; yield break; } #endregion #region 振動型 /// <summary> /// ブリンカー /// </summary> /// <returns></returns> public static IEnumerable<string> GetBlinker() { yield return "□■□"; yield return "□■□"; yield return "□■□"; yield break; } /// <summary> /// ペンタデカスロン /// </summary> /// <returns></returns> public static IEnumerable<string> GetPentadecathlon() { yield return "□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□"; yield return "□□□□□■□□□□■□□□□□"; yield return "□□□□■■□□□□■■□□□□"; yield return "□□□□□■□□□□■□□□□□"; yield return "□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□"; yield break; } /// <summary> /// ヒキガエル /// </summary> /// <returns></returns> public static IEnumerable<string> GetHikigaeru() { yield return "□□■□"; yield return "■□□■"; yield return "■□□■"; yield return "□■□□"; yield break; } /// <summary> /// ビーコン /// </summary> /// <returns></returns> public static IEnumerable<string> GetBeecon() { yield return "□□■■"; yield return "□□■■"; yield return "■■□□"; yield return "■■□□"; yield break; } /// <summary> /// 時計 /// </summary> /// <returns></returns> public static IEnumerable<string> GetClock() { yield return "□□□"; yield return "■□■□"; yield return "□■□■"; yield return "□■□□"; yield break; } #endregion #region 移動型 /// <summary> /// グライダー /// </summary> /// <returns></returns> public static IEnumerable<string> GetGlider() { yield return "□■□"; yield return "■□□"; yield return "■■■"; yield break; } /// <summary> /// 軽量級宇宙船 /// </summary> /// <returns></returns> public static IEnumerable<string> GetSmallSpaceship() { yield return "□■□□■"; yield return "■□□□□"; yield return "■□□□■"; yield return "■■■■□"; yield break; } /// <summary> /// 中量級宇宙船 /// </summary> /// <returns></returns> public static IEnumerable<string> GetMiddleSpaceship() { yield return "□□□■□□"; yield return "□■□□□■"; yield return "■□□□□□"; yield return "■□□□□■"; yield return "■■■■■□"; yield break; } /// <summary> /// 重量級宇宙船 /// </summary> /// <returns></returns> public static IEnumerable<string> GetLargeSpaceship() { yield return "□□□■■□□"; yield return "□■□□□□■"; yield return "■□□□□□□"; yield return "■□□□□□■"; yield return "■■■■■■□"; yield break; } /// <summary> /// グライダー・ガン /// </summary> /// <returns></returns> public static IEnumerable<string> GetGliderGun() { yield return "□□□□□□□□□□□□□□□□□□□□□□□□□■□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□■□■□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□■■□□□□□□■■□□□□□□□□□□□□■■□"; yield return "□□□□□□□□□□□□■□□□■□□□□■■□□□□□□□□□□□□■■□"; yield return "□■■□□□□□□□□■□□□□□■□□□■■□□□□□□□□□□□□□□□"; yield return "□■■□□□□□□□□■□□□■□■■□□□□■□■□□□□□□□□□□□□"; yield return "□□□□□□□□□□□■□□□□□■□□□□□□□■□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□■□□□■□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□■■□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield return "□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□"; yield break; } /// <summary> /// 汽車ぽっぽ /// </summary> /// <returns></returns> public static IEnumerable<string> GetKishaPoppo() { yield return "□□□■□"; yield return "□□□□■"; yield return "■□□□■"; yield return "□■■■■"; yield return "□□□□□"; yield return "□□□□□"; yield return "□□□□□"; yield return "■□□□□"; yield return "□■■□□"; yield return "□□■□□"; yield return "□□■□□"; yield return "□■□□□"; yield return "□□□□□"; yield return "□□□□□"; yield return "□□□■□"; yield return "□□□□■"; yield return "■□□□■"; yield return "□■■■■"; yield break; } #endregion #region 長寿型 /// <summary> /// ダイハード /// </summary> /// <returns></returns> public static IEnumerable<string> GetDieHard() { yield return "□□□□□□■□"; yield return "■■□□□□□□"; yield return "□■□□□■■■"; yield break; } /// <summary> /// ドングリ /// </summary> /// <returns></returns> public static IEnumerable<string> GetDongri() { yield return "□■□□□□□"; yield return "□□□■□□□"; yield return "■■□□■■■"; yield break; } #endregion } }
Programクラス
最後に、LifeGameLibraryを用いて、コンソールアプリケーションでライフゲームを実行します。パターンテンプレはペンタデカスロンを用いています。
using System; using LifeGameLibrary; using LifeGameLibrary.Pattern; namespace ConsoleApplication1 { class Program { static void Main() { //var lifeGame = new LifeGame(StandardLivingCondition.Original,3, 3, new ConsoleDrawer()); //lifeGame.SetRamdomCellMap(); var lifeGame = new LifeGame(StandardLivingCondition.Original, new CellMap(Template.GetPentadecathlon(), '■'), new ConsoleDrawer()); lifeGame.Draw(); var s = Console.ReadLine(); foreach (var map in lifeGame.GetCellMapGeneration()) { s = Console.ReadLine(); if (s == "exit") break; } } } }
以上です。そこそこオブジェクト指向していますね。初心者にもわかりやすいでしょう?
気が向いたら、いろいろ拡張したりGUIとか整備しようかな。
どうもお疲れさまでした(・ω・)
*1:雑でいい加減だったりもしますが