WPF - MVVM 实现康威生命游戏。






2.92/5 (4投票s)
这是康威生命游戏问题的解决方案 - 尽情享用,玩得开心 ;)。死亡的细胞是红色,存活的细胞是绿色,空白细胞是白色。您可以根据需要修改颜色。
引言
生命游戏的游戏区域是二维的(网格)。它被划分为行和列,理想情况下是无限的。网格中的每个细胞可以处于三种可能的状态之一:存活、死亡或空白。空白细胞按死亡细胞处理。每个细胞都会响应其紧邻的邻居。起始网格随机填充了存活或空白的细胞。
背景
迭代步骤 – 生成下一代的规则,即世代 n -> 世代 n+1(下一代是通过同时将这些规则应用于每个细胞来计算的)
1. 任何存活的细胞,如果其存活的邻居少于两个,则会死亡,如同因人口过少引起一样。
2. 任何存活的细胞,如果其存活的邻居有两个或三个,则会继续存活到下一代。
3. 任何存活的细胞,如果其存活的邻居多于三个,则会死亡,如同因人口过剩引起一样。
4. 任何死亡或空白的细胞,如果其存活的邻居恰好有三个,则会变成存活的细胞,如同通过繁殖一样。
此程序执行此模拟:从第 0 代(初始状态)开始,并执行迭代步骤(世代 n -> 世代 n+1)
1. 我通过 app.config 文件提供了世代之间的(可配置的)延迟 200ms。
2. 网格大小也可以通过 app.config 文件进行配置。
3. 存活细胞的初始程度或百分比也是可配置的。
4. 在本例中,边界单元格会与另一侧的边界单元格发生反应,即,假设您将此网格折叠起来,使其相对边缘相互接触,那么边缘的单元格就会获得它们的邻居。
用户界面
我使用了一个 ItemsControl,并为其提供了 DataTemplate。此模板再次包含一个 ItemsControl,该 ItemsControl 在其 DataTemplate 中有一个 Label。
这些标签将显示网格的单元格,而这个 ItemsControl 将显示整个网格。
<Window x:Class="GameOfLife.GameOfLifeWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Game Of Life" Height="350" Width="525" xmlns:local="clr-namespace:GameOfLife"> <Window.Resources> <local:CharToColorConverter x:Key="CharToColorConverter"/> <DataTemplate x:Key="DataTemplateForLabel"> <Label Background="{Binding Mode=OneWay, Converter={StaticResource CharToColorConverter}}" Height="40" Width="50" Margin="4,4,4,4" /> </DataTemplate> <DataTemplate x:Key="DataTemplateForItemInItemsControl"> <ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplateForLabel}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </DataTemplate> </Window.Resources> <StackPanel Orientation="Vertical" > <ItemsControl x:Name="lst" ItemTemplate="{DynamicResource DataTemplateForItemInItemsControl}" ItemsSource="{Binding Lst ,Mode=OneWay}"/> <StackPanel Orientation="Horizontal"> <TextBox Text="Generation :" Width="150" Height="25" HorizontalContentAlignment="Center" Background="AliceBlue" Foreground="Black" FontSize="15"/> <TextBox Text="{Binding Generation, Mode=OneWay}" Height="25" HorizontalContentAlignment="Left" Background="AliceBlue" Foreground="Black" FontSize="15"/> </StackPanel> </StackPanel> </Window>
代码后置
在代码隐藏中,我有一个 DispatcherTimer,它将在提供的间隔后生成下一代。(我从 app.config 提供了时间,以便根据需要进行配置。)
using System; using System.Windows; using System.Windows.Threading; namespace GameOfLife { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class GameOfLifeWindow : Window { ViewModel vm; public GameOfLifeWindow() { InitializeComponent(); vm = new ViewModel(); this.DataContext = vm; DispatcherTimer timer = new DispatcherTimer(); int delay = Int32.Parse(System.Configuration.ConfigurationManager.AppSettings.Get("timer")); timer.Interval = TimeSpan.FromSeconds(delay); timer.Tick += timer_Tick; timer.Start(); } void timer_Tick(object sender, EventArgs e) { vm.Next(); } } }
转换器
该转换器将细胞状态从 A/D/E 转换为各自的颜色。
“A”- 存活的细胞,颜色 - 绿色
“D”- 死亡的细胞,颜色 - 红色
“E”- 空白的细胞,颜色 - 白色
using System; using System.Windows.Data; using System.Windows.Media; using System.Windows.Input; namespace GameOfLife { public class CharToColorConverter:IValueConverter { object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value != null) { switch ((char)value) { case 'A': return Brushes.Green; case 'D': return Brushes.Red; case 'E': return Brushes.WhiteSmoke; default: return null; } } return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }
ViewModel
我创建了一个二维数组,并用随机值填充了它。然后将二维数组转换为 ObservableCollection<ObservableCOllection<char>>,并将其绑定为 UI 的 ItemsSource。
填充初始网格并将其转换为 ObservableCollection 的逻辑在 Model 中。
Next 方法由 DispatcherTimer 的 Tick 方法调用(它会在我们通过上面的 app.config 提供的间隔后调用)。此方法以当前网格状态作为输入,对每个细胞应用规则,并返回网格的下一个状态。此新状态再次转换为 ObservableCollection 并通知 UI。
using System.Collections.ObjectModel; using System.ComponentModel; using System; namespace GameOfLife { public class ViewModel : System.ComponentModel.INotifyPropertyChanged { #region Fields and Properties static int len = Int32.Parse(System.Configuration.ConfigurationManager.AppSettings.Get("gridLength")); static int wid = Int32.Parse(System.Configuration.ConfigurationManager.AppSettings.Get("gridWidth")); private int generation = 0; public int Generation { get { return generation; } set { generation = value; OnPropertyChanged("Generation"); } } char[,] initialGrid = new char[len, wid]; char[,] newgrid = new char[len, wid]; char[,] tempgrid = new char[len, wid]; GameOfLifeModel obj; private ObservableCollection<ObservableCollection<char>> _lst; public ObservableCollection<ObservableCollection<char>> Lst { get { return _lst; } set { _lst = value; OnPropertyChanged("Lst"); } } # endregion #region Constructor public ViewModel() { obj = new GameOfLifeModel(); // char[,] initialGrid = new char[5, 5] { { 'A', 'D', 'E', 'A', 'D' }, { 'A', 'D', 'E', 'A', 'D' }, { 'A', 'A', 'E', 'D', 'D' }, { 'A', 'E', 'A', 'D', 'D' }, { 'A', 'D', 'E', 'A', 'D' } }; obj.FillGrid(initialGrid); Lst = obj.ConvertArrayToList(initialGrid); tempgrid = initialGrid; } # endregion #region Methods public void Next() { newgrid = obj.GenerateNextState(tempgrid); Lst = obj.ConvertArrayToList(newgrid); OnPropertyChanged("Lst"); Generation++; tempgrid = newgrid; } #endregion #region NotifyPropertyChanged Items public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } #endregion } }
模型
这是编写生成状态的整个逻辑的地方。
首先,我填充网格的初始状态。
其次,我将此网格传递以生成下一个状态。
为了生成下一个状态,我创建了一个工作网格(这是一个仅用于内部目的的中间网格,创建此网格是为了查找边缘单元格的邻居。请检查上面背景部分中的第 4 点)。
此工作网格将传递给 GenerateFinalGrid 方法,在该方法中,它对每个单元格应用规则并返回下一个状态。
此 Final grid 将转换为 ObservableCollection,然后通知 UI。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace GameOfLife { /// <summary> /// Game Of Life /// </summary> public class GameOfLifeModel { #region Private fields private char[,] workingGrid, finalGrid; #endregion #region Public methods /// <summary> /// Configures initial state of grid based on degree defined in app.config /// </summary> /// <param name="grid"></param> public void FillGrid(char[,] grid) { Int32 degree =Int32.Parse(System.Configuration.ConfigurationManager.AppSettings.Get("degree")); degree = Convert.ToInt32((degree * grid.GetLength(0) * grid.GetLength(1))/100); Random _random = new Random(); for (int i = 0; i <= grid.GetUpperBound(0); i++) for (int j = 0; j <= grid.GetUpperBound(1); j++) grid[i, j] = 'E'; int x,y; for (int i = 0; i <= grid.GetUpperBound(0); i++) for (int j = 0; j <= grid.GetUpperBound(1); j++) { if (degree <= 0) return; x= _random.Next(0, grid.GetUpperBound(0)); y=_random.Next(0, grid.GetUpperBound(1)); if (grid[x, y] != 'A') { grid[x,y] = 'A'; degree--; } } } /// <summary> /// Generates the next generation /// </summary> /// <param name="grid"> generation n</param> /// <returns> generation n+1</returns> public char[,] GenerateNextState(char[,] grid) { GetWorkingGrid(grid); GenerateFinalGrid(); return finalGrid; } /// <summary> /// Generates the final grid to be returned /// </summary> public void GenerateFinalGrid() { for (int i = 1; i < workingGrid.GetUpperBound(0); i++) { for (int j = 1; j < workingGrid.GetUpperBound(1); j++) { ApplyRulesOnEachCell(i, j); } } } /// <summary> /// Print the grid /// </summary> /// <param name="grid"></param> public void PrintState(char[,] grid) { for (int i = 0; i <= grid.GetUpperBound(0); i++) { for (int j = 0; j <= grid.GetUpperBound(1); j++) Console.Write(grid[i, j]); Console.WriteLine(); } } public ObservableCollection<ObservableCollection<char>> ConvertArrayToList(char[,] grid) { ObservableCollection<ObservableCollection<char>> lsts = new ObservableCollection<ObservableCollection<char>>(); for (int i = 0; i <= grid.GetUpperBound(0); i++) { lsts.Add(new ObservableCollection<char>()); for (int j = 0; j <= grid.GetUpperBound(1); j++) { lsts[i].Add(grid[i, j]); } } return lsts; } #endregion #region Private methods /// <summary> /// Gets a working grid which I will use internally to handle border cells. /// </summary> /// <param name="grid"></param> private void GetWorkingGrid(char[,] grid) { int lastX = grid.GetUpperBound(0); int lastY = grid.GetUpperBound(1); workingGrid = new char[lastX + 3, lastY + 3]; finalGrid = new char[lastX + 1, lastY + 1]; for (int i = grid.GetLowerBound(0); i <= grid.GetUpperBound(0); i++) for (int j = grid.GetLowerBound(1); j <= grid.GetUpperBound(1); j++) workingGrid[i + 1, j + 1] = grid[i, j]; workingGrid[0, 0] = grid[lastX, lastY]; workingGrid[0, lastY + 2] = grid[lastX, 0]; workingGrid[lastX + 2, 0] = grid[0, lastY]; workingGrid[lastX + 2, lastY + 2] = grid[0, 0]; for (int i = 0; i <= lastY; i++) { workingGrid[0, i + 1] = grid[lastX, i]; workingGrid[lastX + 2, i + 1] = grid[0, i]; } for (int i = 0; i <= lastX; i++) { workingGrid[i + 1, 0] = grid[i, lastY]; workingGrid[i + 1, lastY + 2] = grid[i, 0]; } } /// <summary> /// Applies rules to each cell to determine its new state /// </summary> /// <param name="i"></param> /// <param name="j"></param> private void ApplyRulesOnEachCell(int i, int j) { //count the live cells int count = 0; for (int row = i - 1; row <= i + 1; row++) { for (int col = j - 1; col <= j + 1; col++) { if (row == i && col == j) continue; else if ((workingGrid[row, col] == 'A')) count += 1; } } if ((workingGrid[i, j] == 'D') && count == 3) //If cell is dead. finalGrid[i - 1, j - 1] = 'A'; else if (workingGrid[i, j] == 'A' && (count < 2 || count > 3)) // If cell is alive. finalGrid[i - 1, j - 1] = 'D'; else finalGrid[i - 1, j - 1] = workingGrid[i, j]; } /// <summary> /// Used to determine if the next iteration will happen or not.Iteration stops when all cells are dead. /// </summary> /// <param name="newGrid"></param> /// <returns>returns true if any cell in the grid is still alive</returns> private static bool CheckIfAnyCellIsAlive(char[,] newGrid) { bool aliveCellsPresent = false; for (int i = 0; i <= newGrid.GetUpperBound(0); i++) for (int j = 0; j <= newGrid.GetUpperBound(1); j++) { if (newGrid[i, j] == 'A') aliveCellsPresent = true; } return aliveCellsPresent; } #endregion } }
App.Config
它提供了所有配置,请检查上面背景部分中的第 1、2、3 点。
您可以根据需要更改计时器间隔、存活细胞的初始程度(此处为 25%)或网格的长度和宽度。玩得开心 ;) 。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <appSettings> <add key="gridLength" value="20"/> <add key="gridWidth" value="20"/> <add key="Timer" value="2"/> <add key="Degree" value="25"/> </appSettings> </configuration>
值得关注的点:
1. 您可以了解 ItemsControl、DataTemplates 和 Converters 的强大功能。
2. 学习如何将字符的二维数组绑定到 Itemscontrol。(将二维数组转换为 ObservableCollection<ObservableCollection<char>> 并将其设置为 Itemssource)
3. 学习如何将字符的二维数组转换为列表或 ObservableCollection。
4. 如何创建一个边界单元格与另一侧边界单元格发生反应的网格
5. 学习 MVVM 设计模式。
6. 学习如何在运行时设置二维数组的大小,即从用户输入或配置文件中获取。
7. 学习如何从 app.config 文件读取数据。
8. 学习如何在 WPF 中使用 Timer (DispatcherTimer)。
9. 学习 ObservableCollection。
10. 遍历二维数组。
历史
版本 1.0