Conway’s Game of Life is a zero-player game introduced by the mathematician John Horton Conway in 1970. Well, It has it’s own wiki and all.
The rules are simple. You start with a grid of cells with initial state of either living or dead. Cells interact with neighbors to define the next generation of cells.
- Any live cell with fewer than two live neighbors dies, as if by under-population.
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction
Implementation Link to heading
It’s using the following packages
- numpy and scipy for matrix operations
- matplotlib for plotting and animation
Animation Link to heading
matplotlib is new to me, so it took me some time to understand the basic building blocks. but what i want to highlight below is FuncAnimation
def generations(self):
for g in range (self.G):
yield self.step()
def run(self):
fig, ax = plt.subplots()
mat = ax.matshow(self.grid)
ani = animation.FuncAnimation(fig, mat.set_data, self.generations, interval=500, repeat=False)
plt.show()
docs says
fig: Figure The figure object that is used to get draw, resize, and any other needed events.
func: callable The function to call at each frame. The first argument will be the next value in frames. Any additional positional arguments can be > supplied via the fargs parameter.
frames: iterable, int, generator function, or None, optional If an iterable, then simply use the values provided. If the iterable has a length, it will override the save_count kwarg.
So, the second parameter is callable
, in this case i am passing mat.set_data
so, it will be called by matplolib to update frames.
but the most important part is that third parameter is iterable
or int
or generator function
. and this parameter will be passed to the callable in parameter 2.
def func(frame, *fargs)
which effectively is doing the following considering the generator will run G
generations before stopping.
def mat.set_data(this.grid)
gotcha Link to heading
well, the game is straightforward to write but there was something that bugged me little. Mainly, calculating the number of neighbors living cells.
I initially implemented it by manually counting ucelsls sing (i,j) index and it worked but it was ugly because of the boundary checks. Then I found convolve2d
from scipy
. It’s neat because using the right kernel, i can count the neighbors for all cells with one line. more details about convolution wiki
W = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]])
self.neighbour = signal.convolve2d(self.grid, W, 'same')
putting it all together Link to heading
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import argparse
class Conway():
def __init__(self, N=10, G=1, shape='random'):
self.N = N
self.G = G
self.grid = None
self.neighbour = None
if shape == 'random':
self.grid = np.random.choice(a=[False, True], size=(N, N))
else:
self.grid = np.zeros((N, N), dtype='bool')
def calc(self):
W = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]])
self.neighbour = signal.convolve2d(self.grid, W, 'same')
def step(self):
self.calc()
for i in range(self.N):
for j in range(self.N):
# Rule1: if dead cell have exactly 3 surrouding living cell, it become a live
if (self.grid[i,j] == 0 and self.neighbour[i,j] == 3):
self.grid[i,j] = 1
# Rule2 and Rule3: if living cell has > 3 or < 2 surrounding, it dies
if (self.grid[i,j] == 1 and(self.neighbour[i,j] < 2 or self.neighbour[i,j] > 3) ):
self.grid[i,j] = 0
# livining cell has 2 or 3, it staying alive
return self.grid
def generations(self):
for g in range (self.G):
yield self.step()
def run(self):
fig, ax = plt.subplots()
mat = ax.matshow(self.grid)
ani = animation.FuncAnimation(fig, mat.set_data, self.generations, interval=500, repeat=False)
plt.show()
def main():
parser = argparse.ArgumentParser(description='Conway game of life')
parser.add_argument('N', type=int, help='Grid Size')
parser.add_argument('G', type=int, help='Generations Count')
args = parser.parse_args()
g = Conway(args.N,args.G)
g.run()
if __name__ == "__main__":
main()