LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-29

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

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

ライフゲームとは

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

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

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

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

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

ライフゲーム by Wikipedia

WPFで作るにあたって

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

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にリファクタリングしていく予定です。

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2