Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Revisit the Game of Life while Learning about Extension Methods in C#

0.00/5 (No votes)
24 Aug 2008 1  
A fun variation of the Game of Life re-factored using extension methods

Introduction

The article's aim is to provide a fun variation of the Game of Life while learning about extension methods in C# 3.0 (.NET 3.5). Using the Game of Life cellular algorithm, we produce various fractal images. Extension methods are used to re-factor a base class that has become bloated. For further introductions, see the "About" sections below.

Now the Game of Life has been analyzed, programmed and explored by many enthusiasts ever since computers were invented. Hearing about the Game of Life again, many of you may yawn and close the article because you've seen this too many times. For those who are about to opt out, the use of extension methods may interest you. Do they really offer a good alternative to multi-inheritance?

Out of Scope

Time constraints was the reason for excluding further enhancements to this project. For instance, the user interface is simple and does not have an award winning look and feel. Also the efficiency of the code was not tested enough to say that the performance is on par with similar products. Also re-factoring was limited to extension methods.

About the Game of Life

If you know about the original Game of Life that appeared in 1970, then you may be intrigued with this revival.

The Game of Life is a cellular automaton created by John Conway. The use of the word "game" is deceptive because it is really a simulation with a two-dimensional matrix presentation. Users can "play" the "game" by changing the state of cells within the two-dimensional orthogonal grid.

The transition rules that describe the original Game of Life help the reader to understand the basic premise of this kind of simulation. These rules are:

  • A cell is either dead or alive.
  • Each cell (dead or alive) is evaluated according to the number of immediate neighbours (maximum eight in original model).
  • Then according to the number of neighbours, each cell may be born, stay alive or die.

The following table shows the decision matrix for the original model:

New status according to the number of neighbours
Current cell status 0 1 2 3 4 5 6 7 8
Alive dead dead alive alive dead dead dead dead dead
Dead dead dead dead alive dead dead dead dead dead

Game of Life enthusiasts have created many variations of cellular automatons. There is a good chance that the variations offered in this article have been tried before. If you wish to claim you were first, put your hand up and I may recognize your work by amending this article.

That is enough about the original Game of Life. If you want to read more, there is plenty of material on the Web.

Here is a good start, and if you want to see examples of various automatons, you can check out this link.

Other articles on The Code Project that are about the Game of Life:

About Extension Methods

