自分の勉強の為と興味本位でC#(WPF)でライフゲームを作成してみます。
なぜC#で、かつWPFで作るのかと問われると特に意味はありません。何かの縛りプレイだと思っていただくほかないです。
最初はMVVMを守って作ろうかと思ってましたが、DataGridのバインディングがクセ強かったのでとりあえず全部コードビハインドに書いてまずは動かしていきます。
ライフゲームとは
セル・オートマトンの一種
以下のルールに従って、生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム
-
誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
-
生存:生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
-
過疎:生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
-
過密:生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
WPFで作るにあたって
使用するコントロールはDataGridで、各セルを黒(生存)と白(死滅)で塗り分けていきます。
<Window x:Class="WpfTestView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" ResizeMode="NoResize"
Title="MainWindow" Height="650" Width="600" >
<Grid Name="parentGridSample">
<StackPanel>
<Grid Name="gridSample">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Refresh" Click="RefreshButton_Click" />
<Button Grid.Column="1" Content="Random" Click="RandomButton_Click"/>
<Button Grid.Column="2" Content="Start" Click="StartButton_Click"/>
</Grid>
<DataGrid Name="dgSample" HeadersVisibility="None" IsReadOnly="True"
SelectionMode="Single" SelectionUnit="Cell"
MouseUp="dgSample_MouseUp" SizeChanged="dgSample_SizeChanged">
<DataGrid.Columns>
<DataGridTextColumn>
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="White"/>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</Window>
全てイベントドリブンとして実装、コードビハインドでコントロールを操作しているので全く持ってWinFormsと違いありません。
これが上手く動けばMVVMにリファクタリングしていく予定です。
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfTestView
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainView : Window
{
// 内部的なデータテーブル
private DataTable _dataTable = new DataTable();
// 次元
private readonly int _rank = 50;
// 前世代
private Dictionary<int, List<bool>> preGeneration = new Dictionary<int, List<bool>>();
// 現世代
private Dictionary<int, List<bool>> curGeneration = new Dictionary<int, List<bool>>();
public MainView()
{
InitializeComponent();
InitializeDataGrid();
InitializeGeneration();
}
private void InitializeDataGrid()
{
dgSample.Columns.Clear();
_dataTable.Rows.Clear();
_dataTable.Columns.Clear();
for (int i = 0; i < _rank; i++)
{
_dataTable.Columns.Add();
_dataTable.Rows.Add();
}
dgSample.ItemsSource = new DataView(_dataTable);
ResizeDataGrid();
}
private void InitializeGeneration()
{
preGeneration.Clear();
curGeneration.Clear();
preGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList());
curGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList());
}
private void ResizeDataGrid()
{
if (_dataTable.Columns.Count == 0) return;
double parentHeghit = parentGridSample.ActualHeight;
double parentWidth = parentGridSample.ActualWidth;
double buttonHeight = gridSample.ActualHeight;
// ボタンを画面上部に付けているのでその分を全体から引いて、行数で均等に割る
dgSample.MinRowHeight = 0;
dgSample.RowHeight = (parentHeghit - buttonHeight) / _dataTable.Rows.Count;
// 何故か親ウィンドウの幅をそのまま割ると少しズレるので-2というマジックナンバーをつけている
foreach (var col in dgSample.Columns)
{
col.MinWidth = 0;
col.Width = (parentWidth - 2) / _dataTable.Columns.Count;
}
}
private void dgSample_SizeChanged(object sender, SizeChangedEventArgs e)
{
Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
ResizeDataGrid();
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
InitializeDataGrid();
InitializeGeneration();
}
private void dgSample_MouseUp(object sender, MouseButtonEventArgs e)
{
Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
// マウスクリックされたセルを取得して色を反転させる
var curCellInfo = dgSample.CurrentCell;
var curCell = curCellInfo.Column.GetCellContent(curCellInfo.Item).Parent as DataGridCell;
if (curCell == null) return;
int rowIdx = dgSample.Items.IndexOf(curCellInfo.Item);
int colIdx = curCellInfo.Column.DisplayIndex;
curGeneration[rowIdx][colIdx] = !curGeneration[rowIdx][colIdx];
ToggleCellColor(curCell);
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
// とりあえず100世代ライフゲームを実行する
int gen = 0;
Task.Factory.StartNew(() =>
{
do
{
UpdateGeneration();
System.Threading.Thread.Sleep(200);
if (curGeneration.All(_n => _n.Value.All(_ => !_))) break;
} while (++gen < 100);
});
}
private void UpdateGeneration()
{
for (int i = 0; i < _rank; i++)
{
for (int j = 0; j < _rank; j++)
{
// データのコピー
preGeneration[i][j] = curGeneration[i][j];
}
}
for (int i = 0; i < _rank; i++)
{
for (int j = 0; j < _rank; j++)
{
int above = i == 0 ? _rank - 1 : i - 1;
int below = i == _rank - 1 ? 0 : i + 1;
int left = j == 0 ? _rank - 1 : j - 1;
int right = j == _rank - 1 ? 0 : j + 1;
int aliveCell = 0;
if (preGeneration[above][left]) aliveCell++;
if (preGeneration[above][j]) aliveCell++;
if (preGeneration[above][right]) aliveCell++;
if (preGeneration[i][left]) aliveCell++;
if (preGeneration[i][right]) aliveCell++;
if (preGeneration[below][left]) aliveCell++;
if (preGeneration[below][j]) aliveCell++;
if (preGeneration[below][right]) aliveCell++;
if (!preGeneration[i][j])
{
if (aliveCell == 3)
{
// 誕生
curGeneration[i][j] = true;
}
}
else if (aliveCell <= 1)
{
// 過疎
curGeneration[i][j] = false;
}
else if (aliveCell <= 3)
{
// 生存
curGeneration[i][j] = true;
}
else if (aliveCell >= 4)
{
// 過密
curGeneration[i][j] = false;
}
}
}
for (int i = 0; i < _rank; i++)
{
for (int j = 0; j < _rank; j++)
{
// 前世と現世で状態が変われば反転させる
if (preGeneration[i][j] != curGeneration[i][j])
{
if (Application.Current.Dispatcher.CheckAccess())
{
ToggleCellColor(GetCell(i, j));
}
else
{
Application.Current.Dispatcher.Invoke(() =>
{
ToggleCellColor(GetCell(i, j));
});
}
}
}
}
}
private void RandomButton_Click(object sender, RoutedEventArgs e)
{
Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
try
{
InitializeGeneration();
// 全体の1/3のセルを生存状態にする
int totalCellCount = _rank * _rank;
var randomList = GetRandomRange(0, totalCellCount, totalCellCount / 3);
for (int i = 0; i < _rank; i++)
{
for (int j = 0; j < _rank; j++)
{
DataGridCell cell = GetCell(i, j);
SolidColorBrush brush = cell.Background as SolidColorBrush;
if (randomList.Any(_r => _r / _rank == i && _r % _rank == j))
{
// 黒
curGeneration[i][j] = true;
if (brush != Brushes.Black) cell.Background = Brushes.Black;
}
else
{
// 白
curGeneration[i][j] = false;
if (brush == Brushes.Black) cell.Background = Brushes.White;
}
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "Exception Occurred !!");
}
}
private List<int> GetRandomRange(int min, int max, int count)
{
if (min > max || count <= 0) return null;
Random random = new Random(DateTime.Now.Millisecond);
List<int> list = new List<int>();
while (list.Count < count)
{
int r = random.Next(min, max);
if (list.Contains(r)) continue;
list.Add(r);
}
return list;
}
private DataGridCell GetCell(int i, int j)
{
DataGridRow row = dgSample.ItemContainerGenerator.ContainerFromIndex(i) as DataGridRow;
DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row);
return presenter.ItemContainerGenerator.ContainerFromIndex(j) as DataGridCell;
}
private T GetVisualChild<T> (Visual parent) where T : Visual
{
T child = default(T);
int visCt = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < visCt; i++)
{
Visual v = VisualTreeHelper.GetChild(parent, i) as Visual;
child = v as T;
if (child == null) child = GetVisualChild<T>(v);
if (child != null) break;
}
return child;
}
private void ToggleCellColor(DataGridCell cell)
{
SolidColorBrush curCellColor = cell.Background as SolidColorBrush;
if (curCellColor == Brushes.Black)
{
cell.Background = Brushes.White;
}
else
{
cell.Background = Brushes.Black;
}
}
}
}
ちなみに途中記載している行数・列数からセルを取得するメソッドについては、簡便化の為エラー処理は一切していません。
もし実際に書く場合にはもう少し丁寧に書いた方がいいです。
今回はもう開き直って全部xaml.csに書きましたが、性分的にはxaml.csには1行も書きたくない派なので
次回、MVVMパターンに則って書き直しを行っていきます。