Conway Game of Life made Simple.

Asad Ali
6 min readMay 31, 2022
Photo by Joshua Fuller on Unsplash

Conway's game of life is a famous example of a simple dynamic non-deterministic process. Since its conception in 1971, this simple model has been studied thoroughly in physics, biology, and computer science. The system is also an example of a self-organizing system given a fundamental cell of rules. The game is similar to Rule-30 approaches later used by Wolfram in the book "A New Kind of Science," in which a random pattern emerges from an initial state in which a single cell.

The simple rules for "Life" are as follows. The game consists of self-replicating cells that transition based on the below rules.

  1. Any live cell with fewer than two live neighbors dies, as if by underpopulation.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies as if by overpopulation.
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

The initial pattern constitutes the seed of the system applied to the grid world of the game. At each stage of the world we use the same pure function of rules, and the world cells will dynamically change..

In this article, we will use some simple python functions to demonstrate the fundamental ideas of "Life."

So let's start building. Lets's import some basic libraries

from collections     import Counter
from typing import Set, Tuple, Dict, Iterator, List
from itertools import islice
from IPython.display import clear_output, display_html
from time import sleep
import sys

Now let's write down some basic function

Cell: Cell is a just a two-dimensional structure which can be represented using just a tuple, and the World of Life is just a large set of these Cells

Cell  = Tuple[int, int]
World = Set[Cell]

Each 2D cell will have eight neighbor cells. Lets's write a function for that calculating the neighbors we will use -1 and 1 indexing to show cell position left-right or up or down. Neighbor counts are simply the dictionary of cell counts using the Counter function.

def neighbor_counts(world) -> Dict[Cell, int]:
"""A Counter of the number of live neighbors for each cell."""
return Counter(xy for cell in world
for xy in neighbors(cell))

def neighbors(cell) -> List[Cell]:
"""All 8 adjacent neighbors of cell."""
(x, y) = cell
return [(x + dx, y + dy)
for dx in (-1, 0, 1)
for dy in (-1, 0, 1)
if not (dx == 0 == dy)]

So now, lets apply the simple rule that a cell moves to the next state if the neighbor count in the world. now let us write our main generator function, which will generate life forms

def life(world, n=sys.maxsize) -> Iterator[World]:
"""Yield `n` generations, starting from the given world."""
for g in range(n):
yield world
world = next_generation(world)

def next_generation(world) -> World:
"""The set of live cells in the next generation."""
return {cell for cell, count in neighbor_counts(world).items()
if count == 3 or (count == 2 and cell in world)}
world = {(3, 1), (1, 2), (1, 3), (2, 3)}
next_generation(world)
#(1, 2), (1, 3), (2, 3)}

The display doesn't show much so lets convert this into a more graphical format using "@" to represent cell position.

LIVE  = '@'
EMPTY = '.'
PAD = ' '

def picture(world, Xs: range, Ys: range) -> str:
"""Return a picture of the world: a grid of characters representing the cells in this window."""
def row(y): return PAD.join(LIVE if (x, y) in world else EMPTY for x in Xs)
return '\n'.join(row(y) for y in Ys)

Now lets test out the generator life function we wrote again,

g = life(world)
next(g), next(g)
print(picture(world, range(5), range(5)))
# The result shows multiple cells created
. . . . .
. . . @ .
. @ . . .
. @ @ . .
. . . . .

To study this dynamic system, we will need to use some animation. either we can use standard libraries like matplotlib or just jupyter html output.

def animate_life(world, n=10, Xs=range(10), Ys=range(10), pause=1/5):
"""Display the evolving world for `n` generations."""
for g, world in enumerate(life(world, n)):
clear_output(wait=True)
display_html(pre(f'Generation: {g:2}, Population: {len(world):2}\n' +
picture(world, Xs, Ys)), raw=True)
sleep(pause)

def pre(text) -> str: return '<pre>' + text + '</pre>'
animate_life(world, 4, range(5), range(5), 1)Generation: 3, Population: 4
. . . . .
. . . . .
. @ @ . .
. @ @ . .
. . . . .

