Modèle objet de PHP

Licence Informatique, semestre 6

Alexandre NiveauJean-Marc Lecarpentier — Ludovic Jean-Baptiste

Enseignement des technologies du Web

 

Modèle objet de PHP

Notes de cours

Travail personnel

Objectifs

On reprend l'exo Rule 110 que la majorité des étudiant·es n'avait pas eu le temps de commencer au précédent TP. Cependant, cette fois, au lieu de faire du code purement procédural, on l'implémente avec des outils de la POO.

Exercice 1 — Rule 110, un automate cellulaire élémentaire (version objet) #

Attention, tout le début de l’exercice est identique à celui du TP précédent, mais ensuite ce ne sont plus les mêmes questions !

Dans cet exercice on va programmer l'automate cellulaire élémentaire appelé « rule 110 » (un des systèmes les plus simples qui soit Turing-universel, c'est-à-dire capable de simuler n'importe quel calcul).

Contexte

Automate cellulaire élémentaire

On peut voir un automate cellulaire élémentaire comme la simulation d'un monde très simple. Ce monde est constitué de cellules, qui n'ont que deux états possibles, blanc ou noir (ou morte/vivante, ou vide/pleine, ou fausse/vraie… comme vous voulez). Elles sont organisées sur une ligne (le monde n'a donc qu'une seule dimension). En général on considère que les deux extrémités de la ligne se rejoignent — il s'agit donc plutôt d'un cercle, mais ce n'est pas très important ici.

À chaque pas de temps (ou à chaque « génération »), les cellules évoluent : une cellule blanche peut devenir noire et inversement, et ce, en fonction de son état et de l'état de ses deux cellules voisines, suivant une règle établie au départ. Un exemple de règle : une cellule se retrouve noire si, au pas de temps précédent, parmi elle et ses deux voisines il y avait un nombre impair de cellules noires (cette règle est appelée « rule 150 »). En fonction de la règle d'évolution, l'automate se comporte de manière drastiquement différente ; certaines règles donnent des résultats très répétitifs, d'autres complètement chaotiques.

Rule 110
illustration animée du fonctionnement de la règle 110
Illustration animée du fonctionnement de la règle 110. En haut, l'état du monde ; au milieu, la règle d'évolution ; en bas, la génération suivante. Source : Wikimedia Commons, CC-BY-SA

La règle 110 n'est pas très compliquée : une cellule ne peut se retrouver blanche que dans trois cas

  • si elle et ses voisines étaient toutes trois blanches
  • si elle et ses voisines étaient toutes trois noires
  • si sa voisine de gauche était noire, mais elle-même et sa voisine de droite étaient blanches

Cette règle donne des résultats très intéressants, car à la fois réguliers et chaotiques : de fait, l'automate correspondant est capable de simuler l'exécution de n'importe quel programme (en encodant le programme comme un état initial du monde, et en lisant le résultat dans un état considéré comme final).

Implémentation

Après ces éléments de contexte, passons à l'implémentation. On va écrire un script rule-110.php.
Représentation du monde

