~~META:date created=2010-11-15~~
~~DISCUSSION~~
{{tag>programmation python}}
======Game of life======
Le "jeu de la vie" est un [[wp>Cellular_automaton|automate cellulaire]] simple imaginé par [[http://bitstorm.org/gameoflife|John Conwell]] en 1970.
Il est composé d'une grille dont chaque cellule peut être dans deux états : vivante ou morte, modifié au cours du temps selon les trois règles suivantes :
- Une cellule vivante ayant moins de deux voisines vivantes meurt de solitude.
- Une cellule vivante ayant plus de trois voisines vivantes meurt de surpopulation.
- Une cellule morte ayant exactement trois voisines vivantes vit.
Les voisines d'une cellule sont les huit cellules l'entourant : N, S, E, O, NE, NO, SE, SO.
=====Introduction=====
La programmation de cet automate est assez simple, il s'agit généralement de créer et manipuler un tableau de deux dimensions.
Ce travail - en python - vise à créer le moteur fonctionnel de l'automate (et son affichage en console) le plus optimisé possible... en moins de lignes possible.
=====Source=====
Ce programme utilise au maximum générateurs, listes et diverses syntaxes courtes, et un tableau d'une seule dimension parcouru par une seule boucle associé à une technique d'index relatifs permettant de l'analyser comme une grille en deux dimensions.
Le programme crée une grille de taille donnée, la remplit aléatoirement selon un taux de remplissage, la calcule et met à jour selon les règles de l'automate cellulaire puis l'affiche en console (en indiquant chaque cellule vivante par son nombre de voisins) quand l'utilisateur le demande, et permet de quitter le programme simplement.
Le code fait 13 lignes et 334 caractères.
from random import random
l=10
r=range(l*l)
n=(1,-1,l,-l,l+1,l-1,-l+1,-l-1)
g=list(int(random()*1.8) for i in r)
while not raw_input():
p = list(g)
for i in r:
s = sum(p[i+x] for x in n if 03): g[i]=0
print chr(32+p[i]*(16+s)),
if i+1 and not (i+1)%l: print
Appuyer sur Enter pour calculer un cycle, rester appuyé pour calculer en boucle.
Appuyer sur n'importe quelle touche et appuyer sur Enter pour quitter.
=====Explication=====
Je décrit ci-dessous le fonctionnement du programme, ligne par ligne.
====Préparation des variables====
from random import random
Importation du module d'aléatoire utilisé pour remplir initialement la grille.
l=10
''l'' comme **length** : définit la largeur (et longueur) de la grille en nombre de cases. Pour être visible sans problème en console, préférer un tableau inférieur à 50x50 cases. Le programme peut toutefois calculer sans problème un tableau de 600x600 cases par exemple (hors affichage).
r=range(l*l)
''r'' comme **range** : liste des index des cases de la grille utilisée pour la créer et la parcourir. La grille est de taille ''l*l'' afin de correspondre à une grille de deux-dimensions de côté ''l''.
Cette liste est pré-calculée pour une question de performance, cf. [[http://nliautaud.fr/wiki/articles/python_benchmark#while_et_for|python benchmark]].
n=(1,-1,l,-l,l+1,l-1,-l+1,-l-1)
''n'' comme **neighbours** : Cette liste décrit les index relatifs des huit voisins dans le tableau d'une seule dimension. Le processus est détaillé plus bas.
====Initialisation de la grille====
g=list(int(random()*1.8) for i in r)
''g'' comme **grid**.
Il s'agit d'un générateur de liste utilisant la syntaxe :
list(value for i in range)
C'est proche de la syntaxe courte de boucle :
[value for i in range]
Mais préférable pour des questions de performance. Cf. [[http://python.org/dev/peps/pep-0289|Generator Expressions]].
La valeur ajoutée à la liste est une manière d'obtenir aléatoirement soit ''0'' soit ''1'' : convertir en entier la valeur à virgule flottante aléatoire comprise entre ''0'' et ''1'' renvoyée par ''random()''.
int(random()*1.8)
''1.8'' est donc un "taux de remplissage" de la grille pouvant être manipulé entre ''0'' (''0*0 < résultat < 0*1'', donc grille toujours vide) et ''1.99'' (''1.99*0 < résultat < 1.99*1'', donc grille aléatoirement remplie).
====Exécution des cycles====
while not raw_input():
p=list(g)
Cette boucle permet de mettre à jour la grille en appuyant sur Enter (la condition d'exécution est vraie puisque ''raw_input()'' n'a rien renvoyé).
Pour stopper le programme, entrer n'importe quelle valeur puis appuyer sur Enter (la condition d'exécution est fausse puisque ''raw_input()'' a renvoyé une valeur).
''p'' comme **previous** est une copie de la grille permettant par la suite l'analyse de la grille dans son état précédent, non modifié, tout en modifiant l'état actuel.
====Analyse et modification de la grille====
for i in r:
On répète l'opération suivante pour chaque élément de ''r'', i correspondant donc tour à tour à l'index de chaque cellule de la grille.
===Somme des voisins===
Pour savoir le nombre de voisins vivants, il faut faire la somme de l'état des huit voisins.
s=sum(p[i+x] for x in n if 0
Il s'agit encore d'un générateur de liste créant la liste des états des huit voisins, intégré à la fonction ''sum()'' retournant directement la somme chacun des éléments de la liste.
Cette ligne est similaire à :
nbrsList = []
for x in n:
if 0 < i+x < l*l:
nbrsList.append(last[i+x])
s = sum(nbrsList)
On ajoute les index relatifs stockés dans ''n'' à l'index courant ''i'' pour déterminer l'index des voisins ''i+x''. L'état des voisin lors du cycle précédent est alors déterminé par ''last[i+x]''.
Par exemple ''n[0]'' est ''1'', l'index de ce voisin est ''i+1'' et son état ''p[i+1]'', ce qui correspond à la case suivante du tableau, donc au voisin de droite.
Description des index relatifs :
* ''1'' : case suivante du tableau, EST.
* ''-1'' : case précédente du tableau, OUEST.
* ''l'' : case située une longueur de ligne après, SUD.
* ''-l'' : case située une longueur de ligne avant, NORD.
* ''l+1'' : case située une longueur de ligne plus une case après, SUD-EST.
* ''l-1'' : case située une longueur de ligne moins une case après, SUD-OUEST.
* ''-l+1'' : case située une longueur de ligne plus une case avant, NORD-EST.
* ''-l-1'' : case située une longueur de ligne moins une case avant, NORD-OUEST.
==Bordures==
La condition impose que l'index du voisin soit bien dans le tableau (une cellule au bord de la grille va observer des cases qui n'existent pas), c'est à dire que son index soit supérieur à ''0'' et inférieur à la taille du tableau :
if 0
La syntaxe ''x < y < z'', peu courante dans d'autres langages, correspond à ''x < y and y < z''.
La technique de la première version de "[[#bordures_mortes|bordure morte]]" n'a pas été réutilisée car elle s'avère peu utile dans un tableau uni-dimensionnel n'ayant que deux bords à vérifier. Mais du coup les bords droite/gauche de la grille sont infinis (un objet sortant à droite réapparaît à gauche).
===Modification de la grille===
Les règles énoncées plus haut peuvent être décrites en deux règles générales de changement.
if not p[i] and s==3:g[i]=1
Si l'état de la cellule courante est ''0'' (morte), mais que la somme des voisins vivants est égal à ''3'', on change son état à ''1'' (vivante).
if p[i] and(s<2 or s>3):g[i]=0
Si l'état de la cellule courante est ''1'' (vivante), mais que la somme des voisins vivants n'est pas comprise entre ''2'' et ''3'', on change son état à ''0'' (morte).
Si la cellule ne correspond pas à ces règles, on ne change pas son état.
===Affichage===
print chr(32+p[i]*(16+s)),
Cette ligne affiche un espace pour les cellules mortes et le nombre de voisins pour les cellules vivantes, en générant le code ASCII en base 10 du caractère visé. Cf. [[wp>ASCII]] et [[http://pyref.infogami.com/chr|chr()]]
Si la cellule est morte, le calcul sera ''32+0*(16+s)'' donc toujours ''32'', qui est le code ASCII du caractère ''Espace''.
Si la cellule est vivante, le calcul sera ''32+1*(16+s)'' donc toujours ''48+s''. A ''48'' (le code du chiffre ''0'') est rajouté le nombre de voisins, ce qui donne ''49'' (le code du chiffre ''1''), ''50'' (le code du chiffre ''2''), etc...
if i+1 and not(i+1)%l:print
On va à la ligne pour les index multiplicateurs de la largeur de la grille afin d'afficher le tableau en deux dimensions.
=====Benchmark=====
Les temps ci-après concernent l'exécution du programme légèrement modifié afin d'indiquer une moyenne (sur 100 cycles) des temps de calcul sans affichage de la grille en console :
from time import time
t=time()
cycles = 100
from random import random
l=100
r=range(l*l)
n=(1,-1,l,-l,l+1,l-1,-l+1,-l-1)
g=list(int(random()*1.8) for i in r)
for bench in range(cycles):
p=list(g)
for i in r:
s=sum(p[i+x] for x in n if 03):g[i]=0
print str((time()-t) / cycles)
^ Taille ^ Nombre de cellules ^ Secondes par cycle ^ Cycles par seconde ^
| ''50'' | ''5.000'' | ''0.0068'' | ''147'' |
| ''100'' | ''10.000'' | ''0.0267'' | ''37.2'' |
| ''500'' | ''250.000'' | ''0.6934'' | ''1.67'' |
| ''600'' | ''360.000'' | ''1.0087'' | ''0.99'' |
=====Version précédente=====
Cette version utilise certaines syntaxes courtes, un tableau à deux dimensions et affiche l'état de toutes les cellules un certain nombre de fois.
19 lignes, les trois lignes d'affichage en console comprises.
from random import random
l = 500
rng = range(1, l+1)
d = (0,1, 0,-1, 1,0, -1,0, 1,1, 1,-1, -1,1, -1,-1)
w = [[0]*(l+2)]*(l+2)
for i in rng:
for j in rng: w[i][j] = int(random()*1.8)
for t in range(5):
w2 = list(w)
for i in rng:
for j in rng:
sum = 0
for y in range(0,len(d),2): sum += w2[i+d[y]][j+d[y+1]]
if w2[i][j] == 0 :
if sum == 3 : w[i][j] = 1
elif sum < 2 or sum > 3: w[i][j] = 0
print str(w[i][j]),
print
print
===Bordures mortes===
Un problème à résoudre pour l'optimisation du code était de trouver un moyen court et efficace d'éviter les cases inexistantes. Lorsqu'une cellule est au bord de la grille, elle n'a pas huit voisins, le processus doit donc s'assurer de ne pas demander l'état de cases en dehors du tableau.
Pour éviter l'ajout de conditions, lourdes car à calculer pour chaque case pour chaque cycle et prenant plusieurs lignes, il a été choisi d'ajouter au tableau de côté ''l'' une bordure d'une cellule d'état ''0''.
Le tableau réel est donc de côté ''l+2''...
[[0]*(l+2)]*(l+2)
...et le processus de mise à jour et l'affichage ne concerne que les case de ''(1, 1)'' à ''(l, l)'' :
range(1, l+1)
Toutes les cellules mises à jour ayant alors bien huit voisines et les bordures n'ayant aucune influence sur le résultat des règles car toujours définies à ''0'', il n'est plus besoin d'ajouter une quelconque condition durant le processus.
===Benchmark===
Cette version s'exécute (hors affichage en console) en ''0.9613'' secondes par cycle (1.04 cycles par seconde) pour une grille de 500x500, soit 250.000 cellules.