To simulate Conway's Game of Life on a \(20\times20\) grid for arbitrary starting configurations and \(t\) time steps, we can write a function
game_of_life(start_configuration,t) which as its inputs has the starting configuration
start_configuration
(as a \(n\times m\)-matrix with \(0< n< 20,0< m< 20\)) and the number of time steps
t.
As a first step, the function should check whether the
start_configuration is an acceptable input. For this, we can in Julia use the fact that
we can specify the data-type of an input. We will use
start_configuration::Matrix{Int64} and
t::Int64.
Now, the function should check if the dimensions of
start_configuration fulfill the above stated requirement.
if 0 < size(start_configuration,1)< 20 && 0 < size(start_configuration,2) < 20
If this is the case, we can start the actual simulation.
The function defines a variable
state which will at all times be a representation of the current configuration. For this,
state
should again be a matrix, this time with the dimensions of the whole Game of Life grid, namely \(20\times 20\). Considering that most of the time, we only want to
simulate the evolution of small "objects" in an otherwise empty configuration, it makes sense to initialize the
state variable with 0 in every entry.
state=zeros(Int64,20,20)
Now the values in
start_configuration should be included in
state. Even though it is not necessary, the functions
should center the "object" encoded in
start_configuration in the Game of Life grid. Therefore, we have to calculate the "slice"
(Julia Language makes this easy) of the
state that should be replaced by
start_configuration.
This is rather lengthy, so I won't go into much detail. In Julia, the code can look like this (where
f(a,b,A) is a helper function).
function f(a,b,A)
if a
return 10+ceil(Int64,(size(A,b)-1)/2)
else
return 10-floor(Int64,(size(A,b)-1)/2)
end
end
state[f(0,1,start_configuration):f(1,1,start_configuration),
f(0,2,start_configuration):f(1,2,start_configuration)]=start_configuration
The next part gets repeated for every time step, i.e.
t times.
for _ in 1:t
First we create a temporary state
temp_state which we can again initialize with 0 in all entries, for convince.
temp_state = zeros(Int64,20,20)
This
temp_state
will be used to temporarily store the values of the next (i.e. in the time evolution) configuration while it is computed. This is important because when computing the new value of one site,
we have to look at the values of the neighboring sites. If the value of a neighboring site in the next configuration differs from its value in
state, it
would corrupt the computation of said site.
Now the function can go over every site in the grid, to compute its new value and save it in
temp_state.
for i in 1:20
for j in 1:20
For each site, we have to apply the rules of Conway's Game of Life. Basically, we have to first differentiate if the site that is to be computed has a value of 0 or 1 in
state.
If it is 0, if the sum of all neighbors is equal to 3, the function saves a 1 in
temp_state. If it is 1, if the sum of all neighbors is less than 2 or
grater than 3, the function saves a 0 in
temp_state, else a 1.
if state[i,j] == 0
if neighbor_sum(state,i,j) == 3
temp_state[i,j]=1
end
else
n = neighbor_sum(state,i,j)
if n < 2 || n > 3
temp_state[i,j]=0
else
temp_state[i,j]=1
end
end
Where the function
neighbor_sum(state,i,j) returns the sum over all neighboring sites of a given site \((i,j)\) in
state,
considering periodic boundary conditions. An implementation in Julia could look like this.
function neighbor_sum(state,i,j)
return sum(state[[CartesianIndex(mod1(i-1,20),j),
CartesianIndex(mod1(i+1,20),j),
CartesianIndex(i,mod1(j-1,20)),
CartesianIndex(i,mod1(j+1,20)),
CartesianIndex(mod1(i-1,20),mod1(j-1,20)),
CartesianIndex(mod1(i-1,20),mod1(j+1,20)),
CartesianIndex(mod1(i+1,20),mod1(j-1,20)),
CartesianIndex(mod1(i+1,20),mod1(j+1,20))]])
end
When every site in the Game of Life grid has a new value, the function can conclude this time step and copy the values from
temp_state
into
state.
state=copy(temp_state)
And after
t time steps have been computed, we can return the configuration
state.
return state
The full function in Julia could look like this.
function game_of_life(start_configuration::Matrix{Int64},t::Int64)
if 0 < size(start_configuration,1)< 20 && 0 < size(start_configuration,2) < 20
state=zeros(Int64,20,20)
state[f(0,1,start_configuration):f(1,1,start_configuration),
f(0,2,start_configuration):f(1,2,start_configuration)]=start_configuration
for _ in 1:t
temp_state = zeros(Int64,20,20)
for i in 1:20
for j in 1:20
if state[i,j] == 0
if neighbor_sum(state,i,j) == 3
temp_state[i,j]=1
end
else
n = neighbor_sum(state,i,j)
if n < 2 || n > 3
temp_state[i,j]=0
else
temp_state[i,j]=1
end
end
end
end
state=copy(temp_state)
end
return state
end
end
We can test this by inputting some start configurations shown in the figure above.
box = [1 1;
1 1];
star = [0 1 0;
1 0 1;
0 1 0];
bar = [1 1 1];
offset = [0 1 1 1;
1 1 1 0];
And plot them using again the
Plots package.
plot(Gray.(game_of_life(start_configuration,t)),axis=false,ticks=false)
When increasing the value of
t over time, we get the time evolution, which can be visualized in an animation like this (all of the above start configurations
at once because why not).