Sunday, 20 May 2018

Artificial Life And Conway's Game of Life - Part 1/2

One of the grandest ideas for an algorithm is to simulate life - to create artificial life itself!

That would be cool - but we don't really know how life works to a very deep level. Instead, we can work with a simplified model to see where that takes us.

In 1970 a British mathematician, John Conway, came up with such a simple model of life, which is now called Conway's Game of Life.


Conway's Artificial Life

The living beings in Conway's game are very simple creatures that live according to really simple rules. Let's imagine them some character:


These creatures actually live in a landscape made of little cells. The little cells in which they sit are square, and so the landscape looks like grid of squares.


These little creatures are quite social and like company, just like most living things. But the company has to be just right - too crowded and they die, too lonely and they die too.

Conway designed four simple clear rules that decide whether these creatures love or die.


Rule 1 - Loneliness

If a creature has less than 2 neighbours, it dies of loneliness.


That is, if a creature has 0 or 1 neighbours, it will die and not appear in the next generation of the population.


Rule 2 - Just Right

If a creature has 2 or 3 neighbours, it is happy and continues to live into the next generation.



Rule 3 - Overcrowding

If a creature has more than 3 neighbours, then it is overcrowded and sadly it dies.



That means, if a creature has 4, 5, 6, 7 or 8 neighbours, it dies of overcrowding.


Rule 4 - Birth

If there is an empty cell surrounded by exactly 3 neighbours, then a new creature is born into this empty cell.


This is a different kind of rule, because it works on empty cells, not already occupied cells.


Simple Worked Example 1

Let's look at some examples to make sure we understand how populations of these creatures evolve.

Have a look at the following landscape of creatures. There are only 2 creatures sat next to each other.


What does the next generation of this population look like? We must apply the rules to find out:

  • Looking at each of the creatures, we can see they are lonely, Each one has only 1 neighbour. That means they'll die of loneliness, and won't appear in the next generation.
  • The other rules don't apply, there is no overcrowding, and there aren't enough neighbours to be "just right".
  • No creatures are born in an empty cell, because no empty cell has exactly 3 neighbours.

So the next generation is empty - there are no surviving creatures.



Simple Worked Example 2

Let's look at a different, but still simple, example. There are now 4 creatures arranged in a square. How does this population evolve?


Let's see how those rules apply to these creatures:

  • Each one of the creatures has 3 neighbours. That's the "just right" amount, so the creatures will carry on living into the next generation. No creatures will die.
  • There is no overcrowding, and there is no loneliness.
  • No new creatures will be born as there are no empty cells with exactly 3 filled neighbours.

This means that the next generation will be exactly like this generation - there will be no change!



Simple Worked Example 3

Let's look at one more example. This landscape is similar to the previous one, but has one creature missing.


What happens to these creatures? Lets apply each of the rules:

  • Each of the creatures has 3 neighbours. That means it is happy and will live into the next generation.
  • There are no lonely or overcrowded creatures, so no creatures will die.
  • This time, a new creature will be born in that cell that had a creature in the previous example but doesn't here. This is because the empty cell has exactly 3 neighbours. 

So the population will have grown by one creature for the next generation.


In these examples, we've seen creatures die, creatures live and creatures be born.


Let's Code It

Now that we've understood the rules let's write some code to actually simulate a population as it evolves. For now we'll keep the code simple so that we can stay focussed on the key ideas.

Here are the main design decisions:

  • The grid of cells is rectangular. That suggests a 2-dimensional array for storing the creatures. In many programming languages - eg Python - that's very easy to do. However, with Javascript, which underpins p5js, the language itself doesn't provide 2-dimensional arrays, only 1-dimensional lists. This means we just need to "unwrap" our 2-d array to a 1-d list, with each row following the next. The calculation for translating a position (i,j) in the 2d array is i + (rowsize * j) in the list. The following picture makes this clear.



  • We create array (albeit unwrapped as a 1-d list) with 50 rows and 50 columns, and give it an initial random population. That means we visit each cell and use randomness to either put a creature in it, or leave it empty. We use a value of 1 to mean that there is a creature, and 0 for an empty cell. Actually, we'll be a bit biased towards emptiness, so we'll only fill a cell with a probability of 30%. In the code, we can do this using if (random() < 0.3) which is only true 30% of the time.

  • We use the fact that the draw() function is called repeatedly to draw the grid and, then work out the next generation. This should give us an animation if the contents of the grid change. We saw how a repeatedly called draw() can be used to create animation in a previous post. We actually slow down the rate at which draw() is called to 5 frames per second using frameRate(5). The default frame rate is much higher, with is great for smooth animation, but not for watching how our creature populations evolve.

  • We use the familiar loop within a loop to count rows j and columns i, as we visit each cell at (i,j) of the grid and consider how the rules apply. To work out the number of neighbours, we need to also visit the neighbours of this cell at (i,j). We use yet another loop within a loop to count the rows and columns immediately around this cell. 

  • We apply the rules and decide whether the creature lives on into the next generation. If it does, we don't write it into the current array grid_array, because that would make the next neighbour counts wrong. We need to put it into a temporary new array grid_array_2  Once all the cells have been visited, and a new temporary array completed, we point the name name grid_array at the new data, and leave javascript to free up the memory used for the now out of date data. If you're interested, this discussion on stackexchange confirms how/why this works.

This works well. Here's an example of a 50x50 grid:


Cool!

We can see that at the initial random population starts to settle down into shapes that are static, and there are some oscillator in there too. Watching closely I can even see some nice cross patterns emerging before being obliterated by neighbouring creatures!

Before we move on, did you notice how there isn't much going on at the edges of that grid? The cells all around the edge become vacant very quickly. The reason is that we cheated a little by not considering the creatures in those edge cells to make the logic for counting neighbours easier. How do we count the neighbours for a cell on the edge of the grid? We could just count the ones that are on the grid - which makes perfect sense - but the code to make this a special case is a bit messy. So we've kept the code simple and just ignored the edge cells.  For me at least, the resulting image is rather pleasing as the empty edges create a kind of frame for the patterns.

Another approach is to "wrap the grid around the edges". That is, if we walk off the edge of the grid, instead of falling off, we just re-appear on the other side. This means that when we count the neighbours for a cell on the edge of the grid, we include the cells that are on the other side of the grid. The following picture makes this clear.


We can keep the code simple, avoiding special cases for the edges, by using the javascript remainder function. The following code illustrates how coordinates (i,j) are modified to wrap around the grid:

var wrapped_i = (i + x + number_of_rows) % number_of_rows;
var wrapped_j = (j + y + number_of_columns) % number_of_columns;

The fancy term for this wrapping around is making the grid periodic. Here's the result:


If we watch carefully we can see populations on the edge move onto the opposite edge, and we also don't see the empty edges that we had before.

The code and sketch is online at:


Simple Rules, Complex Behaviour

We've explored the very simple rules that Conway designed to model artificial life. It is striking that the rules are so simple - and yet the resulting patterns and their behaviours are quite interesting.


In Part 2 we'll look at extending this basic idea of simple artificial life.


Resources


No comments:

Post a Comment