Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download

📚 The CoCalc Library - books, templates and other resources

132923 views
License: OTHER
Kernel: Python 3

Game of Life

Code examples from Think Complexity, 2nd edition.

Copyright 2016 Allen Downey, MIT License

%matplotlib inline import matplotlib.pyplot as plt import numpy as np import seaborn as sns from utils import savefig

Game of Life entities

from scipy.signal import correlate2d from Cell2D import Cell2D class Life(Cell2D): """Implementation of Conway's Game of Life.""" kernel = np.array([[1, 1, 1], [1,10, 1], [1, 1, 1]]) table = np.zeros(20, dtype=np.uint8) table[[3, 12, 13]] = 1 def step(self): """Executes one time step.""" c = correlate2d(self.array, self.kernel, mode='same') self.array = self.table[c]

The following function creates a Life object and sets the initial condition using strings of 0 and 1 characters.

def make_life(n, m, row, col, *strings): """Makes a Life object. n, m: rows and columns of the Life array row, col: upper left coordinate of the cells to be added strings: list of strings of '0' and '1' """ life = Life(n, m) life.add_cells(row, col, *strings) return life

A beehive is a stable entity, also called a "still life"

# beehive life = make_life(3, 4, 0, 0, '0110', '1001', '0110') life.draw() savefig('figs/chap06-1')
Saving figure to file figs/chap06-1
Image in a Jupyter notebook

Here's what it looks like after one step:

life.step() life.draw()
Image in a Jupyter notebook

A toad is an oscillator with period 2. Here's are its two configurations:

# toad plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) life = make_life(4, 4, 1, 0, '0111', '1110') life.draw() plt.subplot(1, 2, 2) life.step() life.draw() savefig('figs/chap06-2')
Saving figure to file figs/chap06-2
Image in a Jupyter notebook

Here's what the toad looks like as an animation.

life = make_life(4, 4, 1, 0, '0111', '1110') life.animate(10, 0.5)
Image in a Jupyter notebook

A glider is a spaceship that translates one unit down and to the right with period 4.

# glider plt.figure(figsize=(12, 4)) glider = ['010', '001', '111'] life = make_life(4, 4, 0, 0, *glider) for i in range(1, 6): plt.subplot(1, 5, i) life.draw() life.step() savefig('figs/chap06-3')
Saving figure to file figs/chap06-3
Image in a Jupyter notebook

Here's an animation showing glider movement.

life = make_life(10, 10, 0, 0, '010', '001', '111') life.animate(frames=28, interval=0.2)
Image in a Jupyter notebook

Exercise: If you start GoL from a random configuration, it usually runs chaotically for a while and then settles into stable patterns that include blinkers, blocks, and beehives, ships, boats, and loaves.

For a list of common "natually" occurring patterns, see Achim Flammenkamp, "Most seen natural occurring ash objects in Game of Life",

Start GoL in a random state and run it until it stabilizes (try 1000 steps). What stable patterns can you identify?

Hint: use np.random.randint.

# Solution n = 100 life = Life(n) life.array = np.random.randint(2, size=(n, n), dtype=np.uint8) life.animate(frames=1000)
Image in a Jupyter notebook

Methuselas

Most initial conditions run for a short time and reach a steady state. But some initial conditional run for a surprisingly long time; they are called Methuselahs.

The r-pentomino starts with only five live cells, but it runs for 1103 steps before stabilizing.

# r pentomino rpent = ['011', '110', '010'] life = make_life(3, 3, 0, 0, *rpent) life.draw()
Image in a Jupyter notebook

Here are the start and finish configurations.

# r pentomino plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) life = make_life(120, 120, 50, 45, *rpent) life.draw() for i in range(1103): life.step() plt.subplot(1, 2, 2) life.draw() savefig('figs/chap06-4')
Saving figure to file figs/chap06-4
Image in a Jupyter notebook

And here's the animation that shows the steps.

life = make_life(120, 120, 50, 45, *rpent) life.animate(frames=1200)
Image in a Jupyter notebook

Conway's conjecture

Most initial conditions run for a short time and reach a steady state. Some, like the r-pentomino, run for a long time before they reach steady state. Another example is rabbits, which starts with only nine cells and runs 17331 steps before reaching steady state.

To run my implementation of rabbits, open a terminal in ThinkComplexity2/code and run

python LifeRabbits.py

Patterns that take a long time to reach steady state are called Methuselahs

Patterns like these prompted Conway's conjecture, which asks whether there are any initial conditions where the number of live cells is unbounded.

Gosper's glider gun was the first entity to be discovered that produces an unbounded number of live cells, which refutes Conway's conjecture.

