Buts: Permettre à des sources de nutriments de croître spontanément dans la boite de culture, en fonction de la température.
Avant de nous pencher plus en détail sur la modélisation des nutriments, un petit détour sur la question de comment représenter les paramètres caractéristiques d'une simulation s'impose.
Il peut en effet y en avoir de très nombreux. Par exemple, pour les nutriments: quelle représentation graphique choisir, quelle quantité de nutriment maximale et minimale peut-on créer, dans quelles conditions de température la source de nutriment peut croître dans la boite de culture etc.
Il est évidemment souhaitable que les valeurs de ces paramètres fassent partie d'un fichier de configuration (plutôt que de les coder "en dur" dans le programme). Ceci nous permettra de faire varier à souhait les exécutions en modifiant simplement le fichier de configuration et donc d'expérimenter facilement de nombreuses situations d'expérimentation différentes.
Il existe plusieurs formats standard pour la modélisation de données (XML, JSON, YAML etc), qui d'ailleurs ne sont pas uniquement utilisés dans le contexte de la programmation.
Pour ce projet, nous avons pris le parti d'utiliser le format JSON pour répertorier les paramètres caractéristiques de la simulation.
Le fichier de configuration associée à cette étape du projet se trouve est res/app.json.
Nous vous fournissons dans le répertoire src/JSON toutes les fonctionnalités nécessaires à la récupération des données depuis un tel fichier.
Les paramètres sont étiquetés de manière hiérarchique au moyen de chaîne de caractères qui permettent de les décrire.
Par exemple, les paramètres liés aux nutriments sont exprimés sous cette forme
"nutrients" : {
"growth" : {
"speed" : 5,
"max temperature" : 60,
"min temperature" : 30
},
"quantity" : {
"max" : 50,
"min" : 40
},
"texture" : "foodA.png"
}
ce qui se lit comme :
La fonction getAppConfig() de Application.hpp permet d'accéder à la valeur d'un paramètre donné. Il faudra étiqueter la valeur cherchée par toutes les chaînes de caractères le décrivant, au moyen d'opérateur d'indexation .
Par exemple, pour accéder à la température maximale permettant la croissance d'une source de nutriments, on écrira :
getAppConfig()["nutrients"]["growth"]["max temperature"]
La valeur obtenue doit enfin être convertie dans le type attendu au moyen de méthodes de conversion fournies (toDouble() pour les doubles, toString() pour les string, toBool pour les booléens etc.) Par exemple:
getAppConfig()["nutrients"]["texture"].toString()(car la texture est censée être un string) ou
getAppConfig()["nutrients"]["growth"]["max temperature"].toDouble()(car la température est censée être un double)
getAppConfig()["nutrients"]["growth"]retourne un tableau contenant les valeurs associée à "speed", "max temperature" et "min temperature" ({5, 60, 30} dans le fichier app.json fourni)
Notez aussi que pour raccourcir l'écriture de l'accès aux paramètres, il est possible de définir des constantes qui y sont relatives dans le fichier Config.hpp.
Par exemple, un raccourci a été créé pour le paramètre getAppConfig()["simulation"]["time"]["factor"] qui peut s'écrire getShortConfig().simulation_time_factor" (regardez comme cela et codé dans Config.[hpp][cpp]).
./nutrientTest appTest.json
permettra de lancer le programme application en utilisant un fichier appTest.json comme fichier de configuration.
Si l'on ne met rien, il s'agira du fichier dont le nom correspond à la constante DEFAULT_CFG définie dans Config.hpp. Dans le matériel fourni, il s'agit de app.json, mais libre à vous de le modifier.
Nous pouvons maintenant retourner au centre de nos préoccupations.
Il vous est maintenant demandé de compléter la classe Nutrient qui va servir à modéliser les sources de nutriments consommables par les bactéries. Pour simplifier nous supposerons pour commencer qu'elles mangent toutes la même chose !
[Question Q2.6] Comme quasiment toutes les entités à modéliser, nos sources de nutriments seront aussi des contours circulaires. Comment proposez-vous d'utiliser la classe fournie CircularBoundary (dans le répertoire Lab/) pour modéliser cet aspect ? Répondez à cette question dans votre fichier REPONSES.
La classe Nutrient sera spécifiquement caractérisée par une quantité (de nutriments disponible). Vous partirez de l'hypothèse qu'une source de nutriment a connaissance de la boite à laquelle elle appartient (pour simplifier, de son indice dans l'ensemble des boites du laboratoire).
[Question Q2.7] A quoi cela peut-il bien servir d'utiliser le type Quantity plutôt que tout simplement un double ? Répondez à cette question dans votre fichier REPONSES.
Vous doterez la classe Nutrient :
getConfig()["quantity"]["max"]plutôt que
getAppConfig()["nutrients"]["quantity"]["max"]
Un certain nombre de textures sont fournies dans le répertoire res.
La méthode Application::getAppTexture permet de charger une texture de ce répertoire dans un objet de type sf::Texture (à propos du texturage vous pouvez revoir l'exemple du tutoriel).
Par ailleurs, le fichier fourni Utility.[hpp][cpp] vous offre une méthode buildSprite qui vous permet de construire un objet texturé à afficher.
Le code pour l'affichage d'une source de nutriments devrait donc ressembler à ceci :
auto nutrientSprite = buildSprite(centre, une_taille_graphique, texture));
target.draw(nutrientSprite);
auto const& texture = getAppTexture(getConfig()["texture"].toString());
Certains affichages vous seront demandés en plus dans le projet, afin de faciliter certains tests. Votre programme devrait donc pouvoir fonctionner en mode "debugging" ou pas.
Le fichier de configuration contient un paramètre "debug" que vous pouvez utiliser à cet effet (et le fichier fourni Application offre la fonction isDebugOn()).
Il vous est donc demandé de compléter l'affichage de la source de nutriments de sorte que si le mode debugging est activé (paramètre associé à l'étiquette "debug" valant true), la quantité de nutriments associée à la source s'affiche aussi.
auto const text = buildText(...); target.draw(text);
Pour voir vos méthodes d'affichage à l'oeuvre, il ne reste plus qu'à permettre l'ajout de nutriments à la boite de culture.
Pour ce faire, complétez la méthode CultureDish::addNutrient de sorte à ce qu'elle ajoute un Nutrient* à la collection de sources de nutriments de la boite de culture. Ce dernier ne sera ajouté que si sa position lui permet de se trouver à l'intérieur de la boite (son contour circulaire est contenu dans celui de la boite). Dans ce cas, la méthode addNutrient retournera true pour indiquer que le nutriment a bien trouvé sa place dans la boite. Dans le cas contraire, aucun nutriment ne sera ajouté et addNutrient retournera false. Pour effectuer les tests nécessaires, vous utiliserez les méthodes programmées dans CircularBoundary en veillant à une bonne modularisation de votre code!
[Question Q2.8] En examinant le code du test fourni pour cette partie (src/Tests/GraphicalTests/NutrientTest.cc) quelle méthode doit-elle être ajoutée à la classe Laboratory pour permettre l'ajout de nutriments à la boite de culture courante lorsque l'on appuie sur la touche 'N', une fois la ligne en question décommentée ? Quelle méthode existante doit être modifiée pour permettre le dessin des sources de nutriments nouvellement ajoutées? Répondez à ces question dans votre fichier REPONSES puis programmez cette méthode.
Pour tester vos affichages, décommentez dans le ficher tests NutrientTest les lignes de code correspondant à la programmation des touches 'N' et 'T'.
Lancez alors le test comme précédemment en ayant pris soin de mettre l'application en mode «debugging». Ce mode peut-être activé/désactivé au moyen de la touche 'D'.
En plaçant votre souris sur une position de l'environnement et en cliquant sur 'N' vous devriez y voir apparaître une source de nourriture.
La touche 'T' permet de prélever de la dernière source de nutriments créée une quantité de 15. En appuyant successivement sur 'T' vous devriez voir la quantité de nutriments décroître jusqu'à atteindre 0. Appuyer sur la touche 'T' doit alors rester sans effet.
Vous prendrez soin de vérifier que les tentatives d'ajout de nutriments trop proches de la bordure de la boite ou en dehors de celle-ci ne produisent aucun effet.
Vérifiez aussi que les quantités ne s'affichent pas textuellement lorsque le mode «debugging» est désactivé.
Un exemple de déroulement de test est fourni dans la petite vidéo suivante:
| ← en appuyant sur 'T' la dernière source de nutriments ajoutée est décrémentée de 15 jusqu'à atteindre la valeur 0. |
"simulation" : {
....
"background" : "sand.png",
"debug background" : "sand.png",
"size" : 800
}
Nous avons jusqu'ici fait appel aux méthodes drawOn pour afficher graphiquement les éléments de notre simulation qui demeurent, pour l'heure, figés et statiques.
Nous allons nous intéresser maintenant à la facette update pour commencer à insuffler une peu de dynamique à notre simulation.
Commencez compléter dans votre classe Nutrient, la méthode void update(sf::Time dt) permettant de calculer l'évolution du nutriment après écoulement d'un pas de temps dt.
L'évolution consistera à le faire croître d'une quantité:
auto growth = speed * dt.asSeconds();
où speed est la valeur double du paramètres liés aux étiquettes ["nutrients"]["growth"]["speed"] dans le fichier de configuration.
Les contraintes liées à la croissance sont les suivantes :
[Question Q2.9] Sachant que la classe Laboratory ne donne pas d'accès à ses CultureDish via des getters. Comment permettre à Nutrient::update de connaître la température de sa propre boite de culture (ou d'en connaitre les limites pour ne pas en déborder) en utilisant Application::getAppEnv, ? Répondez à cette question dans le fichier REPONSES et programmez ce qui est nécessaire.
Contrôle de la température
Tout comme pour la sélection de la boite courante, la température de la boite courante est un paramètre contrôlable directement depuis l'interface graphique. Faites en sorte que la valeur ajoutée à la température ou qui en est supprimée soit celle associée à l'étiquette ["culture dish"]["temperature"]["delta"] et faites en sorte que la température se situe entre les deux bornes données par les étiquettes ["culture dish"]["temperature"]["min"] et ["culture dish"]["temperature"]["max"]. Vérifiez ensuite que vos méthodes Lab::increaseTemperature et Lab::decreaseTemperature sont correctement codées lorsque vous sélectionnez la température comme paramètre à contrôler. Vérifiez aussi que les températures des différentes boites peuvent chacune être contrôlées de manière indépendante (chaque boite peut avoir sa propre température).
Complétez enfin la méthode Lab:resetControls de sorte à de qu'elle permette de ramener la température de la boite courante à la température par défaut (celle donnée à la construction de la boite). La méthode reset devra aussi provoquer cet effet (sans duplication de code!).
Pour tester vos derniers développements, lancez le test comme précédemment et créez des sources de nutriments en appuyant sur la touche 'N'. Ils devraient toujours être figés ! La raison est que les nutriments ne croissent qu'à partir de la valeur 30 pour la température (regardez le fichier app.json):
"nutrients" : {
"growth" : {
"speed" : 5,
"max temperature" : 60,
"min temperature" : 30
},
Augmentez la température avec les contrôles que vous avez programmé, vous devriez voir les sources de nourriture commencer à croître à partir de la valeur 30.
| ← La température est augmentée dans la première boite, puis on passe à la seconde où on augmente la température puis on la diminue pour figer la croissance. Dans la troisième boite, on laisse la température stable. En naviguant d'une boite à l'autre on voit des contenus et des températures différents. | |
|
[Video : Croissance des nutriments en fonction de la température & cultures dans plusiseurs boites] |
Vérifiez que la touche 'R' a bien pour effet de vider la boite courante et de réinitialiser sa température à la valeur par défaut.
Vérifiez enfin que chaque boite peut avoir sa propre composition et sa propre température et que de passer de l'une à l'autre préserve ces contenus.
Pour comprendre enfin les effets de la touche 'C', alors que votre programme est encore ouvert, changez la valeur de ["simulation"]["time"]["factor"] dans le fichier de configuration (par exemple faites la passer à 5). Appuyer sur 'C' pour recharger cette nouvelle valeur dans la simulation. Si vous créez des nutriments avec les bonnes conditions de température vous devriez les voir croître beaucoup plus vite. Tous les paramètres de simulation peuvent ainsi être modifiés en cours de d'exécution du programme, sans avoir à relancer le programme.
Il est souvent nécessaire en programmation de remettre en question des choix antérieurs et d'apporter des modifications utiles au code existant ("code refactoring"). C'est d'autant plus nécessaire dans le cadre de ce projet, que vous ne connaissez pas forcément toutes les notions utiles au moment d'aborder telle ou telle partie.
Si vous avez correctement fait les choses la semaine passée, votre classe CircularBoundary a été programmée en déclarant comme privés les attributs et comme publiques les méthodes.
Pour améliorer cette conception, apportez les modifications garantissant que: