# Conway's game of life

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
end
end
return model
end
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
else
new_status[pos...] = false
end
end
# Then, update the new statuses into the old
status .= new_status
return
end

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
end
end
return c
end
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 = (
heatarray = :status,
heatkwargs = (
colorrange = (0, 1),
colormap = cgrad([:white, :black]; categorical = true),
),
)

abmvideo(
"game_of_life.mp4",
model;
title = "Game of Life",
framerate = 10,
frames = 60,
plotkwargs...,
)