Extension methods are just another way to add methods to a class. Before we indulge into sample code, consider the other techniques of adding methods to a class:

  • Changing the base class directly (soon will lead to a bloated class)
  • Inheritance (should not be used merely to add methods, soon the inheritance hierarchy will be bloated)
  • Partial classes (your base class has to be partial as well and the same namespace is required)
  • Multiple inheritance (not available in C#)

Each of the above techniques have significant disadvantages but it is not the aim of this article to discuss these design concerns. They are merely mentioned as a lead-up to the extension method technique.

A more obvious choice for a quick solution is to create a helper class that contains the desired methods. Albeit the methods will need a parameter to pass the instance of the base class.

class Base
{
}
static class Helper
{
    public static void NewMethod(Base obj)
    {
    }
}
//...
// Usage:
Base obj = new Base();
Helper.NewMethod(obj);

Unfortunately a helper class is a bad design choice for large projects mainly because callers of the new methods have to break away from the neat object oriented syntax. The reason helper classes are mentioned here is because they lead out design towards extension methods.

Extension methods are helper classes with one small variation. The parameter for passing the instance of the base class is declared with a "this" predicate. This change effectively adds our new method to the domain of the parameter's class.

class Base
{
}
static class Helper
{
    public static void NewMethod(this Base obj)
    {
    }
}
//...
// Usage:
Base obj = new Base();
obj.NewMethod();

Interestingly, programmers can now easily convert their helper classes to extension methods. This advantage may lead to design improvements without much effort.

Shortly, the advantages of extension methods are that they provide easy extension of class functionality without changing the base class. Some disadvantages of extension methods are that they only apply to methods, not properties; and that they do not easily allow you to add private variables. Beware if you add a static private variable; unless you want all your base class instances to update the same private variable instance (there are thread issues as well).

Another advantage of extension methods is that the additional methods can optionally be hidden if the static class uses a namespace.

class Base
{
}
namespace MyHelpers
{
    public static class HelperExtension
    {
        public static void NewMethod(this Base obj)
        {
        }
    }
}
/...
// For the extension method to be accessible, a using statement is required:
using MyHelpers;
// ...
Base obj = new Base();
obj.NewMethod();

If the context of your code does not require the extended method, don't add the using statement. This effectively hides the extended methods and leads to better control of where the extensions are supposed to be used.

For further reading on extension methods, see the following articles:

I hope this short explanation of extension methods will suffice, since the focus of this article needs to direct back to the fun-factor.

Variation on the Game of Life

The first variation to mention is the concept of cell aging. When a cell is born, it is given a countdown value to simulate age. The countdown value for each cell is decremented after each iteration of the game calculation. When a cell's countdown reaches zero, it dies.

The second variation to mention is the extended neighbourhood of a cell. The way a cell's neighbours are counted depends on a parameter called the neighbour distance (ND). The extent of countable neighbours is defined below. To clarify, the original Game of Life is based on neighbour distance 2.

ND=1
x
x c x
x
ND=2
x x x
x c x
x x x
ND=3
x
x x x
x x c x x
x x x
x
ND=4
x x x
x x x x x
x x c x x
x x x x x
x x x
ND=5
x x x x x
x x x x x
x x c x x
x x x x x
x x x x x

If the life model has a neighbour distance of 5, then the maximum number of neighbours is 24. The addition of this variance into the Game of Life has the effect of adding several fascinating "lifeforms".

The third variation is to color the cells according to number of neighbours and age. The number of neighbours determine the hue by simply combining red, green and blue according to the three least significant bits of the integer. For example, if the number of neighbours is 4 or 12, then the cell's hue will be blue. The cell's age is used to determine the opacity of the cell. A cell will be fully opaque at birth and then gradually become transparent as it ages. A cell that is near death due to aging will have similar visibility to a cell that is already dead and the appearance will be black.

The third variation is merely what most other enthusiasts have done. The feature of allowing the model to change the decision matrix. This flexibility allows the user to change what neighbour count will trigger a death or birth of a cell.

The Code

The lifeform model is coded into a class with no significant method implementations.

    public class LifeModelBase {
        public LifeModelBase() {
            CellSpawnIndicators = new List<int>();
            CellDeathIndicators = new List<int>();
        }

        public List<int> CellSpawnIndicators { get; private set; }
        public List<int> CellDeathIndicators { get; private set; }

        private int _NeighbourDistance = 4;
        public int NeighbourDistance { get {return _NeighbourDistance;} 
				  set {_NeighbourDistance = value;} }

        private int _MaximumAge = int.MaxValue;
        public int MaximumAge { get {return _MaximumAge;} set {_MaximumAge = value;} }
    }

The LifeModelBase class originally had methods that implement the cellular state transitions. However in an effort to clean and re-factor the code, the methods were moved to classes as extension methods. This was a design improvement because the implementation could be separated into different classes according to what type of extensions they offer. You can think of this as separation of concerns" without the explosion of interfaces, inheritance and dependency injection. Enough philosophy for now, here is an extract of some extension methods.

namespace LifeSimulation.Extensions {
    static class ProcessLifeModelExtension {
        public static void Process(this LifeModelBase model, World World) {
            List<Point> dieList = new List<Point>();
            List<Point> spawnList = new List<Point>();
            for (int i = 0; i < World.Space.GetLength(0); i++) {
                for (int j = 0; j < World.Space.GetLength(1); j++) {
                    switch (GetCellOutcome(model, World, i, j)) {
                        case CellOutcome.Dies:
                            dieList.Add(new Point(i, j));
                            break;
                        case CellOutcome.Spawns:
                            spawnList.Add(new Point(i, j));
                            break;
                    }
                }
            }

            //Die
            foreach (Point point in dieList)
                World.Space[point.X, point.Y] = 0;
            //Spawn
            foreach (Point point in spawnList)
                World.Space[point.X, point.Y] = model.MaximumAge;

            //Age
            for (int i = 0; i < World.Space.GetLength(0); i++)
                for (int j = 0; j < World.Space.GetLength(1); j++)
                    if (World.Space[i, j] > 0)
                        World.Space[i, j]--;
        }

        public static int GetMaximumNeighbours(this LifeModelBase model) {
            return GetMaximumNeighbours(model.NeighbourDistance);
        }
    }
}

namespace LifeSimulation.Extensions {
    static class SerializeLifeModelExtension {
        public static string Serialize(this LifeModelBase model) {
            StringBuilder sb = new StringBuilder();
            sb.Append("ND=");
            sb.Append(model.NeighbourDistance.ToString());
            sb.Append(" SI=");
            bool first = true;
            foreach (int i in model.CellSpawnIndicators) {
                if (!first) sb.Append(",");
                sb.Append(i.ToString());
                first = false;
            }
            sb.Append(" DI=");
            first = true;
            foreach (int i in model.CellDeathIndicators) {
                if (!first) sb.Append(",");
                sb.Append(i.ToString());
                first = false;
            }
            sb.Append(" MA=");
            sb.Append(model.MaximumAge.ToString());

            return sb.ToString();
        }

        public static void Deserialize(this LifeModelBase model, string value) {
            try {
                string[] parts = SerialToParts(value);

                //NeighbourDistance
                model.NeighbourDistance = int.Parse(parts[0]);

                //SpawnIndicators
                model.CellSpawnIndicators.Clear();
                foreach (string part in parts[1].Split(',')) {
                    if (part.Length > 0)
                        model.CellSpawnIndicators.Add(int.Parse(part));
                }

                //CellDeathIndicators
                model.CellDeathIndicators.Clear();
                foreach (string part in parts[2].Split(',')) {
                    if (part.Length > 0)
                        model.CellDeathIndicators.Add(int.Parse(part));
                }

                //MaxEnergy
                model.MaximumAge = int.Parse(parts[3]);
            }
            catch (Exception ex) {
                MessageBox.Show(ex.Message);
            }
        }
    }
}

And the following code snippets show how the extension methods are called:

using LifeSimulation.Extensions;

//...
private void paintTimer_Tick(object sender, EventArgs e) {
    if (!drawing) appState.LifeModel.Process(appState.World);
    pictureBox1.Invalidate();
}

//...

private void statusStripTimer_Tick(object sender, EventArgs e) {
    toolStripStatusLabel4.Text = appState.LifeModel.Serialize();
}

//...

private void editToolStripStatusLabel_Click(object sender, EventArgs e) {
    drawing = true;
    EditForm frm = new EditForm();
    appState.LifeModel.Deserialize(frm.EditModel(appState.LifeModel.Serialize()));
    PaintFavourites();
    drawing = false;
}

The code can be improved in many other ways - please go ahead and leave your comments about this. The intention was to limit re-factoring to extension methods and thereby help bring clarity to what extent this feature can be exploited. One aspect of extension methods that originally appears to limit its use, was that most of the supporting private methods in those static classes also had to include a parameter for the base class. This was after all not a limitation but a useful design feature. Those private methods become extensions of the base class only within the context of the helper class. One last visit to the code will show what I mean.

namespace LifeSimulation.Extensions {
    static class AppStateExtension {
        
        #region Extension methods

        public static void Spawn(this AppState appState, bool random) {
            if (random)
                appState.CreateRandomLife(10);
            else
                appState.CreateLifeAtCentre();
        }
                //...
        #endregion

        #region Private methods

        private static void CreateLifeAtCentre(this AppState appState) {
            appState.CreateLifeBlossom(
                appState.World.Space.GetLength(0) / 2,
                appState.World.Space.GetLength(1) / 2
            );
        }

        private static void CreateRandomLife(this AppState appState, int num) {
            for (int i = 0; i < num; i++)
                appState.CreateRandomLife();
        }
                //...
        #endregion
    }
}

That is enough about extension methods since we really have to get back to the fun part.

User Interface

As mentioned before, a simple user interface was chosen. Most of the main form is used for the display of cells. A simple toolbar at the bottom contains information and control functions.

Demo Mode

Here comes the fun part. Run the application. Select a zoom factor (this decides how big each cell is and how many cells can fit in the window). Then maximize the window. Then switch on the demo mode under options. Now sit back and watch the show. A new lifeform model will be generated every half a minute. Some lifeforms will disappoint - they vanish after aging without ability to regenerate. Extinction is detected and the demo engages another lifeform earlier because you obviously don't want to watch a blank screen for the rest of the half minute.

Given enough time to cycle through other lifeforms, you will soon see some glorious fractals. Some grow quickly, other grow slowly, and others become extinct after a few bursts of life. Many start very orderly and gradually lose that order as reflections against the matrix border introduce chaos. A good illustration of entropy, you think?

Some lifeforms pulsate with waves of colour as if the aggregation of cells behave like an organ from a more evolved species. Similarities with fauna and flora quickly become apparent. Most lifeforms seem to be plant-like, but others will clearly seem to be crystalline. A few crystalline lifeforms look a bit like the "Borg" from Star Trek. Another scarce phenomenon are those lifeforms that fill a rectangular section and do not venture further. These rectangular lifeforms can be enticed to grow further by clicking near its border, then the new dimensional limits of the rectangle will slowly be absorbed as if you gave it permission to do so.

Go ahead and discover this microcosm for yourself. Take your chance with random picks (using the "New" function). Or if you feel that you can handle the responsibilities of a god, edit the "DNA" of your lifeform and see if you can create life that could inspire a lengthy discussion.

Case Studies

The Original Game of Life

Use the following model:

ND=2 SI=3 DI=0,1,4,5,6,7,8 MA=99999

But let us not bother with pictures for this one.

The Case of the Borg

To show a crystalline analogy, try the following model:

ND=2 SI=2 DI=0,4,5,6,7,8,9,10,11 MA=50

Life19.png

Or do you think this ressembles worms or an electronic circuit board?

The Case of the Church Window

Now start with:

ND=4 SI=1 DI=10,11,12,13,14,15,16,17,18,19,20 MA=200

and get this church window effect...

Life41.png

Now while the church window is alive, change the model to:

ND=4 SI=10,11,12,13,14,15,16,17,18,19,20 DI=0 MA=100

and watch the whole thing explode with activity...

Life42.png

Then after a few seconds it seems to stabilize with a blue theme...

Life43.png

But given enough time, it explodes into action again...

Life44.png

Supernova

ND=4 SI=6,7,8 DI=5,9,10,11,12,13,14,15,16,17,18,19,20 MA=70

Life45.png

But the still image does not do that one justice. Try it!

Plant Analogies

To get a plant effect, use the model:

ND=4 SI=6 DI=5 MA=70

Life1.png

But let us not spoil your fun by preempting all cases. Go ahead and discover them for yourself.

Conclusion

The ramblings of the author hint towards the level of fun such animated images can give the viewer. Unfortunately all the images included in this article are static and don't fascinate as much as they should.

Most variations on the decision factors of the Game of Life produce lifeforms that spawn too quickly to be of interest to us. As you will read in other articles, finding a unique yet fascinating formula is rare. However, the introduction of aging has allowed us to re-consider many combinations that we would previously have rejected.

The software allows you to keep a list of your favourite lifeforms. Also the user can edit the decision factors at any time. What change to the death indicators does it take to make a cancerous lifeform slowly disappear into the void?

For those who don't want to compile the source code, the author has released this software under ClickOnce on the Coded Silicon website. This does require .NET 3.5 however. You can find it here.

History

  • 26th May, 2008: Initial version
  • 27th May, 2008: Updated to prevent null reference error

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here