On va créer une classe WorldState, qui représentera un état courant du monde. En interne, le monde sera représenté par un tableau de booléens, true représentant une cellule noire (vivante, présente) et false une cellule blanche (morte, absente). Depuis l'extérieur de la classe, on ne pourra pas accéder au tableau lui-même : on pourra seulement vérifier si une cellule à une position donnée est vivante ou non. En particulier, on ne pourra pas modifier l'état des cellules — les instances de WorldState sont dites immutables.

  1. Créer une classe WorldState dans le script.
  2. Lui déclarer une propriété $cells, de visibilité privée.
  3. Le constructeur de la classe doit prendre un paramètre $nbCells, le nombre de cellules du monde (par défaut 100), et créer dans sa propriété $cells un tableau de booléens représentant les cellules, qui doivent toutes être mortes. Remarque : vous pouvez stocker $nbCells dans une propriété de la classe si vous voulez, mais ce n'est pas obligatoire, puisqu'on a accès à la longueur du tableau.
  4. Tester : créer une petite instance de WorldState et l'afficher avec var_export ou var_dump. A-t-elle bien les propriétés attendues ?
  5. Ajouter une méthode statique buildFixedWorld($nbCells), qui va construire une instance de WorldState, rendre vivantes quatre ou cinq cellules (mettez-les où vous voulez), et renvoyer cette instance. (Remarque : les méthodes de ce genre s'appellent des factory methods. On en ajoutera une autre plus tard.) Tester cette méthode de construction (de la même façon que précédemment).
  6. Ajouter une méthode isCellAliveAtPosition. Elle doit attendre un entier $position en paramètre, et renvoyer true si et seulement si la cellule à la position indiquée est vivante (c'est-à-dire noire). NB: la position correspond à l'indice dans le tableau. Si la position demandée n'existe pas, lever une exception. Tester sur des cellules censées être mortes, sur des cellules censées être vivantes, et pour des positions trop grandes ou trop petites : assurez-vous dans tous ces cas que le comportement obtenu est le bon.
  7. On veut pouvoir afficher le monde comme une chaîne de caractères, en utilisant des espaces pour les cellules blanches et un autre caractère pour les cellules noires (on pourra utiliser un #, ou le caractère unicode « FULL BLOCK, █ »). Implémenter la méthode __toString afin de remplir cet objectif, et vérifier que l'affichage d'une instance construite par buildFixedWorld est bien cohérent.
Évolution
Pour implémenter l'évolution, on commence par une version simple mais peu flexible, qu'on rendra plus générale dans la suite et au prochain TP.
  1. Écrire une fonction compute_next_state_rule110($leftAlive, $selfAlive, $rightAlive), qui renvoie l'état d'une cellule à la prochaine génération, en fonction de son propre état (paramètre $selfAlive) et de celui de ses deux voisines ($leftAlive et $rightAlive).
  2. Ajouter une méthode computeNextGeneration() à la classe WorldState, qui renvoie une nouvelle instance de WorldState représentant le prochain état du monde, après application de la règle d'évolution (grâce à compute_next_state_rule110). Pour simplifier, on ignorera les cellules aux extrémités du tableau (on considère que le monde est une ligne, et pas un cercle).
  3. Écrire une classe Simulator, qui a comme propriété une instance de WorldState (passée à son constructeur), et comme méthode displayEvolution($nbGenerations), qui affiche $nbGenerations successives du monde.
Pour tester le programme, construire un monde et un simulateur, et lui faire afficher 50 générations. Vérifiez que ça marche ! (La règle est-elle bien respectée à vue de nez ? Est-ce que ça ressemble aux dessins sur Wikipédia ?)

Améliorations

Maintenant que le programme fonctionne, on va l'améliorer.

Monde aléatoire
  1. Notre factory method buildFixedWorld n'est pas terrible. Créer une deuxième méthode statique dans WorldState buildRandomWorld, qui remplit le monde aléatoirement. (manuel de la fonction rand)
  2. Ajouter un paramètre $ratioAlive à buildRandomWorld, pour contrôler la proportion moyenne de cellules vivantes. Par exemple, si on passe 0.75, alors chaque cellule doit avoir une probabilité 0.75 d'être noire.
Paramètres du script

Pour pouvoir facilement observer les résultats, on va passer des paramètres au programme lui-même. Dans PHP, la variable $argv est un tableau contenant chacun des arguments au script (ainsi que le nom du script, dans la case d'indice 0).

  1. Faire en sorte qu'on puisse passer le ratio de cellules noires, la taille du monde, et le nombre de générations voulus en paramètres du script. Exemple d'appel : php rule-110.php 0.2 150 40
  2. Faire en sorte que ces paramètres soient optionnels (avec des valeurs par défaut convenables).
Testez un peu avec des ratios bas, très bas, hauts…
Monde en cercle

Le fait qu'on ignore les extrémités du monde n'est pas très satisfaisant (et cela peut générer des effets de bord (au sens propre)). On va faire en sorte que le monde soit circulaire, c'est-à-dire que la voisine de gauche de la première cellule soit en fait la dernière (et réciproquement).

  1. Modifier la méthode isCellAliveAtPosition pour qu'on puisse lui passer des positions qui sortent du tableau (notamment des positions négatives). Par exemple, la position -1 doit correspondre à la dernière case du tableau.
  2. Modifier la méthode computeNextGeneration pour qu'elle tire parti de cette modification, et considère bien le monde comme un cercle (c'est-à-dire que les cases aux deux extrémités du tableau doivent être considérées comme voisines).
D'autres règles

Que faudrait-il changer si on voulait implémenter une autre règle que la règle 110 ? Notre code n'est pas très extensible de ce point de vue. On doit pouvoir choisir la règle sans modifier le code de la classe WorldState. Commencez par y réfléchir. Comment feriez-vous ?

Il y a plusieurs solutions ; dans le prochain TP on verra une solution « orientée objet ». Ici on implémente une solution plus « fonctionnelle ».

  1. Ajouter à la méthode computeNextGeneration un paramètre $compute_next_state, qui va contenir une fonction. Modifier ensuite le code pour que ce soit cette fonction qui soit appelée au lieu de compute_next_state_rule110. Vérifier que ça marche toujours si on passe 'compute_next_state_rule110' comme paramètre (NB : pour passer comme paramètre de fonction, une fonction qui existe déjà, il faut passer son nom sous forme de chaîne de caractère.)
  2. Implémenter une autre règle, par exemple la règle 184, et afficher le résultat. Voir ici des dessins d'un bon nombre d'autres règles.
  3. Optionnel : écrire une fonction create_rule($rule_number) qui prend un nombre entre 0 et 254 en paramètre, et renvoie une fonction de trois paramètres qui implémente la règle ayant ce numéro. Par exemple, create_rule(110) devrait renvoyer une fonction qui fait exactement la même chose que notre fonction compute_next_state_rule110. La signification du numéro peut être trouvée sur Wikipédia.
Si vous avez tout fait…
Ce qui suit est réservé à celles et ceux qui auraient pris de l'avance sur le TP ! Si vous avez des difficultés, pas la peine de vous lancer là-dedans.

Utiliser les fonctions de la librairie GD pour créer une image PNG du résultat de l'évolution de l'automate, avec des pixels (ou des carrés) blancs/noirs pour les cellules.

Vous aurez notamment besoin de imagecreate, imagecolorallocate, imagesetpixel, et imagepng.