[WPF]ライフゲームを作ってみるver.1


自分の勉強の為と興味本位でC#(WPF)でライフゲームを作成してみます。

なぜC#で、かつWPFで作るのかと問われると特に意味はありません。何かの縛りプレイだと思っていただくほかないです。
最初はMVVMを守って作ろうかと思ってましたが、DataGridのバインディングがクセ強かったのでとりあえず全部コードビハインドに書いてまずは動かしていきます。

## ライフゲームとは

セル・オートマトンの一種
　以下のルールに従って、生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム

* 誕生：死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。

* 生存：生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。

* 過疎：生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。

* 過密：生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

[ライフゲーム by Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)

## WPFで作るにあたって

使用するコントロールはDataGridで、各セルを黒（生存）と白（死滅）で塗り分けていきます。

```C#:MainWindow.xaml
<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にリファクタリングしていく予定です。

```C#:MainWindow.xaml.cs
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;
            }
        }
    }
}

```

ちなみに途中記載している行数・列数からセルを取得するメソッドについては、簡便化の為エラー処理は一切していません。
もし実際に書く場合にはもう少し丁寧に書いた方がいいです。

上記コードで実際に動かしてみたのがこちら
![LaifeGame1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/782968/edcc2d5c-8a2f-d34b-1a8f-27f323bb4aff.gif)

今回はもう開き直って全部xaml.csに書きましたが、性分的にはxaml.csには1行も書きたくない派なので
次回、MVVMパターンに則って書き直しを行っていきます。