C#のフォームアプリでライフゲーム作成

Youtubeで解説したC#で作成したライフゲームのソースコード。
下記URLで長々と解説していますが、結局はこのコードを最終的に書きました。
遊んでくれると嬉しいかも。
１回目：ライフゲーム作成するための考え方とか試行錯誤のお話だったはず。
<iframe width="560" height="315" src="https://www.youtube.com/embed/Ye_EnRkFqN4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

２回目：クラス設計～完成までと、最後は小話。
<iframe width="560" height="315" src="https://www.youtube.com/embed/1Ay7m3FTKpk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

```c#:LifeGame.cs
using System;
using System.Text;
using System.Drawing;
using System.Windows.Forms;

//クラスはFormを継承して作成
class LifeGame : Form
{
	private Button startBtn, resetBtn, randomBtn;
	private static Timer timer = new Timer();

    //ここで作成したMapクラスのオブジェクトを作成(タイマー操作の記述中でも使用したいため)
	private Map mp = new Map(40, 40);
	private Label lb;
	private int dotSize=20;

	public static void Main()
	{
		Application.Run(new LifeGame());
	}

	public LifeGame()
	{
		this.Text = "LifeGame";
		//ウィンドウサイズの指定(px)
		this.Width = 1200; this.Height = 1000;

		//タイマー発動間隔の指定(下記では100ms毎にタイマー発動)
		timer.Interval = 100;
		timer.Tick += new EventHandler(timer_Tick);

		lb = new Label();
        //テキストでグラフィック表現するため、フォントやフォントサイズをあらかじめ定めておく
        //Font(フォント名, フォントサイズ, フォントサイズをピクセル単位で指定)
		lb.Font = new Font("MS UI Gothic", dotSize, GraphicsUnit.Pixel);
		lb.Location = new Point(0,0);
		lb.Size = new Size(this.Width, this.Height - 100);

		lb.Text = mp.textMap();
		lb.Parent = this;
		
		lb.MouseClick += Form_MouseClick;

        //Startボタン
		startBtn = new Button();
		startBtn.Text = "▶";
		startBtn.Location = new Point(0,this.Height - 100);
		this.Controls.Add(startBtn);
		startBtn.Click += new EventHandler(startBtn_Click);

        //Resetボタン
		resetBtn = new Button();
		resetBtn.Text = "Reset";
		resetBtn.Location = new Point(startBtn.Location.X+startBtn.Width+10,this.Height - 100);
		this.Controls.Add(resetBtn);
		resetBtn.Click += new EventHandler(resetBtn_Click);

        //ランダム入力ボタン
		randomBtn = new Button();
		randomBtn.Text = "Random";
		randomBtn.Location = new Point(resetBtn.Location.X+resetBtn.Width+10,this.Height - 100);
		this.Controls.Add(randomBtn);
		randomBtn.Click += new EventHandler(randomBtn_Click);
	}

	private void Form_MouseClick(Object sender, MouseEventArgs e)
	{
		int pointX, pointY;
		if(e.Button==MouseButtons.Left)
		{
			pointX = (int)(e.X - 0.2*dotSize) / dotSize;
			pointY = (int)(e.Y - 0.025*dotSize) / dotSize;

			mp.dotReverse(pointX, pointY);
			lb.Text = mp.textMap();
			lb.Parent = this;
		}
	}

	private void startBtn_Click(Object sender, EventArgs e)
	{
		if(startBtn.Text == "▶")
		{
			startBtn.Text = "■";
			timer.Enabled = true;
		}
		else
		{
			startBtn.Text = "▶";
			timer.Enabled = false;
		}
	}

	private void resetBtn_Click(Object sender, EventArgs e)
	{
		mp.Reset();
		lb.Text = mp.textMap();
		lb.Parent = this;
	}

	private void randomBtn_Click(Object sender, EventArgs e)
	{
		mp.randomInput();
		lb.Text = mp.textMap();
		lb.Parent = this;
	}

	private void timer_Tick(Object sender, EventArgs e)
	{
		nextStep();
		lb.Text = mp.textMap();
		lb.Parent = this;
	}

    //ライフゲームのルールから1世代後のマップ情報計算
	private void nextStep()
	{
		int[,] nowStatus = new int[mp.Width,mp.Height];
		int[,] nextStatus = new int[mp.Width,mp.Height];
		int cellSum;

        //現在情報取得
		for(int j=0; j<mp.Height; j++)
		{
			for(int i=0; i<mp.Width; i++)
			{
				nowStatus[i,j] = mp.dotStatus(i, j);
			}
		}

        //ライフゲームのルールから次世代生死ドットの計算
		for(int j=0; j<mp.Height; j++)
		{
			for(int i=0; i<mp.Width; i++)
			{
				cellSum = 0;
				//マップの外周部は無視
				if(i>0 && j>0 && i <mp.Width-1 && j <mp.Height-1)
				{
					for(int aj=j-1; aj<j+2; aj++)
					  for(int ai=i-1; ai<i+2; ai++)
					    cellSum += nowStatus[ai, aj];
					cellSum -= nowStatus[i, j];
				}
				if(nowStatus[i,j]==0 && cellSum==3) nextStatus[i, j] = 1;
				else if(nowStatus[i,j]==1)
				{
					if(cellSum==2 || cellSum==3) nextStatus[i, j] = 1;
					if(cellSum <2 || 3< cellSum) nextStatus[i, j] = 0;
				}
			}
		}

        //現在マップ情報と次世代マップ情報で差分あればドット反転
		for(int j=0; j<mp.Height; j++)
		{
			for(int i=0; i<mp.Width; i++)
			{
				if((nowStatus[i,j]-nextStatus[i,j])!=0) mp.dotReverse(i, j);
			}
		}
	}
}

class Map
{
	private string strT="■", strF="□", strN="\n";

	public string text;
	public string mapString;
	public StringBuilder sbmap;
	public int Width, Height;
	public int maxLength;

    //オブジェクト生成時に縦横のドット数からテキストマップ情報をstringbuilderに格納する
	public Map(int m, int n)
	{
		Width = m;
		Height = n;
		maxLength = m * n - 1;

		text = "";
		for(int i=0; i<n; i++)
		{
			for(int j=0; j<m; j++)
			{
				text += strF;
			}
			text += strN;
		}

		mapString = text;
		sbmap = new StringBuilder(mapString);
	}

    //指定したドット位置(x方向, y方向)を反転させるメソッド
	public void dotReverse(int m, int n)
	{
        //エラー処理
		if(m>Width-1 || n>Height-1) return;
		if(0>m || 0>n) return;

		int changePoint = (Width+1) * n + m;

		if(sbmap.ToString()[changePoint] == strF[0])
		{
			sbmap.Insert(changePoint, strT, 1);
		}
		else
		{
			sbmap.Insert(changePoint, strF, 1);	
		}
		sbmap.Remove(changePoint+1, 1);
	}

    //指定されたドット情報(■か□)を判定し1or0で返す
	public int dotStatus(int m, int n)
	{
        //エラー処理
		if(m>Width-1 || n>Height-1) return 0;
		if(0>m || 0>n) return 0;

		int getPoint = (Width+1) * n + m;

		if(sbmap.ToString()[getPoint] == strT[0]) return 1;
		else return 0;
	}

    //テキストのマップ情報をリセット
	public void Reset()
	{
		text = "";
		for(int i=0; i<Height; i++)
		{
			for(int j=0; j<Width; j++)
			{
				text += strF;
			}
			text += strN;
		}

		mapString = text;
		sbmap = new StringBuilder(mapString);
	}

    //マップの情報をランダムに入力するメソッド
	public void randomInput()
	{
		Random r = new Random();
		int randomN;

		text = "";
		for(int i=0; i<Height; i++)
		{
			for(int j=0; j<Width; j++)
			{
				randomN = (int)r.Next(0,2);

				if(randomN == 0) text += strF;
				else text += strT;
			}
			text += strN;
		}

		mapString = text;
		sbmap = new StringBuilder(mapString);
	}

    //操作は全てstringbuilderで行ったため、string型を返すメソッドも用意しておく
    //他のメソッドで全て返り値stringを返すようにしてしまってもいいかもしれない。。。
	public string textMap()
	{
		return sbmap.ToString();
	}
}
```