glider_gun = [ '000000000000000000000000100000000000', '000000000000000000000010100000000000', '000000000000110000001100000000000011', '000000000001000100001100000000000011', '110000000010000010001100000000000000', '110000000010001011000010100000000000', '000000000010000010000000100000000000', '000000000001000100000000000000000000', '000000000000110000000000000000000000' ]

Here's the initial configuration:

life = make_life(11, 38, 1, 1, *glider_gun) life.draw() savefig('figs/chap06-5')
Saving figure to file figs/chap06-5
Image in a Jupyter notebook

And here's what it looks like running:

life = make_life(50, 50, 2, 2, *glider_gun) life.animate(frames=200)
Image in a Jupyter notebook

Puffer train

Another way to "refute" Conway's conjecture is a puffer train.

To see a puffer train run, open a terminal and run

python LifePuffer.py

Implementing Game of Life

As an example, I'll start with an array of random cells:

a = np.random.randint(2, size=(10, 10), dtype=np.uint8) print(a)
[[0 0 0 0 1 0 0 0 1 0] [0 0 1 0 1 1 0 1 1 0] [1 0 0 0 1 0 1 1 0 1] [1 0 1 0 1 0 1 0 0 1] [1 0 0 1 1 1 1 1 0 1] [0 0 0 0 1 1 0 1 0 0] [1 0 1 0 0 1 0 1 0 0] [0 0 1 1 1 0 1 0 0 0] [1 0 0 0 1 0 0 0 1 0] [0 1 1 0 0 0 0 0 1 1]]

The following is a straightforward translation of the GoL rules using for loops and array slicing.

b = np.zeros_like(a) rows, cols = a.shape for i in range(1, rows-1): for j in range(1, cols-1): state = a[i, j] neighbors = a[i-1:i+2, j-1:j+2] k = np.sum(neighbors) - state if state: if k==2 or k==3: b[i, j] = 1 else: if k == 3: b[i, j] = 1 print(b)
[[0 0 0 0 0 0 0 0 0 0] [0 0 0 0 1 0 0 0 0 0] [0 0 0 0 1 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [0 1 0 0 0 0 0 1 0 0] [0 1 0 0 0 0 0 1 0 0] [0 1 1 0 0 0 0 1 0 0] [0 0 1 0 1 0 1 1 0 0] [0 0 0 0 1 1 0 1 1 0] [0 0 0 0 0 0 0 0 0 0]]

Here's a smaller, faster version using cross correlation.

from scipy.signal import correlate2d kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]) c = correlate2d(a, kernel, mode='same') b = (c==3) | (c==2) & a b = b.astype(np.uint8) print(b)
[[0 0 0 1 1 1 0 1 1 0] [0 0 0 0 1 0 0 0 0 1] [0 0 0 0 1 0 0 0 0 1] [1 0 0 0 0 0 0 0 0 1] [0 1 0 0 0 0 0 1 0 0] [0 1 0 0 0 0 0 1 0 0] [0 1 1 0 0 0 0 1 0 0] [0 0 1 0 1 0 1 1 0 0] [0 0 0 0 1 1 0 1 1 1] [0 1 0 0 0 0 0 0 1 1]]

Using a kernel that gives a weight of 10 to the center cell, we can simplify the logic a little.

kernel = np.array([[1, 1, 1], [1,10, 1], [1, 1, 1]]) c = correlate2d(a, kernel, mode='same') b = (c==3) | (c==12) | (c==13) b = b.astype(np.uint8) print(b)
[[0 0 0 1 1 1 0 1 1 0] [0 0 0 0 1 0 0 0 0 1] [0 0 0 0 1 0 0 0 0 1] [1 0 0 0 0 0 0 0 0 1] [0 1 0 0 0 0 0 1 0 0] [0 1 0 0 0 0 0 1 0 0] [0 1 1 0 0 0 0 1 0 0] [0 0 1 0 1 0 1 1 0 0] [0 0 0 0 1 1 0 1 1 1] [0 1 0 0 0 0 0 0 1 1]]

More importantly, the second version of the kernel makes it possible to use a look up table to get the next state, which is faster and even more concise.

table = np.zeros(20, dtype=np.uint8) table[[3, 12, 13]] = 1 c = correlate2d(a, kernel, mode='same') b = table[c] print(b)
[[0 0 0 1 1 1 0 1 1 0] [0 0 0 0 1 0 0 0 0 1] [0 0 0 0 1 0 0 0 0 1] [1 0 0 0 0 0 0 0 0 1] [0 1 0 0 0 0 0 1 0 0] [0 1 0 0 0 0 0 1 0 0] [0 1 1 0 0 0 0 1 0 0] [0 0 1 0 1 0 1 1 0 0] [0 0 0 0 1 1 0 1 1 1] [0 1 0 0 0 0 0 0 1 1]]

Exercise: Many Game of Life patterns are available in portable file formats. For one source, see http://www.conwaylife.com/wiki/Main_Page.

