Introduction
Having only used it for a few months, I wanted to learn more about C#. I decided to create a project that would require knowledge in various areas of development so I would be forced to learn new things. I had always found the idea of the Game of Life (GoL) interesting, and I decided to implement my own simulator in C#.
Of course, my first step was to search The Code Project to see what was already out there. I found several articles. Of course, none of these really fit what I wanted to create - but they were useful for getting ideas.
Background
In case you've never heard of it before, here is Wikipedia's definition for Conway's Game of Life:
The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970. The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input from humans. One interacts with the Game of Life by creating an initial configuration and observing how it evolves.
Basically, you have a grid of cells that are either 'alive' or 'dead'. The ruleset being used determines the number of neighbors that will affect a cell's state.
For example, Conway's rules are 23/3 where the numbers before the slash are survival rules and numbers after are birth rules. In this case, if a live cell has two or three neighbors, it will survive - if it has more or less, it will die. If a dead cell has exactly 3 neighbors, it is born (becomes alive).
Project Requirements
Though I wanted to just start coding and see what I could come up with, I first thought about the project requirements. I knew I wanted to have a simulation engine separate from the UI. I knew I would have at least two different UIs (CLI and WinForms). And I knew I wanted to create a custom control for the grid display in the WinForms GUI.
These three goals (separate data from UI, make a custom control and create two UIs) were the driving factors for learning from this project.
GoL Simulation Engine
My first decision was to make the game grid a finite field - this simplifies the algorithm design greatly. Once that decision was made, the rest fell into place rather quickly.
To run a GoL simulation, we need a representation of a grid of cells that can exist in two states. I chose to use a one dimension bool
array for the best speed with human-readability. Since we need an array for both the current and next generation, we actually end up with two cell state arrays. I also threw in a third for storing the starting states to allow for a restart of the game.
The next requirement for a GoL simulation is the algorithm for getting from one generation to the next. Since there are various rulesets for GoL sims, I wanted to use variable rules. Knowing the number of cells, columns and rows would be good too, so we end up with the following fields in the LifeGame
class.
#region Fields
private bool[] _currentStates;
private bool[] _newStates;
private bool[] _startStates;
private int _rows;
private int _cols;
private int _cells;
private int _liveCells;
private List<int> _surviveRules;
private List<int> _birthRules;
private string _unparsedRules;
#endregion
Because I used generic lists of int
s for the rules, the method for moving forward one generation ends up rather short:
private void advancePopulation()
{
if (_liveCells == 0)
{ return; }
_liveCells = 0;
int neighbors;
int index;
bool alive;
for (int y = 0; y < _rows; y++)
{
for (int x = 0; x < _cols; x++)
{
neighbors = getNeighbors(x, y);
index = x + y * _cols;
alive = _currentStates[index];
if ((alive && _surviveRules.Contains(neighbors)) ||
(!alive && _birthRules.Contains(neighbors)))
{
_newStates[index] = true;
_liveCells += 1;
}
else
{ _newStates[index] = false; }
}
}
}
As you can see, accessing the state of an x
,y
coordinate in the grid is accomplished by transforming the coordinate to an index in the one dimension array (x + y * _cols)
.
Also, advancePopulation
only creates the next generation in the _newStates
array. The copying of that array to the _currentStates
array is handled in the public
method call for moving forward a generation (LifeGame.Step()
).
Now, I know I mentioned I wanted to keep the UI separate from the data side, but this just lends itself to command-line output. I simply created an override for the ToString
method and voilĂ - instant CLI display.
public override string ToString()
{
StringBuilder sb = new StringBuilder();
for (int y = 0; y < _rows; y++)
{
for (int x = 0; x < _cols; x++)
{
sb.Append(_currentStates[x + y * _cols] ? '*' : '.');
}
sb.AppendLine();
}
return sb.ToString();
}
Command Line Interface
Having a ToString
override built into the LifeGame
class, creating a CLI interface was rather simple. This was good, since it made for a fast testing platform to verify the simulation was accurately following the rules.
Here is the entire command line program:
static void Main(string[] args)
{
LifeGame lifeGame = new LifeGame(60, 40);
double lifeProbability = 0.25;
lifeGame.Randomize(lifeProbability);
bool exit = false;
bool first = true;
int genCount = 0;
while (!exit)
{
Console.Clear();
if (!first)
{
lifeGame.Step();
genCount += 1;
}
first = false;
Console.Write(lifeGame.ToString());
Console.Write("Any key to continue, 'r' to randomize, 'q' to quit. Gen: " + genCount);
char key = Console.ReadKey(false).KeyChar;
if (key == 'q')
{
exit = true;
}
else if (key == 'r')
{
lifeGame.Randomize(lifeProbability);
first = true;
genCount = 0;
}
}
}
Pretty simple, eh? The important parts to note are the creation of a LifeGame
with a 60x40 grid (2,400 cells) and the method used to randomize cells. When you call the Randomize
method, you provide a double
representing the percent probability that a cell will be alive. In this case, about 25% of the cells will start as live cells.
It looks like this when run (notice the glider bottom-right):
WinForms Interface
This is where I spent the majority of my development time, though I don't think there's anything particularly noteworthy in the code. If you notice anything that deserves more detail, please let me know and I'll update the article.
I wanted a clean UI that allowed for plenty of customization of the GoL simulation, as well as the ability to load and save life pattern files (saving is a TODO still). Thanks to the design of the LifeGame
class, we can change the ruleset at any point - even while the simulation is running. Because of this, I created a combobox
with various popular rulesets that are applied upon selection. It's interesting to see the changes when a stable pattern has its ruleset changed.
As you can see in the screenshot at the beginning of this article, I wanted a fair amount of control without having to dig too deeply. All the main functionality is controlled by buttons below the grid. This is also where the current generation and population are displayed.
The menu items allow for opening and saving (soon!) life pattern files (File
), as well as customizing the grid display (Options
) and some information about the latest loaded pattern and the application (Help
).
One point of interest is the ability to click the grid control to change the state of cells in the grid. This is accomplished by a MouseDown
event handler for the grid control.
void lifeGrid_MouseDown(object sender, MouseEventArgs e)
{
int y = (int)(((float)e.Y) * _lifeGame.Rows / lifeGrid.Height);
int x = (int)(((float)e.X) * _lifeGame.Columns / lifeGrid.Width);
_lifeGame.ToggleCellState(x, y);
lifeGrid.UpdateGrid(_lifeGame.GameGrid);
}
The grid itself is a custom control which allows for a fair amount of customization. At any point, you can show/hide grid lines, change the 'alive' cell color and change the 'dead' cell color. The custom grid is contained within a panel, to allow zooming by simply increasing the size of the custom grid (then scrolling around in the panel). Because the grid is designed to draw to whatever size it is set to, this has the effect of zooming in.
UserControl LifeGrid
I wanted to create a UserControl
since I had never done it before. I found a grid control online somewhere (if I ever find it again, I'll give credit - if you recognize it, please let me know!) which gave me an excellent starting point. The Paint
event handler is largely derived from their work.
After learning a bit about custom properties, I implemented a few for this control. This is what allows the changing of cell colors, and grid line visibility while in use. Here's how the grid visibility is set:
[Category("LifeGrid"),
Description("Whether or not lines are displayed."),
DefaultValue(true)]
public bool LinesVisible
{
get { return _gridLinesVisible; }
set { _gridLinesVisible = value; Invalidate(); }
}
Notice the Invalidate
call when setting the visibility. This guarantees the control is redrawn any time this property is changed (including at design time).
The main part of this control is, of course, the drawing of the grid. When UpdateGrid
is called, it expects (and checks for) a bool
array matching the size of the grid control. This array is then copied to an internal field for use by the Paint
event handler.
private void LifeGrid_Paint(object sender, PaintEventArgs e)
{
float cellWidth = (float)Width / _cols;
float cellHeight = (float)Height / _rows;
float line = 0;
if (_gridLinesVisible) { line = _gridLineThickness; }
Graphics painter = e.Graphics;
SolidBrush aliveBrush = new SolidBrush(_cellColorAlive);
SolidBrush deadBrush = new SolidBrush(_cellColorDead);
painter.FillRectangle(new SolidBrush(BackColor), new Rectangle(0, 0, Width, Height));
for (int y = 0; y < _rows; y++)
{
for (int x = 0; x < _cols; x++)
{
if (_gridStates[x + y * _cols])
{
painter.FillRectangle(aliveBrush, x * cellWidth, y * cellHeight,
cellWidth - line, cellHeight - line);
}
else
{
painter.FillRectangle(deadBrush, x * cellWidth, y * cellHeight,
cellWidth - line, cellHeight - line);
}
}
}
}
I made this as best I could, but it is still the bottleneck in the GUI application. Running without display, the simulator can work 1,000 generations on a 250x250 grid (62,500 cells) in under 10 seconds. However, when displaying (even skipping a few generations between display updates) it takes more like a minute to hit the 1,000 generation mark.
Known Bugs/Planned Improvements
The only bug I know of right now involves zooming in on the game grid and clicking it. What should happen is that the clicked cell is flipped (yes, works) and the display stays in place (no, it resets the panel holding our custom control).
Planned improvements include fixing that darn bug, possibly moving from using a timer to a threaded model, and implementing the save functionality (preferably with more than one save type).
I also want to learn more about painting a control to see if I can't improve the bottleneck in the grid.
Final Thoughts
This entire project was a learning experience for me, and I hope someone else can learn something from it as well. That said, I'm not done learning from this.
Please let me know what you thought of both the article and the code. If there are any ideas for improvement of either, I'm more than happy to hear constructive criticism!
History
- 9th September, 2009 - Initial release
- 10th September, 2009 - Minor additions and spelling/grammar fixes