Concevoir et réaliser un programme
Déterminer le sujet et définir des objectifs
Avec les tableaux, les structures et les fonctions, il devient possible de faire des programmes assez importants éventuellement répartis sur plusieurs fichiers sources. Par exemple des versions simples de beaucoup de jeux classiques, Pacman, Casse-brique, Space Invaders, Tétris, etc. De tels programmes supposent une réflexion préalable : avec quelles variables et quelles fonctions et selon quel algorithme général je fais mon programme ? Mais dans le cas de programme que l’on imagine sur des sujets que l’on découvre, avant de pouvoir répondre définitivement à ces questions il est nécessaire de poser une hypothèse de départ et de se donner des objectifs à atteindre. Ces objectifs pourront évoluer au fur et à mesure du développement du projet et l’hypothèse de départ sera ou non corrigée en fonction des résultats obtenus.
L’idée ici est de réaliser un automate cellulaire 2D en mode console. Qu’est-ce qu’un automate cellulaire ? Quel fonctionnement mettre en œuvre ? Avec quelle structure de données ? Le premier point est de créer un espace de connaissance propre au projet sur lequel s’appuyer ensuite pour le réaliser.
1. Principe de l’automate cellulaire
Un automate cellulaire traduit le comportement d’un ensemble d’individus, de cellules, de...
Trouver une structure de données valable
Voilà pour le principe de l’automate mais comment en faire un programme ? Comment traduire ce processus en une suite d’instructions dans un langage pour la machine ?
Une fois l’idée sur laquelle partir est posée, le premier point est de réfléchir à une possibilité de structure de données. Sur quoi va s’appuyer le moteur de l’automate pour fonctionner ? C’est-à-dire quel type de données peut-on choisir pour coder l’automate ? Variables simples ? Structure ? Tableaux ?
En l’occurrence cela saute aux yeux ici et ce n’est pas très compliqué. Essentiellement il y a deux surfaces 2D de booléens, des matrices d’entiers feront très bien l’affaire, une pour le plan initial et une autre pour le plan résultant. La taille sera définie par deux macros constantes, ce qui donne :
#define TX 80
#define TY 50
int MAT[TY][TX];
int SAV[TY][TX];
Identifier les fonctions principales
Maintenant que nous savons à partir de quoi travailler, nos deux matrices, quelles sont les opérations à réaliser afin de mettre en œuvre le principe de fonctionnement que nous avons retenu ? C’est-à-dire quelles sont les opérations à faire et dans quel ordre ?
Il s’agit ici en s’appuyant sur nos deux matrices de repérer et d’imaginer un algorithme efficace pour le projet.
1) A priori la première étape est une étape d’initialisation :
-
Au début les deux matrices sont initialisées à 0. Quelques positions du plan initial, la matrice MAT sont mises à 1. Écrire une fonction d’initialisation.
2) Ensuite le moteur tourne en boucle et à chaque cycle il doit :
-
Afficher le plan initial.
-
Calculer la transition pour chaque position et sauver le résultat dans SAV.
-
Recopier la matrice SAV dans le plan initial MAT.
Il se dégage déjà quatre fonctions à écrire :
-
une fonction d’initialisation,
-
une fonction d’affichage du plan,
-
une fonction de calcul,
-
une fonction de recopie.
Initialisation, affichage et recopie sont simples. La fonction de calcul nécessite un petit zoom :
Calculer c’est passer en revue (boucle) toutes les positions du plan initial et :
1 - pour chaque position compter le nombre des positions voisines à...
Choisir le niveau des variables fondamentales
Maintenant nous avons une hypothèse de structure de données et une hypothèse des fonctions à écrire, il reste juste un point à décider : est-ce que les matrices sont des variables globales visibles de toutes les fonctions, ou sont-elles encapsulées localement dans le main(), ce qui nécessite une transmission via les paramètres ?
Quelle est la différence du point de vue du développement du projet ? Quels sont les avantages ou inconvénients de l’une et de l’autre ?
Si nos matrices sont globales, il n’y a pas besoin de les passer en paramètre et cela allège un peu l’écriture des fonctions. C’est un petit programme qui tient sur un seul fichier, aucune confusion n’est possible avec d’autres processus à l’œuvre dans le même programme. Un problème pourrait éventuellement survenir ultérieurement si le développement du projet portait à faire tourner simultanément plusieurs automates et gérer plusieurs plans initiaux. Dans ce cas, il serait préférable de pouvoir passer au moins le plan initial en paramètre.
Cependant, il y aurait aussi la possibilité de partir sur un tableau de matrices déclaré en global et de modifier un peu chaque fonction en ajoutant une boucle...
Écrire les fonctions
1. Fonction d’initialisation
La fonction d’initialisation est simple : mettre à 1 quelques positions choisies.
void init_matrice(void)
{
// quatre positions au centre qui sont mises à 1 (le reste est à 0)
MAT[TY/2][TX/2]=1;
MAT[TY/2+1][TX/2]=1;
MAT[TY/2][TX/2+1]=1;
MAT[TY/2+1][TX/2+1]=1;
}
Voici la même fonction avec possibilité de transmission de différentes matrices de même taille (TY et TX) en paramètre :
void init_matrice(int M[][TX])
{
M[TY/2][TX/2]=1;
M[TY/2+1][TX/2]=1;
M[TY/2][TX/2+1]=1;
M[TY/2+1][TX/2+1]=1;
}
2. Fonction d’affichage
La fonction d’affichage utilise les fonctions gotoxy() et textcolor(). Le principe est simple : parcourir la matrice et pour chaque position, si la valeur est 1 colorer le fond en rouge, si la valeur est 0 colorer en bleu et dans les deux cas afficher un espace.
void affiche(void)
{
int x,y ;
for (y=0; y<TY; y++){ // pour chaque position
for (x=0 ;x<TX ;x++){
gotoxy(x,y); // déplacer le curseur à la position
if(MAT[y][x]==1) // si valeur 1
textcolor(192); // couleur rouge en fond
else
textcolor(16); // sinon couleur bleu en fond
putchar(' '); // affiche un espace
}
}
}
/****************************************************************
****************************************************************/
void gotoxy(int x, int y)
{
HANDLE h=GetStdHandle(STD_OUTPUT_HANDLE); ...
Intégrer une librairie personnelle
Une librairie personnelle est un fichier .h, nommé aussi fichier d’en-tête ou header en anglais. Dans la perspective de programmes importants en taille, il est utile de transporter tout l’en-tête du fichier, c’est-à-dire les include, les macros, les variables globales, les définitions de type s’il y en a et les déclarations de fonctions dans une librairie personnelle. Ensuite, il suffit de faire un include de cette librairie dans chaque fichier C qui utilise des données qu’elle contient.
Le programme que nous venons de présenter est un petit programme qui ne nécessite pas de librairie. Rien n’empêche toutefois de lui en adjoindre une. La démarche sera la même quelle que soit la taille du programme.
Selon l’environnement de développement utilisé, créez un fichier .h si possible du même nom que le fichier .c auquel il correspond ou du nom du programme s’il n’y a qu’un .h pour le programme.
Ensuite, retirez du fichier .c ce qu’il y a en en-tête et mettez-le dans le fichier .h :
// retiré de autocell.c et mis dans autocell.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>
#include <windows.h>
#define TX 80
#define TY 50 ...
Répartir son code sur plusieurs fichiers C
Notre programme ne nécessite pas une décomposition en plusieurs fichiers C, mais nous pouvons le faire à titre d’exemple.
Décomposer son programme en plusieurs fichiers suppose une décomposition par unité de traitement. Cette décomposition doit avoir un sens : l’objectif est de s’y retrouver plus facilement dans le code. Pensez que vous écrirez des programmes de plusieurs milliers de lignes, voire plusieurs dizaines ou centaines de milliers de lignes. À cette échelle, les programmes deviennent comme des villes et il faut des plans très précis pour s’y retrouver. Il faut donc trouver une architecture judicieuse dès qu’il s’agit de décomposer son code en fichiers.
Pour notre programme par exemple, nous pouvons mettre ce qui concerne le calcul dans un fichier, ce qui concerne l’affichage dans un autre et l’initialisation dans un troisième fichier.
Cela rend plus lisible le projet et du coup plus simple son développement. Vous pouvez par exemple décomposer le projet en plusieurs thématiques et avoir plusieurs types d’affichage, plusieurs types de calcul et plusieurs initialisations possibles, le tout contrôlé par une interface dans le main().
Techniquement, pour avoir plusieurs fichiers, il suffit de créer ces fichiers dans son environnement de développement et de les intégrer dans le projet. Ensuite, il reste à y inclure sa ou ses librairies et à y écrire le code voulu.
1. Code réparti en quatre fichiers C
Notre automate se décompose maintenant en quatre fichiers C et chaque fichier C inclut la librairie unique automat.h :
Contenu du fichier calcul.c
#include "automat.h"
/****************************************************************
*****************************************************************/
void calcul(void)
{
int x, y, nb_voisins;
for (y = 1; y<TY - 1; y++){
for (x = 1; x<TX - 1; x++){
nb_voisins = compte_voisins(y, x);
if (nb_voisins <2 || nb_voisins>3)
SAV[y][x] = 0; ...
Mise en pratique : structuration d’un programme
1. Simulation d’un feu de forêt
En vous inspirant de l’automate cellulaire présenté, simuler un feu de forêt. Une fois que votre espace représentant la forêt est constitué, le feu commence par exemple sur un bord et se propage en passant d’arbre en arbre lorsque des arbres se touchent. Il s’arrête sinon. Ne prendre en compte que quatre directions : nord, est, sud, ouest. C’est un programme court, quatre fonctions maximum.
2. Tristus et rigolus
Nous sommes dans un monde qui se partage entre les "tristus" aux personnalités tristes et négatives, voyant toujours ce qui va mal et réactivant sans cesse pour rien les problèmes insolubes et les "rigolus", toujours de bonne humeur, un peu légers, rigolant pour pas grand-chose. Un tristus peut réussir à attrister un rigolus et celui-ci finit par devenir un tristus s’il n’arrive plus à rigoler. Un rigolus peut réussir à faire progressivement rigoler un tristus qui se transforme alors en rigolus.
Simuler un monde partagé entre les tristus (une couleur froide) et les rigolus (une couleur chaude). Nous verrons apparaître une ligne fluctuante de démarcation entre les deux sphères, ou plusieurs s’il y a plusieurs territoires.
3. Simulation d’une attaque de microbes dans le sang
Au microscope nous pouvons voir dans le sang d’un individu des microbes aux prises avec les globules du système immunitaire. Selon différents paramètres, la maladie s’étend ou ne s’étend pas. Faire une simulation simplifiée de ce processus en 2D.
4. Bancs de poissons, mouvements de populations
Trouver un moyen de simuler l’organisation collective du mouvement d’un ensemble de poissons.
5. Élection présidentielle
C’est la période précédant le deuxième tour de l’élection présidentielle, chacun se demande pour lequel des deux candidats il va voter.
Il y a :
-
ceux qui prennent l’idée dominante de leur entourage proche,
-
ceux qui au contraire se révoltent contre les idées de leurs proches et prennent le contre-pied de l’idée qui domine autour d’eux,
-
ceux qui sont indécis...