Write a function to parse one of these formats and initialize the array.

# Solution # The easiest format to parse is plain text: def read_life_file(life, filename, row, col): i = row with open(filename) as f: for line in f: if line.startswith('!'): continue line = line.strip() line = line.replace('O', '1') line = line.replace('.', '0') life.add_cells(i, col, line) i += 1
# Solution # Here's an example that loads a period 52 oscillator. n = 19 m = 19 row = 1 col = 1 life = Life(n, m) filename = '35p52.cells.txt' read_life_file(life, filename, row, col)
life.draw()
Image in a Jupyter notebook
# Solution # And here's the animation life.animate(frames=52, interval=0.1)
Image in a Jupyter notebook

Highlife

One variation of GoL, called "Highlife", has the same rules as GoL, plus one additional rule: a dead cell with 6 neighbors comes to life.

You can try out different rules by inheriting from Life and changing the lookup table.

Exercise: Modify the table below to add the new rule.

# Starter code class MyLife(Life): """Implementation of Life.""" table = np.zeros(20, dtype=np.uint8) table[[3, 12, 13]] = 1

One of the more interesting patterns in Highlife is the replicator, which has the following initial configuration.

replicator = [ '00111', '01001', '10001', '10010', '11100' ]

Make a MyLife object with n=100 and use add_cells to put a replicator near the middle.

Make an animation with about 200 frames and see how it behaves.

# Solution n = 100 life = MyLife(n) life.add_cells(n//2, n//2, *replicator) life.animate(frames=200)
Image in a Jupyter notebook

Exercise:

If you generalize the Turing machine to two dimensions, or add a read-write head to a 2-D CA, the result is a cellular automaton called a Turmite. It is named after a termite because of the way the read-write head moves, but spelled wrong as an homage to Alan Turing.

The most famous Turmite is Langton's Ant, discovered by Chris Langton in 1986. See http://en.wikipedia.org/wiki/Langton_ant.

The ant is a read-write head with four states, which you can think of as facing north, south, east or west. The cells have two states, black and white.

The rules are simple. During each time step, the ant checks the color of the cell it is on. If black, the ant turns to the right, changes the cell to white, and moves forward one space. If the cell is white, the ant turns left, changes the cell to black, and moves forward.

Given a simple world, a simple set of rules, and only one moving part, you might expect to see simple behavior---but you should know better by now. Starting with all white cells, Langton's ant moves in a seemingly random pattern for more than 10 000 steps before it enters a cycle with a period of 104 steps. After each cycle, the ant is translated diagonally, so it leaves a trail called the "highway".

Write an implementation of Langton's Ant.

# Solution from matplotlib.patches import RegularPolygon class Turmite(Cell2D): """Implements Langton's Ant""" # map from orientation to (di, dj) move = {0: (-1, 0), # north 1: (0, 1), # east 2: (1, 0), # south 3: (0, -1)} # west def __init__(self, n, m=None): """Initializes the attributes. n: number of rows m: number of columns """ m = n if m is None else m self.array = np.zeros((n, m), np.uint8) self.loc = np.array([n//2, m//2]) self.state = 0 def step(self): """Executes one time step.""" # in order to use an array as an index, we have to make it a tuple # https://docs.scipy.org/doc/numpy/user/quickstart.html#indexing-with-arrays-of-indices loc = tuple(self.loc) # get the state of the current cell try: cell = self.array[loc] except IndexError: raise IndexError('The turmite has gone off the grid') # toggle the current cell self.array[loc] ^= 1 if cell: # turn left self.state = (self.state + 3) % 4 else: # turn right self.state = (self.state + 1) % 4 move = self.move[self.state] self.loc += move def draw(self): """Updates the display with the state of the grid.""" super().draw() # draw the arrow center, orientation = self.arrow_specs() self.arrow = RegularPolygon(center, 3, color='orange', radius=0.4, orientation=orientation) ax = plt.gca() ax.add_patch(self.arrow) def arrow_specs(self): """Computes the center and orientation of the arrow.""" a = self.array n, m = a.shape i, j = self.loc center = j+0.5, n-i-0.5 orientation = -np.pi / 2 * self.state return center, orientation
n = 5 turmite = Turmite(n) turmite.draw()
Image in a Jupyter notebook
turmite.step() turmite.draw()
Image in a Jupyter notebook
# Solution # Here's a small version that shows the first 20 steps: turmite = Turmite(n=5) anim = turmite.animate(frames=20, interval=0.5)
Image in a Jupyter notebook
# And a larger version with 1000 steps turmite = Turmite(n=30) anim = turmite.animate(frames=1000)
Image in a Jupyter notebook
# Solution # To see the rest, run `python Turmite.py` in a terminal, running for 10700 steps.