Conway's game of life

Game of life on wikipedia.

using Agents, Random

1. Define the rules

Conway's game of life is a cellular automaton, where each cell of the discrete space contains one agent only.

The rules of Conway's game of life are defined based on four numbers: Death, Survival, Reproduction, Overpopulation, grouped as (D, S, R, O) Cells die if the number of their living neighbors is <D or >O, survive if the number of their living neighbors is ≤S, come to life if their living neighbors are ≥R and ≤O.

rules = (2, 3, 3, 3) # (D, S, R, O)
(2, 3, 3, 3)

2. Build the model

Like in the Forest fire example, we have a cellular automaton in our hands. This is a model that does not require any agents. Just a matrix whose "color" or "status" is the only thing necessary for the simulation.

We still need to define a dummy agent type though for ABM:

@agent struct Automaton(GridAgent{2}) end

The following function builds a 2D cellular automaton given some rules. dims is a tuple of integers determining the width and height of the grid environment. metric specifies how to measure distances in the space, and in our example it actually decides whether cells connect to their diagonal neighbors or not. :chebyshev includes diagonal, :manhattan does not.

This function creates a model where all cells are dead.

function build_model(rules::Tuple;
        alive_probability = 0.2,
        dims = (100, 100), metric = :chebyshev, seed = 42
    space = GridSpaceSingle(dims; metric)
    properties = Dict(:rules => rules)
    status = zeros(Bool, dims)
    # We use a second copy so that we can do a "synchronous" update of the status
    new_status = zeros(Bool, dims)
    # We use a `NamedTuple` for the parameter container to avoid type instabilities
    properties = (; rules, status, new_status)
    model = StandardABM(Automaton, space; properties, model_step! = game_of_life_step!,
                rng = MersenneTwister(seed), container = Vector)
    # Turn some of the cells on
    for pos in positions(model)
        if rand(abmrng(model)) < alive_probability
            status[pos...] = true
    return model
build_model (generic function with 1 method)

Now we define a stepping function for the model to apply the rules to agents. We will also perform a synchronous agent update (meaning that the value of all agents changes after we have decided the new value for each agent individually).

function game_of_life_step!(model)
    # First, get the new statuses
    new_status = model.new_status
    status = model.status
    @inbounds for pos in positions(model)
        # Convenience function that counts how many nearby cells are "alive"
        n = alive_neighbors(pos, model)
        if status[pos...] == true && model.rules[1] ≤ n ≤ model.rules[4]
            new_status[pos...] = true
        elseif status[pos...] == false && model.rules[3] ≤ n ≤ model.rules[4]
            new_status[pos...] = true
            new_status[pos...] = false
    # Then, update the new statuses into the old
    status .= new_status

function alive_neighbors(pos, model) # count alive neighboring cells
    c = 0
    @inbounds for near_pos in nearby_positions(pos, model)
        if model.status[near_pos...] == true
            c += 1
    return c
alive_neighbors (generic function with 1 method)

now we can instantiate the model:

model = build_model(rules)
StandardABM with 0 agents of type Automaton
 agents container: Vector
 space: GridSpaceSingle with size (100, 100), metric=chebyshev, periodic=true
 scheduler: fastest
 properties: rules, status, new_status

3. Animate the model

We use the InteractiveDynamics.abmvideo for creating an animation and saving it to an mp4

using CairoMakie

plotkwargs = (
    add_colorbar = false,
    heatarray = :status,
    heatkwargs = (
        colorrange = (0, 1),
        colormap = cgrad([:white, :black]; categorical = true),

    title = "Game of Life",
    framerate = 10,
    frames = 60,