Game of Life

Recently, I’ve been reading a very interesting book (The Recursive Universe) about a game called Game of Life.

The game consists of three elements:

  • An (unlimited) grid where each cell can be defined alive or dead;
  • A number of alive cells placed somewhere in the grid;
  • Three simple rules which set the conditions for cells to duplicate or die

Once the grid and the set are defined, the next generation of cells is obtained by applying three rules recursively:

  • Any live cell with fewer than 2 neighbours and more than 3 dies by isolation or overpopulation, respectively;
  • Any live cell with exactly 2 or 3 live cells stays alive;
  • Any dead cell with exacly 3 live neighbours becomes alive.

I thought about implementing the Game of Life in a simple Python script. The World in the Game of Life universe becomes a Python class. The whole source code is available at the end of the post (and on Bitbucket at bitbucket.org/co2h5/gameoflife ). Here, a quick description of the class is given.

class World:
    def __init__(self, sizex, sizey):
        # This method initialises the class by defining the maximum size of the World. 
        ...

    def set(self, x, y, value):
        # Set the state of the cell at location (x,y). The state can be on (alive) or off (dead). If the location is outside the World, no assignment is performed.
        ...

    def get(self, x,y):
        # Get the state of the cell at location (x,y). If the location is outside the World, a value corresponding to a dead cell is returned.
        ...

    def empty_grid(self):
        # Initialise an empty World by allocating memory and setting all cells to off. 
        ...

    def count_neighbours(self, x, y):
        # Count the number of neighbouring cells which are alive.
        ...

    def next_step(self):
        # Take the next step of the evolution by applying the three rules recursively.
        ...

    def generate_picture(self, filename, blksize=2, xrange=None, yrange=None, colorscheme=1):
        # Generate a picture of the World and save it to file. This method uses the Pillow library to generate a PNG file.
        ...

    def save_to_file(self, filename):
        # Save the state of the simulation to file so that it is possible to restart from this point later now. This method uses Scipy IO library.
        ...

    def load_from_file(self, filename):
        # Load the state of the simulation from file so that it is possible to restart from that point. This method uses Scipy IO library.
        ...

And now… some interesting animations from the Game of Life!

R-pentomino

Evolution in the Game of Life is something very interesting. Once the initial set of alive cells is given, the user does not intervene anymore. A simple initial set of cells can lead to an interesting final pattern. For example, the pattern described below is called r-pentomino and it takes around 1100 generations to get to its final step. This is the animation which describes the whole evolution:

Random set

The fun is also to try playing the Game of Life with a random set of cells. For example, what will happen to these few cells?

More information about the Game of Life can be found on this Wiki.

Source code

import subprocess
import random
import numpy as np
import scipy.io as sio

class World:
    def __init__(self, sizex, sizey):
        self.grid = (sizex, sizey)
        self.data = self.empty_grid()
        self.age = 0

    def set(self, x, y, value):
        if x < 0 or x > self.grid[0]:
            return

        if y < 0 or y > self.grid[1]:
            return

        self.data[x,y] = value

    def get(self, x,y):
        if x < 0 or x >= self.grid[0]:
            return False

        if y < 0 or y >= self.grid[1]:
            return False

        return self.data[x,y]

    def empty_grid(self):
        return np.zeros(self.grid, dtype=bool)

    def count_neighbours(self, x, y):
        count = 0

        for c in range(x-1, x+2):
            for r in range(y-1, y+2):
                if x == c and r == y:
                    continue
                if self.get(c,r):
                    count += 1

        return count

    def place_cells(self, cells, offset=(0,0)):
        for x, y in cells:
            self.set(x+offset[0], y+offset[1], True)

    def place_cells_random(self, num, xrange=None, yrange=None):
        if xrange is not None:
            xmin, xmax = xrange
        else:
            xmin, xmax = 0, self.grid[0]

        if yrange is not None:
            ymin, ymax = yrange
        else:
            ymin, ymax = 0, self.grid[1]

        for i in range(num):
            x = random.randint(xmin, xmax-1)
            y = random.randint(ymin, ymax-1)
            self.set(x, y, True)

    def next_step(self):
        new_data = self.empty_grid()

        for x in range(self.grid[0]):
            for y in range(self.grid[1]):
                n = self.count_neighbours(x,y)

                if n == 2:
                    new_data[x,y] = self.get(x,y)
                elif n == 3:
                    new_data[x,y] = True

        self.data = new_data
        self.age += 1

    def generate_picture(self, filename, blksize=2, xrange=None, yrange=None, colorscheme=1):
        from PIL import Image, ImageDraw

        if xrange is not None:
            xmin, xmax = xrange
        else:
            xmin, xmax = 0, self.grid[0]

        if yrange is not None:
            ymin, ymax = yrange
        else:
            ymin, ymax = 0, self.grid[1]

        if colorscheme == 2:
            bkc = 'white'
            fgc = 'black'
        else:
            bkc = 'black'
            fgc = 'white'

        img = Image.new('RGB', ((xmax - xmin)*blksize, (ymax - ymin)*blksize))
        draw = ImageDraw.Draw(img)

        draw.rectangle((0, 0, (xmax - xmin)*blksize, (ymax - ymin)*blksize), fill=bkc)

        for x in range(xmin, xmax):
            for y in range(ymin, ymax):
                if self.get(x,y):
                    draw.rectangle(((x-xmin)*blksize, (y-ymin)*blksize,
                                    (x-xmin+1)*blksize, (y-ymin+1)*blksize),
                                   fill=fgc)

        img.save(filename)

    def save_to_file(self, filename):
        sio.savemat(filename, {'data': self.data, 'age': self.age}, appendmat=True)
        pass

    def load_from_file(self, filename):
        data_dic = sio.loadmat(filename, appendmat=True)

        self.grid = (self.data.shape[0], self.data.shape[1])
        self.data = np.array(data_dic['data'], dtype=bool)
        self.age = data_dic['age'][0,0]

if __name__ == "__main__":
    cells = [
        ( 0,  1),
        ( 0,  0),
        ( 0, -1),
        (-1,  0),
        ( 1,  1),
    ]

    wld = World(256,256)
    try:
        wld.load_from_file('last_step')
    except FileNotFoundError:
        wld.place_cells(cells, offset=(128,128))
    #    #wld.place_cells_random(128, xrange=(448,576), yrange=(448,576))

    for i in range(wld.age, wld.age + 1200):
        print("Step {}".format(i))
        wld.generate_picture('step{:05d}.png'.format(i),
                             xrange=(43,213),
                             yrange=(64,192),
                             blksize=8, colorscheme=2)
        wld.save_to_file('last_step')
        wld.next_step()

    output_gif = 'r_pentomino.gif'
    subprocess.call(['ffmpeg', '-framerate', str(20), '-i', 'step%05d.png', '-pix_fmt', 'gray', output_gif])

Leave a Reply

avatar
  Subscribe  
Notify of

Pin It on Pinterest