Up till now, we have built some exciting examples of cellular automata but let dive deep into more interesting shapes and patterns of “Life”, these will have their own particular set of terminology. Generally speaking, innumerable forms of patterns or behavior can exist; however, some patterns in practicere are more common such as oscillators which have names called blinkers, toads or spaceships. To test out some of the shapes, we will write a simple function to convert the image or shape of the pattern into grids.

Systems will generally converge on these types of patterns.
def shape(picture, dx=3, dy=3) -> World:
"""Convert a graphical picture (e.g. '@ @ .\n. @ @') into a world (set of cells)."""
cells = {(x, y)
for (y, row) in enumerate(picture.splitlines())
for (x, c) in enumerate(row.replace(PAD, ''))
if c == LIVE}
return slide(cells, dx, dy)

def slide(cells, dx, dy):
"""Translate/slide a set of cells by a (dx, dy) offset."""
return {(x + dx, y + dy) for (x, y) in cells}

blinker = shape("@@@")
block = shape("@@\n@@")
beacon = block | slide(block, 2, 2)
toad = shape(".@@@\n@@@.")
glider = shape(".@.\n..@\n@@@")
rpentomino = shape(".@@\n@@.\n.@.", 36, 20)
line = shape(".@@@@@@@@.@@@@@...@@@......@@@@@@@.@@@@@", 10, 10)
growth = shape("@@@.@\n@\n...@@\n.@@.@\n@.@.@", 15, 20)
zoo = (slide(blinker, 5, 25) | slide(glider, 8, 13) | slide(blinker, 20, 25) |
slide(beacon, 24, 25) | slide(toad, 30, 25) | slide(block, 13, 25) | slide(block, 17, 33))
#lets try becon
animate_life(beacon)
#lets try more complex zoo pattern 
animate_life(zoo, 160, range(48), range(40))

We can even convert the above into a more fanciful 3-D plot with animation as below.

from collections     import Counter
from typing import Set, Tuple, Dict, Iterator, List
from itertools import islice
from IPython.display import clear_output, display_html
from time import sleep
import sys
Cell = Tuple[int, int,int]
World = Set[Cell]
def life(world, n=sys.maxsize) -> Iterator[World]:
"""Yield `n` generations, starting from the given world."""
for g in range(n):
yield world
world = next_generation(world)
def next_generation(world) -> World:
"""The set of live cells in the next generation."""
return {cell for cell, count in neighbor_counts(world).items()
if count == 4 or (count == 5 and cell in world)}
def neighbor_counts(world) -> Dict[Cell, int]:
"""A Counter of the number of live neighbors for each cell."""
return Counter(xy for cell in world
for xy in neighbors(cell))
def neighbors(cell) -> List[Cell]:
"""All 8 adjacent neighbors of cell."""
(x, y,z) = cell
return [(x + dx, y + dy,z+dz)
for dx in (-1, 0, 1)
for dy in (-1, 0, 1)
for dz in (-1,0,1)
if not (dx == 0 == dy == dz)]
world = {(2,1,1),(1,1,1),(1,0,1)}
d = neighbors((1, 2,3))
Counter(xy for xy in d)
next_generation(world)
world = {(1,2, 1), (1,1, 2), (1,1, 3), (1,2, 3)}
def animate_life(world, n=50, pause=1/5):
"""Display the evolving world for `n` generations."""
for g, world in enumerate(life(world, n)):
clear_output(wait=True)
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='3d')
ax.scatter(1, 2, 3, marker="o",color="blue")
for (x,y,z) in next_generation(world):
ax.scatter(x, y, z, marker="o")
plt.show()
sleep(pause)
world = {(2,2, 2), (2,2, 4), (1,1, 2),(1,1, 1),(1,2, 3),(1,3, 3)}
animate_life(world)

The output will converge into a giant 3-D spaceship.

--

--

Asad Ali

Data Science, Analytics, and Machine Learning Professional.