Course Website
Cours : Jeudi 8h15–9h00, CM 1 4
Exercices : Jeudi 9h15–11h, INF 3, BC 07-08
Plus exactement, voici toutes les consignes officielles qui s’appliquaient à ce midterm :
__init__ “standard” (avec des paramètres qui correspondent aux attributs, et où l’init ne fait qu’éventuellement appeler super puis assigner les attributs), vous pouvez simplement écrire “... # init standard” dans votre définition de classe.@DC pour @dataclass, et @DCF pour @dataclass(frozen=True).
Pour les définitions d’Enum, il n’est pas nécessaire d’écrire les = auto().import, ni de documentation, ni de __str__/__repr__, ni de tests.cast, du type Any ou de # type: ignore sera pénalisé (si ça ne vous dit rien, tant mieux).Nous allons modéliser la gestion des ressources et prérequis du jeu de société Terraforming Mars. Bien qu’il y ait eu une série d’exercices parlant de ce jeu, nous modélisons ici de tout autres aspects ; il n’y a donc pas de lien.
Dans ce jeu, chaque joueur ou joueuse incarne une corporation. Une corporation possède des forêts et des ressources. Il y a 6 types de ressources différentes dans le vrai jeu, mais nous allons nous limiter à 4 : l’argent, les plantes, l’énergie et la chaleur. Pour chaque type de ressource, une corporation possède une certaine quantité, ainsi qu’une production. À chaque tour, pour chaque ressource, la quantité est augmentée en fonction de la production. Par exemple, si une corporation a 6 plantes en quantité, et une production de 3 plantes, au début d’un tour elle passera à 9 en quantité, mais restera à une production de 3. Il n’y a pas de “production” de forêts ; c’est seulement le cas pour les ressources. Toutes ces données (nombre de forêts, quantités et productions de ressources) vont évoluer au cours de la partie. Elles sont toutes des valeurs entières.
Une subtilité concerne l’énergie : au début d’un tour, et avant d’appliquer les productions, toute la quantité d’énergie d’une corporation est transférée en quantité de chaleur. La corporation perd donc toute sa quantité d’énergie, mais gagne autant de quantité de chaleur.
Le jeu possède 3 paramètres globaux : le pourcentage d’oxygène dans l’atmosphère, la température ambiente, et le nombre d’océans. Ces valeurs sont entières et vont également évoluer au cours de la partie.
Les corporations vont jouer des cartes pour améliorer leur économie. Les cartes ont des prérequis (requirements) qui dictent des conditions requises pour pouvoir les jouer. Elles ont aussi une série d’actions qui vont influer sur le jeu : par exemple, modifier les quantités et/ou productions de ressources.
Il y a un très grand nombre de cartes différentes dans ce jeu (plus de 200, rien que dans le jeu de base).
On ne veut pas toutes les coder dans le programme, mais plutôt les charger depuis un fichier externe.
La structure de ce fichier pourrait par exemple être du YAML, mais ce n’est pas le sujet.
Ce qui nous intéresse, c’est comment représenter les prérequis et les actions des cartes.
On les représente sous forme de texte dans le fichier.
On utilise des " dans cette description pour représenter une chaîne de caractères, imitant la notation Python, mais ces guillements ne font pas partie de ce qu’on écrirait dans le fichier.
Pour les prérequis :
"OK" : toujours vrai (pour les cartes qui n’ont pas de prérequis)"temperature <= -8" : la température globale doit être maximum -8"oxygen >= 5" : la taux d’oxygène doit être minimum 5"oceans >= 3" : le nombre global d’océans doit être minimum 3"player_forests <= 10" : la corporation qui joue la carte doit avoir maximum 10 forêts"(prérequis1) AND (prérequis2)" : les deux prérequis doivent être vrais"(prérequis1) OR (prérequis2)" : au moins un des deux prérequis doit être vraiLa structure est récursive : on peut avoir une condition du nombre d’océans dans un AND, lui-même dans un OR.
Les comparaisons ne sont pas récursives ; elles ont toujours une “variable” à gauche et un entier littéral à droite.
Les seuls opérateurs possibles sont <= et >=.
Les variables peuvent être un des 3 paramètres globaux (temperature, oxygen ou oceans) ou le nombre de forêts de la corporation qui veut jouer la carte (player_forests).
Pour les actions, on a une liste de chaînes de la forme suivante :
"plants amount +3" : augmente la quantité de plantes de 3 (évidemment, la ressource et le nombre peuvent varier)"money amount -2" : diminue la quantité d’argent de 2"energy production -1" : diminue la production d’énerge de 1"heat production +2" : augment la production de chaleur de 2"build forest" : construit une forêtDe manière générale, on a donc soit "resource amount/production +/-x", soit "build forest".
Certaines cartes ont aussi une action spéciale ; il y a plusieurs dizaines d’actions spéciales dans le jeu. On ne les modélisera pas, mais pensez au fait qu’elles devraient exister dans le futur, lorsque vous concevez votre solution. Autrement dit, les actions possibles ne sont pas bornées.
Lexique anglais, dans l’ordre d’apparition des mots en italique : corporation, forest, resource, money, plants, energy, heat, amount, production, oxygen, temperature, oceans, requirement, action.
Remarque générale 1 : à tout moment, vous pouvez décider de “revenir en arrière” pour ajouter des méthodes ou des attributs à vos classes, si vous le désirez. Pensez à laisser de la place dans vos classes, au cas où. Vous pouvez toujours aussi définir plus de classes, de méhodes et de fonctions que demandé, bien entendu ! Les points par question sont donc un peu flexibles.
Remarque générale 2 : quand on dit qu’une fonction doit accepter un certain type de données $B$, il est permis de l’écrire de sorte qu’elle accepte en fait des valeurs de type $A$ à la place, si $B <: A$.
Remarque générale 3 : quand on vous demande d’écrire une fonction, vous pouvez définir une méthode à la place, et inversement. On ne fait pas la différence.
Remarque générale 3 : vous avez spécifiquement le droit de “copier-coller” du code entre la gestion des AND et des OR, même une classe entière si besoin.
Il est même encouragé de ne pas écrire le code gérant les OR.
Vous pouvez écrire des “, ou une ligne horizontale, ou tout autre moyen d’indiquer explicitement qu’on fait “la même chose” pour OR que pour AND.
Ou ne rien noter du tout.
La méthode str.split sépare une chaîne en sous-chaînes séparées par des espaces.
Par exemple, pour s = "hello world", on a s.split() == ["hello", "world"].
Voici aussi un rappel d’un usage des fonctions d’expressions régulières :
import re
date_re = re.compile('([0-9]+)/([0-9]+)/([0-9]+)')
result = date_re.fullmatch('22/5/2025')
if result:
print(result.group(1)) # '22'
print(result.group(2)) # '5'
print(result.group(3)) # '2025'
Il est possible, ou pas, que ces fonctions vous servent dans cet examen.
1. Définissez (en Python et/ou sous forme de diagramme) des types de données pour représenter une corporation et tout ce qu’elle possède : forêts, quantités de ressources et productions de ressources.
Définissez aussi un type de données pour représenter les 3 paramètres globaux (température, oxygène et océans).
2. Définissez et implémentez une fonction start_turn qui accepte une corporation, et applique les changements de début de tour à ses ressources :
3. Définissez un type de données Action pour représenter une action, avec potentiellement des types de données auxiliaires.
Vous ne devez pas modéliser les actions spéciales, mais votre conception doit supporter l’ajout d’actions spéciales dans le futur.
Attention : on parle bien de représenter/modéliser les actions.
On veut une forme structurée, pas une simple str.
Au besoin, relisez la question 5 pour clarifier.
4. Définissez et implémentez une fonction apply_actions qui accepte la corporation dont c’est le tour, et une list[Action].
Elle doit appliquer ces actions.
5. On rappelle qu’on veut lire les actions des cartes depuis un fichier.
On vous épargne la lecture du fichier en tant que telle, ainsi qu’en extraire la structure.
On suppose qu’on a déjà extrait une list[str] dont les éléments sont la représentation textuelle des actions, telle que définie dans le scénario.
Définissez et implémentez une fonction load_actions qui accepte cette list[str] et renvoie une list[Action] correspondante, dans le modèle que vous avez défini à la question 3.
Si une des str ne correspond pas à une description d’action valide, votre fonction doit déclencher une exception de type ValueError ou KeyError.
6. Définissez un type de données Requirement pour représenter un prérequis, avec potentiellement des types de données auxiliaires.
Attention : on parle bien de représenter/modéliser les prérequis.
On veut une forme structurée, pas une simple str.
Au besoin, relisez la question 9 pour clarifier.
Conseil : relisez les questions 7 et 8 avant de commencer cette question. On peut supposer qu’il y aura, dans le futur, d’autres bizarreries telles que celle décrite à la question 8.
Rappel de la Remarque générale 3 : vous avez spécifiquement le droit de “copier-coller” du code entre la gestion des AND et des OR, même une classe entière si besoin.
Il est même encouragé de ne pas écrire le code gérant les OR.
Vous pouvez écrire des “, ou une ligne horizontale, ou tout autre moyen d’indiquer explicitement qu’on fait “la même chose” pour OR que pour AND.
Ou ne rien noter du tout.
7. Définissez et implémentez une fonction test_requirement qui accepte un Requirement, une corporation dont c’est le tour, et l’état des paramètres globaux.
Elle doit renvoyer True si le prérequis est satisfait par la corporation et les paramètres globaux, et False sinon.
8. Une des innombrables cartes du jeu permet de relâcher partiellement les prérequis pour d’autres cartes.
Elle permet d’élargir de 2 unités les valeurs possibles pour les paramètres globaux du jeu (température, oxygène et océans).
Par exemple, si un prérequis demande normalement “au moins 3 océans”, la présence de cette carte relâche ce prérequis à “au moins 1 océan”.
Si un prérequis demande “maximum 2 de température”, elle devient “maximum 4 de température” (remarquez que ça ne va pas dans le même sens en fonction de l’opérateur de comparaison).
Si un prérequis demande “minimum 1 forêt” appartenant à la corporation qui joue, il reste inchangé (les forêts ne sont pas un des paramètres globaux).
Évidemment, si un prérequis est une combinaison avec AND ou OR, cela s’applique à tous les “sous-prérequis”.
Définissez et implémentez une fonction relax_requirement qui accepte un Requirement $A$ et renvoie un autre Requirement $B$.
Le prérequis $B$ doit représenter la version relâchée de $A$, suivant la spécification ci-dessus.
Remarque : la question 9 a été transformée en bonus.
9. On rappelle qu’on veut lire les prérequis des cartes depuis un fichier.
De nouveau, on suppose qu’on a déjà extrait une unique str qui est la représentation textuelle d’un prérequis, telle que définie dans le scénario.
La présence des AND et OR rend l’analyse de cette chaîne très compliquée, à cause de sa nature récursive.
On va se servir d’une “bibliothèque fictive” pour transformer une représentation textuelle d’un prérequis (str) en un Requirement, tel que vous l’avez défini à la question 6.
Évidemment, la bibliothèque ne connaît pas Requirement.
Il faut donc lui donner une sorte de “configuration” qui lui permette de créer les bouts de votre représentation.
Cette configuration se fera sous forme de fonctions que l’on transmet à la bibliothèque.
Cette bibliothèque fournit donc une seule fonction parse, qui prend les arguments suivants :
str qui est la représentation textuelle ;Requirement qui représente un prérequis “OK” ;Requirement de comparison de variable, à partir de 3 str : le nom de la variable, le nom de l’opérateur (">=" ou "<="), et la valeur à laquelle la comparer (qui est donc une chaîne représentant un entier, comme "-5") ;Requirement pour former un Requirement AND ; elle prend donc deux Requirement, et renvoie un Requirement.Requirement pour former un Requirement OR ; elle prend donc deux Requirement, et renvoie un Requirement.La définition de parse ressemblerait donc à ceci (où les ? devraient être remplacés par de vrais types) :
def parse(text: str, ok: ?, make_comparison: ?, make_and: ?, make_or: ?) -> ?:
... # code within the library; not shown
Exemple : si la chaîne passée en paramètre pour text était "(temperature <= 2) AND (OK)", cette fonction se comporterait comme si elle était écrite comme suit :
def parse(text: str, ok: ?, make_comparison: ?, make_and: ?, make_or: ?) -> ?:
return make_and(
make_comparison("temperature", "<=", "2"),
ok
)
(a) Définissez et implémentez une fonction parse_requirement qui accepte une str et renvoie un prérequis sous forme de Requirement, en se servant de parse.
Vous pouvez supposer qu’aucune erreur ne se produit pour cette question (la représentation texte est valide).
(b) Puis, complétez la déclaration de parse avec les types manquants, sans l’implémenter.
Vous devez donc uniquement ajouter les types dans sa signature.
Vous ne devez pas écrire son implémentation !
Conseil : commencez par définir parse comme si elle avait le droit de connaître le type Requirement (ça vaut déjà des points !).
Ensuite, faites en sorte d’éliminer les références à Requirement dans sa définition.
Rappelez-vous que cette fonction est en fait définie dans une bibliothèque, qui ne connaît pas votre Requirement.