<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>
Code Behind:
In the code behind I have a DispatcherTimer that will generate the next generation after supplied interval of time.(I have supplied the time from app.config to make it configurable as per need.)
using System;
using System.Windows;
using System.Windows.Threading;
namespace GameOfLife
{
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();
}
}
}
Converter :
The converter converts the cell state from A/D/E to their respective colors.
'A'- alive cell , colour - Green
'D'- dead cell , colour - Red
'E'- empty cell , colour - White
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:
I have created a 2D array, filled it initially with random values. Converted the 2D array into ObservableCollection<ObservableCOllection<char>> and then binded this as ItemsSource to the UI.
The logic to fill the initial grid and to convert it into ObservableCollection is in the Model.
Method Next is called by the DispatcherTimer's Tick method(it gets called after the interval we supplied through the app.config) defined above in code behind.This method take the current state of grid as input , applies rules on each cell and returns the grid in its next state. This new state is again converted into ObservableCollection and notified to 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();
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
}
}
Model:
This is where the entire logic of generating states in written.
First I fill the initial state of the grid.
Second I pass this grid to generate the next state.
Inorder to generate next state I have created a working grid(this is an intermediate grid which is used for internal purposes only, this grid is created to find neighbours for the cells at the edges. Check the point no. 4 in the background section above. )
This working grid is passed to method GenerateFinalGrid where it applies rules on each cell and returns the next state.
This Final grid is converted into ObservableCollection and is then notified to the UI.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace GameOfLife
{
public class GameOfLifeModel
{
#region Private fields
private char[,] workingGrid, finalGrid;
#endregion
#region Public methods
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--;
}
}
}
public char[,] GenerateNextState(char[,] grid)
{
GetWorkingGrid(grid);
GenerateFinalGrid();
return finalGrid;
}
public void GenerateFinalGrid()
{
for (int i = 1; i < workingGrid.GetUpperBound(0); i++)
{
for (int j = 1; j < workingGrid.GetUpperBound(1); j++)
{
ApplyRulesOnEachCell(i, j);
}
}
}
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
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];
}
}
private void ApplyRulesOnEachCell(int i, int j)
{
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)
finalGrid[i - 1, j - 1] = 'A';
else if (workingGrid[i, j] == 'A' && (count < 2 || count > 3))
finalGrid[i - 1, j - 1] = 'D';
else
finalGrid[i - 1, j - 1] = workingGrid[i, j];
}
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:
It provides all the configurations , check point 1,2,3 in the back ground section above.
You can change the timer interval and the initial degree of alive cells(here its 25 %) or the length and width of the grid as per your need. Have fun ;) .
<?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>