I was saddened to hear of the passing of the Mathematician John Horton Conway today so I decided to put together a post on his 'game of life' simulator.
This game of life, more commonly called Conway's game of life, was devised by John Conway in 1970 and is a way of modelling very simple cell population dynamics. The game takes place on a two dimensional board containing a grid of orthogonal cells. The game is technically a zero player game in that the initial setup of the game dictates the eventual evolution of the board.
The rules of the game (taken from wikipedia) are as follows.
- Any live cell with fewer than two live neighbours dies, as if by underpopulation.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
For every 'tick' of the game board the rules are evaluated and each cell is kept alive, given life or killed. Through these simple rules a great deal of complexity can be generated. This is the simulation running on a 100 by 100 game board for 100 frames.
If you look up Conway's game of life you will see a large collection of examples and programs that can be used to generate the game. It is often used as a teaching aid in computer science both in terms of data science but also as a very simple introduction to artificial intelligence.
To create this in PHP we first need to represent the game board. This is essentially a two dimensional array so we can represent this as a property in a class called Life.
class Life {
public $grid = [];
}
To actually create the grid we need to generate the multi-dimensional array. We can do this by generating lots of arrays that represent the y axis of the grid and then add them to an array that represents the x axis of the grid. Each cell will contain number 0 for dead and 1 for alive.
public function createGrid(){
for ($i = 1; $i <= 25; ++$i) {
$height = [];
for ($j = 1; $j <= 50; ++$j) {
$height[$j] = 0;
}
$this->grid[$i] = $height;
}
}
When we run function our array contains a 25 by 50 grid of 'cells', all of which are dead. This isn't a great start, but this can be easily altered to create a random grid where each cell is randomly selected to be alive or dead. This random grid is a good way of starting out a game of life simulation.
public function generateRandomGrid() {
for ($i = 1; $i <= 25; ++$i) {
$height = [];
for ($j = 1; $j <= 50; ++$j) {
$height[$j] = round(rand(0,1));
}
$this->grid[$i] = $height;
}
}
The next step is to create the 'tick' for each generation of the grid. All this does is loop through every cell in the grid and find out how many neighbours it has using the countAdjacentCells() method (which will be tackled later). With this count of neighbours we can then made a decision as to what to do with that cell.
public function runLife() {
$newGrid = [];
foreach ($this->grid as $widthId => $width) {
$newGrid[$widthId] = [];
foreach ($width as $heightId => $height) {
$count = $this->countAdjacentCells($widthId, $heightId);
if ($height == 1) {
// The cell is alive.
if ($count < 2 || $count > 3) {
// Any live cell with less than two or more than three neighbours dies.
$height = 0;
}
else {
// Any live cell with exactly two or three neighbours lives.
$height = 1;
}
}
else {
if ($count == 3) {
// Any dead cell with three neighbours lives.
$height = 1;
}
}
$newGrid[$widthId][$heightId] = $height;
}
}
$this->grid = $newGrid;
unset($newGrid);
}
Working out the count of adjacent cells is perhaps the most tricky bit in this algorithm. When I first sat down to write this I started off with a bunch of if statements that looked at each of the 8 adjacent cells. I realised that I could compress this "if statement hell" into a simple loop that goes through the 8 adjacent squares surrounding the central cell and look to see if they contain a live cell. This method will count the number of cells found and return that count.
public function countAdjacentCells($x, $y) {
$coordinatesArray = [
[-1, -1],[-1, 0],[-1, 1],
[0, -1],[0, 1],
[1, -1],[1, 0],[1, 1]
];
$count = 0;
foreach ($coordinatesArray as $coordinate) {
if (isset($this->grid[$x + $coordinate[0]][$y + $coordinate[1]])
&& $this->grid[$x + $coordinate[0]][$y + $coordinate[1]] == 1) {
$count++;
}
}
return $count;
}
Running the simulation without being able to see the output is pretty dull, so we need a way of doing this. The simplest way of doing this is by generating a string that represents the multi-dimensional array. This will just create a string and then loop through the multi-dimensional array, adding a "*" to represent a live cell and a "." to represent a dead cell. Each line is appended with a newline character so that the grid is correctly represented.
function render(Life $life) {
$output = '';
foreach ($life->grid as $widthId => $width) {
foreach ($width as $heightId => $height) {
if ($height == 1) {
$output .= '*';
} else {
$output .= '.';
}
}
$output .= PHP_EOL;
}
return $output;
}
This is everything we need to run Conway's game of life. The final step here is to put all of this together and run the simulation.
The following code will create a Life object, fill it with random data and then run the simulation. This will clear the command line output on each tick so that it will appear to animate the simulation. This is a really quick way of printing the output of the simulation without trying to generate gif animations.
Also, the size of 25 by 50 cells may seem odd, but this will print out a square shape when printed out in the command line.
<?php
$life = new Life();
$life->generateRandomGrid();
while (true) {
system('clear');
echo render($life);
$life->runLife();
usleep(1000);
}
Note that to stop this simulation you'll need to press ctrl + c, otherwise it will just continue forever.
This will produce the following output when first run, which will change on each run of the tick.
*..****...***....*....**.....*..**..*..******.*.*.
.*.**.**.**....****.**********.**.*.**.*.*******.*
.***.*..*......*.***...****.*.*.*..*...**....*.*..
**.......*.**..*..*.*.*..**.***..*.**..*.****....*
**..**..***.**.*......*.**..*.*.*.......***...**.*
*.**..*.*..*...*.********.*..**.****.*..*..**.***.
.*.**.**.***..*..***....**...*.*...**.*....*.*.**.
...*.**.*.***..**.*..**..*..**..*.*..*.*.**..*.**.
..******.*.****...**..*..*.**...**.*...*.****.*..*
*.***.*..**...**.*.****..****.***.*..**..***.*..**
**.**.....*..***..**..*.*...***..*****.*****.****.
*.*...*.**.*..*****.**...***...*........**.*.**.**
..*...***.**.*.*......*.*..**.****.***.*...*..***.
*..*****.*.*.**.*.*****.*....***...**..*..*.****.*
...*.***....*...*.*.**.**.**....***.**...*.*..*.*.
..**..*....**........*.*****......**....***.*..*.*
..*****.**..*..**.******..*.*.**..**..**.****...**
***..**.*..**..*.*..**....*..**.***...*****.*.**.*
****..**.***..***..*.*...**.*..**..**.*.**.*.*****
*...***.*...*.**..***..........*.*.*****..**.**.**
***.....*...*.....**..*...*.*.**.***.**..**.*..*.*
..**..*...*.*****.*.***..*..*.*.**.**.....*.**...*
**.*****..****.***.******.*.*.**.*.******.**..*..*
*.*.....****.*.....**..*.*..*.*....*..*.**.*.*.**.
**.*.**.**....*...***.**.....****.*..*..*..**...*.
This basic output will run until stopped, but you might find that on such a small grid that the simulation will quickly reach a state in which everything is either dead or (more likely) into some stable state where nothing is moving.
.....**...........................................
.....**....**............................**.......
...........**..............*.......**....**.......
..........................*.*......**.............
..........................*.*.....................
...........................*......................
..................................................
..................................................
.......**.........................................
.......**..**.....................................
...........**...............................**....
............................................**....
..................................................
..................................................
..................................................
....................*.............................
...................*.*............................
...................*.*............................
....................*.............................
......................................**..........
..***................................*..*.........
......................................**..........
..................................................
..................................................
..................................................
Using random data in this way will mean that you will rarely encounter what is called a 'generator'. That is, a shape of cells that will produce other shapes that move away. I mentioned at the start that Conway's game of life can generate great complexity, and these generators is part of that.
This is a simple solution to Conway's game of life, but it produces the results pretty well using the limitations of PHP.
Comments
Hello Philip,
I wrote down your code so now it is available on GitHub https://github.com/kaurov/lifeGame
Regards
Eugene
Submitted by Eugen Kaurov on Thu, 04/22/2021 - 04:54
PermalinkAdd new comment