Game of Life

Background

Covered in class.

Implementation

The following is an implementation of the Game of Life using the Mesa Agent Based Modeling library. This library has a few versions of the Game Of Life, the implementation here is slower but simpler to help you understand how to use the mesa library.

Outline of core classes Agent / Cell and Model

To implement an agent based model in mesa we need to create at least two classes:

Agent / Cell class

The Agent / Cell class should

So to start, the code for Cell will look like

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Cell(mesa.Agent):

    def __init__(self, model):
        super().__init__(model)

    def is_alive(self): ...

    def get_neighbors(self): ...

    def compute_step(self): ...

    def step(self): ...

Then as we add the required functionality, we will update this class. First we add the state and ALIVE and DEAD constants.

Store the state

A Cell has a state that is either DEAD or ALIVE. And since we want to perform a simultaneous update of all agents, we need a second variable _next_state to store the state that the agent will change to in the next step.

1
2
3
4
5
6
7
8
9
class Cell(mesa.Agent):

    DEAD = 0
    ALIVE = 1

    def __init__(self, model, state=DEAD):
        super().__init__(model)
        self.state = state
        self._next_state = state

The is_alive function

The is_alive function just returns True iff the state is ALIVE.

1
2
    def is_alive(self):
        return self.state == self.ALIVE

The get_neighbors function

The get_neighbors function returns a list of all neighbours of the agent. If the Model has a spatial grid, then each agent has a pos attribute that represents the agent's location in the model space.

Each agent can assess the model using self.model, access the model grid using self.model.grid, and from there use grid method get_neighbors function to get the neighbours of the agent.

For the Game of Life, we use the Moore neighbourhood (so moore=True), and we do not treat the agent as being a neighbour of itself (so include_center=False). Hence we have 8 neighbours and use the following code:

1
2
    def get_neighbors(self):
        return self.model.grid.get_neighbors(self.pos, moore=True, include_center=False)

The compute_step function and step function

The compute_step function computes the value of _next_state, typically based on the agent's state and the state of its neighbours.

1
2
3
4
5
6
7
    def compute_step(self):

        # get the neighbours

        # count the number of live neighbours

        # update _next_state based on state and number of live neighbours

The step function applies the update computed in compute_step to the state.

1
2
    def step(self):
        self.state = self._next_state

The Model class

The Model class stores the model parameters, the model geometry (space/grid), and the agents in the model.

The basic structure of this class is

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Model(mesa.Model):
    def __init__(self, seed=None):
        super().__init__(seed=seed)

        # create the model space/grid

        # create agents in the model


    def step(self):
        self.agents.do("compute_step")
        self.agents.do("step")

Implementing Game of Life

In Model.__init__ create a grid to store the cells/agents

To create the grid, we want a rectangular grid with wrap around geometry, and at each position of the grid we want a single cell (so use mesa.space.SingleGrid):

1
    def __init__(self, width=20, height=10, seed=None):
1
2
# create the model space/grid
self.grid = mesa.space.SingleGrid(width, height, torus=True)

In Model.__init__ create the agents/cells at each position in the grid

In the constructor, Model.__init__ we:

First add extra parameter to Mode.__init__

1
    def __init__(self, width=20, height=10, density=0.1, seed=None):

Then have loop to iterate over grid locations and for each location, create a cell, set its state, and place in grid.

1
2
3
4
5
6
7
8
        # create agents in the model
        for _, pos in self.grid.coord_iter():
            cell = Cell(self)

            if self.random.random()<density:
                cell.state = cell.ALIVE

            self.grid.place_agent(cell, pos)

Visualise the model

We need a function, draw_model with structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def draw_model(model, ax):

    # setup the matplotlib axes

    # draw the model grid

    # draw the model agents

    # add title to the plot

    plt.show()

To setup the axis we want to set the plot limits to match the model grid size, and hide the axis tick marks. So we have

1
2
3
4
5
6
7
    # setup the matplotlib axes

    width, height = model.grid.width, model.grid.height
    ax.set_xlim(0, width)
    ax.set_ylim(0, height)
    ax.set_axis_off()
    ax.set_aspect('equal')

We could use the matplotlib grid function to draw the grid, but I decided instead in drawing the (horizontal and vertical) lines using a for loop.

1
2
3
4
5
    # draw the model grid lines
    for x in range(width + 1):
        ax.axvline(x, color='black', linewidth=1)
    for y in range(height + 1):
        ax.axhline(y, color='black', linewidth=1)

We next draw the agents, I used a small circle and the colour is set based on the cell state.

1
2
3
4
5
6
7
    # draw the model agents as circle
    colors = ['white', 'green']
    for cell in model.agents:
        pos = cell.pos + np.array([0.5,0.5])
        color = colors[cell.state]
        circle = plt.Circle(pos, 0.4, color=color)
        ax.add_patch(circle)

Note: in the above I draw the DEAD cells as white and the ALIVE cells as green, so the DEAD cells are not visible. While checking your code, you may want to draw the DEAD cells as black so that you can see them.

Finally we add a title to the plot. Here we could display the number of steps taken (i.e. the stage) but we could display other information such as the population size, the number of changes etc.

1
2
    # add title to the plot
    ax.set_title(f"Stage {model.steps}")

Output of the draw_model function is shown below.

Animation

We will animate our models using the same process that we used for the numpy implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
n_frames = 500
model = Model(width=50, height=10, density=0.2, seed=667)

def frame(f):
    model.step()
    draw_model(model, ax)

fig, ax = plt.subplots(figsize=(10,3))

anim = animation.FuncAnimation(fig, frame, frames=n_frames)
plt.close()
HTML(anim.to_jshtml())

Data Collecting

TODO