= 100
taille = 0.4
probability
= matrix(0, taille + 2, taille + 2) # + 2 la bordure à gauche à droite, en haut et en bas.
grille 2:(taille + 1),2:(taille + 1)] = rbinom(taille^2, 1, probability)
grille[
image(grille, asp=1, xaxt='n', yaxt='n', ann=FALSE)
Le jeu de la vie
Le jeu et ses règles
Le jeu de la Vie (Game of Life) est un automate cellulaire — devenu un jeu de simulation mathématique — imaginé par John Horton Conway en 1970 https://fr.wikipedia.org/wiki/Jeu_de_la_vie.
Le jeu se déroule sur une grille à deux dimensions, théoriquement infinie, dont les cases — appelées « cellules », par analogie avec les cellules vivantes — peuvent prendre deux états distincts : « vivante » ou « morte ».
Une cellule possède huit voisines, qui sont les cellules adjacentes horizontalement, verticalement et diagonalement.
À chaque itération, l’état d’une cellule est entièrement déterminé par l’état de ses huit cellules voisines, selon les règles suivantes :
- Une cellule morte possédant exactement trois cellules voisines vivantes devient vivante (elle naît) ;
- Une cellule vivante ne possédant pas exactement deux ou trois cellules voisines vivantes meurt.
L’initialisation de la grille
Nous allons initialiser une grille en la remplissant aléatoirement avec des cellules vivantes (représentées par 1) et mortes (0). Pour simplifier la suite des calculs, nous ajoutons une bordure autour de la grille : cela permet d’éviter de gérer explicitement les effets de bord.
Pouvez-vous exécuter plusieurs fois ce code, en modifiant la valeur du paramètre probability, puis observer les résultats obtenus ? Cela vous permettra de mieux comprendre le comportement de la fonction rbinom et son effet sur la répartition des cellules vivantes (1) et mortes (0).
Programmer la vie
Ma première fonction : pour connaitre l’état futur d’une cellule
Réaliser une itération sur l’ensemble de la grille est une opération relativement complexe. Pour simplifier, nous allons commencer par nous concentrer sur une seule cellule : l’objectif est de calculer son état au prochain tour. La fonction que nous allons écrire devra donc récupérer le voisinage de cette cellule, puis en déduire son état futur.
<- function(i,j, grille){ # ligne i, colonne j
cellule_etat_n_plus_un = grille[i,j]
etat_cellule <- grille[(i-1):(i+1), (j-1):(j+1)]
voisinage <- sum(voisinage) - etat_cellule
nb_voisin if (etat_cellule==0){ # je teste si la cellule est morte (égale à 0)
if (nb_voisin==3){ # si oui, je teste si elle a trois voisins
return(1) # si oui je retourne 1 car elle est vivante
else { # si non,
}return(0) # je retourne 0
else { # si j'arive ici la cellule est vivante
}}if (nb_voisin %in% c(2,3)){# je teste si la cellule a deux ou trois voisins
return(1) # qu'est ce que j'affecte ?
else{
}return(0) # qu'est ce que j'affecte .
}
}
}
# je la lance de cette manière
cellule_etat_n_plus_un(52,18,grille)
[1] 1
Boucler pour trouver l’état futur de la grille
Maintenant que nous avons défini notre fonction principale, il est temps de voir comment l’utiliser sur toute la grille. Pour cela, nous allons créer une nouvelle fonction qui va parcourir la grille, ligne par ligne et colonne par colonne, et appliquer notre fonction principale à chaque case. Cette nouvelle fonction prendra donc en entrée une grille complète, et s’appuiera sur la fonction précédente pour effectuer le traitement souhaité à chaque étape.
<- function(grille){
grille_etat_n_plus_un = nrow(grille) - 2 # je déduis la taille directement depuis la grille (attention la variable est locale)
taille = matrix(0, taille + 2, taille + 2)
grille_n_plus_un for (i in 2:(taille + 1)){
for (j in 2:(taille + 1)){
= cellule_etat_n_plus_un(i, j, grille)
grille_n_plus_un[i,j]
}
}return(grille_n_plus_un)
}
# je la lance de cette manière
<- grille_etat_n_plus_un(grille)
grille_n_plus_un image(grille_n_plus_un, asp=1, xaxt='n', yaxt='n', ann=FALSE)
Et maintenant la vie
On crée une fonction qui applique autant de fois que souhaité la fonction d’itération précédente. Le nombre d’itérations à effectuer est défini par le paramètre N.
<- function(grille, N){
jeu_de_la_vie for (i in 1:N){
<- grille_etat_n_plus_un(grille)
grille
}return(grille)
}
<- jeu_de_la_vie(grille, 10)
resultat image(resultat, asp=1, xaxt='n', yaxt='n', ann=FALSE)
Le résultat est intéressant, mais il reste difficile de suivre visuellement l’évolution de la grille à chaque étape. Pour y remédier, nous allons créer une fonction qui génère une image à chaque itération. Cette option ne sera active que si le paramètre image_plot est défini sur TRUE (par défaut, il est sur FALSE pour ne pas générer trop d’images inutilement).
<- function(grille, N, image_plot=FALSE){
jeu_de_la_vie for (i in 1:N){
<- grille_etat_n_plus_un(grille)
grille if (image_plot){
png(filename=paste0("resultats/image", i ,".png"))
image(grille, asp=1, xaxt='n', yaxt='n', ann=FALSE)
dev.off()
}
}return(grille)
}
<- jeu_de_la_vie(grille, 100, image_plot = TRUE) resultat
Pour les plus hardcoreu.s.es, on utilise magick pour transformer les images en une gif animée :
## list file names and read in
library(magick)
Linking to ImageMagick 6.9.13.17
Enabled features: cairo, fontconfig, freetype, ghostscript, lcms, pango, raw, rsvg, webp, x11
Disabled features: fftw, heic
Using 29 threads
= file.info(list.files("resultats/", full.names = TRUE))
details = details[with(details, order(as.POSIXct(mtime))), ]
details = rownames(details)
imgs
<- lapply(imgs, image_read)
img_list
## join the images together
<- image_join(img_list)
img_joined
## animate at 2 frames per second
<- image_animate(img_joined, fps = 10)
img_animated
## view animated image
img_animated
## save to disk
image_write(image = img_animated,
path = "gameoflife.gif")
Un peu de poésie
Maintenant, tu vas appliquer ta fonction sur une grille particulière : un U (cf https://fr.wikipedia.org/wiki/Jeu_de_la_vie#Dimension_et_complexit%C3%A9 )
<- matrix(0, taille + 2, taille + 2)
grille 50,50] <- 1
grille[50,51] <- 1
grille[50,52] <- 1
grille[51,52] <- 1
grille[52,52] <- 1
grille[52,51] <- 1
grille[52,50] <- 1
grille[<- jeu_de_la_vie(grille, 110)
resultat image(resultat, asp=1, xaxt='n', yaxt='n', ann=FALSE)
Créer un band d’essai pour le jeu de la vie
La dernière étape consiste à créer une fonction capable de générer une grille aléatoire, en fonction d’une taille et d’une probabilité données. Une fois la grille initiale créée, la fonction lancera N étapes d’évolution.
<- function(probability, taille, N){
experience = matrix(0, taille + 2, taille + 2) # + 2 la bordure à gauche à droite, en haut et en bas.
grille 2:(taille + 1),2:(taille + 1)] = rbinom(taille^2, 1, probability)
grille[
return(jeu_de_la_vie(grille, N))
}
image(experience(0.2,100,150), asp=1, xaxt='n', yaxt='n', ann=FALSE)
image(experience(0.8,100,150), asp=1, xaxt='n', yaxt='n', ann=FALSE)
Bonus : est ce que l’on aurait pu vectoriser les calculs ?
L’exécution du programme est assez lente, principalement parce que nous utilisons des boucles pour parcourir la grille. Une question intéressante à se poser est la suivante : peut-on améliorer les performances en utilisant des opérations vectorisées, qui permettent de traiter les données plus efficacement, notamment en tirant parti des optimisations internes du langage ?
= grille[1:(taille),1:(taille)] + grille[2:(taille+1), 1:(taille)] + grille[3:(taille+2), 1:(taille)] +
somme 1:(taille),2:(taille+1)] + grille[3:(taille+2), 2:(taille+1)] +
grille[1:(taille),3:(taille+2)] + grille[2:(taille+1), 3:(taille+2)] + grille[3:(taille+2), 3:(taille+2)]
grille[image(somme, asp=1, xaxt='n', yaxt='n', ann=FALSE)