Conways Game Of Life problem in WPF - MVVM.

Datetime:2016-08-23 01:26:12          Topic: MVVM Model  WPF           Share

Introduction

The field of the Game of Life is a two-dimensional field (grid). It is divided into rows and columns and ideally infinite. Each cell in this grid can be in one of three possible states, alive, dead or empty. Empty cells are handled like dead cells. Each cell responds to its immediate neighbours. The starting field is randomly filled with live or empty cells.

Background

Iteration Step – Rules for generating the next generation, i.e. Generation n -> Generation n+1 (The next generation is computed by applying these rules to each cell at the same time)

1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.

2. Any live cell with two or three live neighbours lives on to the next generation.

3. Any live cell with more than three live neighbours dies, as if by overcrowding.

4. Any dead cell  or empty cell with exactly three live neighbours becomes a live cell, as if by reproduction.

This program does this simulation: Start with Generation 0 (the initial state) and implement the iteration step (Generation n -> Generation n+1)

1.I have provided a (configurable) delay between generations of 200ms using app.config file2.Also grid-size is configurable via app.config file

3. The initial degree or percentage of live cells is also configurable.

4. In this example border-cells react with border-cells on the opposite side i.e.  suppose if you fold this grid such that its  opposite edges touches each other then the cells at the edges will get their neighbours.

User Interface

I have used a ItemsControl, supplied a DataTemplate to it. This Template again has an ItemsControl  which has Label in its DataTemplate.

These labels will depict the cells of the grid and this ItemsControl depicts the grid in total.

<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
{

    /// <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();

        }

    }
}

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();
            // 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


    }

}

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
{
    /// <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:

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>

Points of interest:

1. You can understand the power of ItemsControl , DataTemplates and Converters.

2. Learn how to bind 2D array of characters to Itemscontrol. (Convert the 2D array into ObservableCollection<ObservableCollection<char>> and set it as Itemssource)

3.Learn how to convert 2D array of characters to list orObservableCollection.

4. How to create a grid whose border cells react with border-cells on the opposite side

5. Learn MVVM design pattern.

6. Learn how to give size of 2D arrray at runtime ie either take it from user as input or take from config file.

7. Learn how to read data from app.config file.

8. Learn how to use Timer in WPF (DispatcherTimer).

9.Learn ObservableCollection.

10.Iterating over 2D arrays.

History

Version 1.0





About List