EPFL CS-112(j) POO

Course Website

Cours : Jeudi 8h15–9h00, CM 1 4

Exercices : Jeudi 9h15–11h, INF 3, BC 07-08

Accueil

Site Moodle

Tutoriels

Références

Séries

Projet

Final 2025

Solutions

Instructions

Plus exactement, voici toutes les consignes officielles qui s’appliquaient à ce midterm :

Sujet : Ressources et prérequis

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 :

La 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 :

De 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.

Fonctions utiles

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.

Questions

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 :

  1. Transférer la quantité d’énergie en quantité de chaleur ;
  2. Appliquer les productions.

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 :

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.