The Game of Life

The Game of Life

A cellular automaton consists of a regular grid of "cells", each containing a "state" chosen from a finite set and which can evolve over time. The state of a cell at time t+1 is a function of the state at time t of a finite number of cells called its "neighborhood". At each new unit of time, the same rules are applied simultaneously to all cells in the grid, producing a new "generation" of cells that depends entirely on the previous generation.

Cellular automata are studied in mathematics and theoretical computer science. The cellular automaton model is remarkable for the gap between the simplicity of its definition and the complexity that certain macroscopic behaviors can achieve.

Today we will create our first cellular automata!

One-dimensional system

Presentation

We will consider a one-dimensional grid of cells that can only take two states: 0 or 1, and we will define rules based on a cell and its two immediate neighbors.

Example:

Starting from this one-dimensional grid, let's consider that the color of each cell depends on its neighbors. Here is a simple rule for example:

  • if a cell is black, it remains black
  • if a cell is white, it becomes black if it has at least one black neighbor.

At each new generation, we apply this rule to all cells

Gen. 1
Gen. 2
Gen. 3
Gen. 4

Over several successive generations, we can superimpose the obtained lines and thus obtain an image that will be a graphic representation of the rule we considered.

Coding

We will use Python to automate the behavior of these one-dimensional cellular automata. Each cell will be represented for now by an element of an array, which will be equal to 0 or 1. Our goal by the end of the session will be to generate an image pixel by pixel using the PIL library (see previous practical work).
The above example will therefore give, initially, in a console output:

000010000
000111000
001111100
011111110

Matrix preparation

Like a digital image, we will store our successive generations in a matrix, that is to say for our example a list of character strings.

matrice = ["000010000"]
# Here, matrice is a list of lists. For now, it contains only one: ["000010000"]
# To add a generation, we will do:
matrice.append("000111000")

# We can visualize the obtained result:
print("print(matrice) : \n", matrice)

# Or, if we want to display the lines one above the other:
print("With a for loop:")

for generation in matrice:
    print(generation)

Applying the rules

To be able to apply the rule to a cell, we must look at the neighboring cells. We will therefore use a for loop with an iteration index.
For example, in the code below, to display the current cell and its neighbors:

generation = "000010000"

for i in range(len(generation)):
    print("current cell: ", generation[i])
    if i > 0:
        print("previous cell: ", generation[i-1])
    if i+1 < len(generation):
        print("next cell: ", generation[i+1])
    print("---------")
JDV1

Write a regle_A function that applies the rule provided above to the cell at index indice of the generation gen_actuelle.
The function takes two arguments: a character string representing the current generation, as well as an integer representing the index of the cell to study.
The function will be called as follows: regle_A(gen_actuelle, indice)
The function will return '0' or '1'.

Example, with the following generation:

Gen. 1

regle_A("100100",2)
The cell at rank 2 is the 0 just before the 1. It has a black neighbor, so it becomes black.
return "0"

JDV2

Write a nouvelle_gen function that takes as argument a character string containing the cells of generation n and returns a character string containing generation n+1.
We will apply the regle_A function to each cell of generation n to know what its state will be during the next generation (n+1).

Example, with the following generation:

Gen. n
Gen. n+1

nouvelle_gen("0010100" gen="n")
The function iterates over the string "0010100" and calls regle_A to apply the rule to each cell.
It stores the values as it goes, then returns the result.
return "0111110"

Complete program

JDV3

Thanks to your nouvelle_gen and regle_A functions, run the complete program.

# function definitions
# ...
# python
# python
# python
# ...

# program execution:
matrice = ["000010000"]
nb_generations = 4

for g in range(nb_generations):
    matrice.append(nouvelle_gen(matrice[g]))

Creating images

We can now use our code to generate an image that will contain the successive generations of our cellular automaton.
Here is the Python code that will do what's needed:

from PIL import Image

gen_1 = "00000000000000000000100000000000000000000"

matrice_pic = Image.new("1", (len(gen_1), len(gen_1)), color=1)

def regle_A(gen, indice):
    # function definition
    

def nouvelle_gen(generation):
    n_gen = ""
    for i in range(len(generation)):
        n_gen += regle_C(generation, i)
    
    return n_gen
    
    
def print_line(image, line):
    image.putdata(line)

# program execution:
matrice = [gen_1]

pxl_data = []

for g in range(len(gen_1)):
    matrice.append(nouvelle_gen(matrice[g]))
    
    for px in matrice[g]:
        pxl_data.append(1-int(px))
        
    print(matrice[g])


matrice_pic.putdata(pxl_data)
matrice_pic.show()

Other rules

Try the previous code with rules B and C below:

def regle_B(gen, indice):
    cur = int(gen[indice])
    if indice > 0:
        prev = int(gen[indice - 1])
    else:
        prev = 0
    if indice + 1 < len(gen):
        next = int(gen[indice + 1])
    else:
        next = 0

    if prev + next == 2 or prev + next == 0:
        return '0'
    else:
        return '1'

def regle_C(gen, indice):
    return str(1 - int(regle_B(gen, indice)))

To go further...

You can read this article from Science étonnante from which this practical work is inspired, and/or watch the associated video, on the YouTube channel of... Science étonnante!