python projet objets

Identification

Infoforall

32 - Manoir Hanté


Vous allez concevoir un jeu consistant à faire explorer un Manoir Hanté (ou un Vaisseau Spacial inconnu, ou ce que vous voulez) à un ou plusieurs Aventuriers.

Un Manoir Hanté
Philarm, CC BY-SA 4.0 https://creativecommons.org/licenses/by-sa/4.0, via Wikimedia Commons

Vous aurez à concevoir :

  • La Classe Aventurier
  • La Classe Monstre
  • La Classe Salle
  • La Classe Trésor ?
  • ...

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Résumé :

1 - la Classe Monstre (4 points)

Un vampire
Image libre de droit issue de https://freesvg.org/img/1547602063.png - CC0 1.0 Universal (CC0 1.0) Public Domain Dedication

01° Créer une classe Monstre qui possédera les attributs suivants :

  • nom : un string qui contient l'appelation du monstre : Squelettre, Troll, Vampire... On doit fournir cette information au constructeur pour qu'on puisse la stocker.
  • habilite : un entier compris entre 4 et 12. Plus le score est grand, plus le monstre est habile au combat. On peut la faire définir par le constructeur. Valeur par défaut de 8.
  • endurance : un entier compris entre 2 et 24. Plus le score est grand, plus le monstre peut recevoir de coups. On peut la faire définir par le constructeur. Valeur par défaut de 12.
  • force : on peut la faire définir par le constructeur. Valeur par défaut de 2.
  • description : un string contenant la description du monstre. On peut la faire définir par le constructeur. La valeur par défaut est 'Monstre pas beau'

Un exemple d'instantiation directe sans utiliser les paramètres nommés :

>>> a = Monstre("Squelette", 7, 12, 2, "Un squelette semblant bouger seul et muni d'une épée.")

Et une instantiation en utilisant les paramètres nommés qui n'ont pas les valeurs par défaut :

>>> b = Monstre(nom="Squelette", habilite=7, description="Un squelette semblant bouger seul et muni d'une épée.")

Testez le constructeur dans la console et passez à la question suivante uniquement si cela fonctionne.

02° Rajouter dans le corps du programme quelques instantiations de monstres.

On rappelle que la structure de votre script doit être :

  • La partie Importation
  • La partie Déclaration de CONSTANTES
  • La partie Déclaration de Classes
  • La partie Déclaration de fonctions
  • Le corps du programme en lui-même (le "programme principal")

Testez le programme et passez à la question suivante uniquement si cela fonctionne.

03° Rajouter une méthode carac() de la classe Monstre qui renvoie un string donnant les caractéristiques globales de l'instance fournie (Habilite, endurance...). Il faudra donc utiliser un f-string.

Voici un exemple de réponse possible :

>>> a = Monstre("Squelette", 7, 12, 2, "Un squelette semblant bouger seul et muni d'une épée.") >>> a.carac() 'Squelette H:7 E:12 F:2'

Testez la méthode et passez à la question suivante uniquement si cela fonctionne.

2 - la Classe Aventurier (4 points)

Un aventurier
Image libre de droit issue de https://pixabay.com/fr/vectors/aventurier-blonde-dessin-anim%C3%A9-1197392/

04° Créer une classe Aventurier qui possédera les attributs suivants :

  • nom : le nom de l'aventurier. A fournir au constructeur.
  • habilite : un entier. Le score est aléatoire entre 9 et 12 : 8 + 1d4. Attention : on ne transmet pas cette valeur au constructeur. Elle est générée aléatoirement à la création.
  • endurance : un entier. Le score est aléatoire entre 13 et 24 : 12 + 1d12. Attention : on ne transmet pas cette valeur au constructeur. Elle est générée aléatoirement à la création.
  • force : un entier valant 2 au départ. Plus tard dans le jeu, ramasser une arme magique pourrait la faire passer à 3 par exemple. Mais à la création, c'est 2. Attention : on ne transmet pas cette valeur au constructeur.
  • salle : la référence d'une instance de Salle (classe non créée pour l'instant). Initialement, on y placera donc None. Attention : on ne transmet pas cette valeur au constructeur.

La seule chose que doit donc recevoir la méthode spéciale constructeur __init__(), c'est donc le nom de l'aventurier.

>>> h = Aventurier("Alice") >>> h.habilite 11

Testez le constructeur de votre classe et passez à la question suivante uniquement si cela fonctionne.

05° Rajouter une méthode combattre(). Cette méthode doit recevoir la référence d'un Monstre (en plus du self de l'aventurier donc). Voici un exemple d'appel :

1
alice.combattre(reine_rouge)

La méthode va lancer le combat à mort entre l'Aventurier (alice) et le Monstre (reine_rouge) :

  • Tant que les deux combattants ont une endurance supérieure à 0 :
    • On mémorise 1d6 + 1d6 + habilite pour l'Aventurier et on affiche le résultat dans la console (avec un print() ).
    • On mémorise 1d6 + 1d6 + habilite pour le Monstre et on affiche le résultat dans la console (avec un print() ).
    • Si les deux scores sont identiques, les deux combattants perdent 1 point d'endurance : on modifie les états des deux combattants. Un petit message avec un print() en informe le joueur.
    • Sinon si l'un des deux combattants fait plus que l'autre, il inflige ses dégats (force) à l'autre combattant. Un petit message avec un print() en informe le joueur.
  • Si l'aventurier est mort, on en informe le joueur avec un print().
  • Si le monstre est mort, on en informe le joueur avec un print().

Réaliser quelques combats pour être certain que votre méthode fonctionne.

Fonction d'interface

Puisque cette méthode comporte des instructions d'affichage (via le print()), il s'agit donc d'une fonction d'interface : si vous désirez modifier un jour votre jeu et l'afficher via une interface graphique, il faudra donc la modifier.

06° Rajouter une méthode soigner() qui reçoit un entier et qui rajoute cette valeur à l'endurance de l'aventurier si l'entier est positif. Si l'endurance dépasse 24, on la bloque à 24.

ATTENTION : comme il faut pouvoir recevoir un entier, cette méthode a besoin d'un paramètre en plus du self.

1
alice.soigner(10)

Testez la méthode sur votre aventurier et passez à la question suivante uniquement si cela fonctionne.

07° Rajouter une méthode carac() qui renvoie un string donnant les caractéristiques globales de l'aventurier (Habilite, endurance...).

Attention : vérifier qu'elle fonctionne, surtout si vous faites un copier-coller avec celle de Monstre : l'aventurier n'a pas de description par exemple.

3 - la Classe Salle (4 points)

Une salle
Image libre de droit issue de https://pxhere.com/fr/photo/741532/

08° Créer une classe Salle qui possédera les attributs suivants :

  • nom : le nom de la pièce. A fournir au constructeur.
  • description : un string contenant la description de la salle. A fournir au constructeur.
  • monstre : la référence éventuel du monstre présent. None au départ.
  • objet : la référence éventuel d'un Tresor présent dans la Salle. None au départ.
  • nord : la référence d'une instance de Salle qu'on peut atteindre en partant au nord. None au départ.
  • sud : la référence d'une instance de Salle qu'on peut atteindre en partant au sud. None au départ.
  • est : la référence d'une instance de Salle qu'on peut atteindre en partant à l'est. None au départ.
  • ouest : la référence d'une instance de Salle qu'on peut atteindre en partant à l'ouest. None au départ.
1 2
entree = Salle('Entrée', "Une entrée délabrée et poussiéreuse. Une porte entrouverte se trouve à l'est") cuisine = Salle("Cuisine", "Tout est très bien rangé dans cette cuisine. Un étrange plat mijote sur le feu. Une porte mène à l'ouest.")

09° Créer quelques salles dans le programme principal et créer quelques liaisons entre elles en utilisant des modifications directes des attributs nord, sud...

Le programme principal pourrait contenir des lignes de ce type :

1 2 3 4
entree = Salle('Entrée', "Une entrée délabrée et poussiéreuse. Une porte entrouverte se trouve à l'est") cuisine = Salle("Cuisine", "Tout est très bien rangé dans cette cuisine. Un étrange plat mijote sur le feu. Une porte mène à l'ouest.") entree.est = cuisine cuisine.ouest = entree

Remarquez bien que comme on peut atteindre la cuisine en partant vers l'est depuis l'entrée, on peut atteindre l'entrée en partant vers l'ouest depuis la cuisine.

10° Attribuer une salle à l'aventurier et placer un monstre dans cette même salle.

Le plus facile est encore de modifier directement, sans passer par un mutateur. Ce n'est pas très POO mais c'est rapide :

1 2
alice.salle = entree entree.monstre = squelette

Le truc magique ?

Sur l'exemple précédent, l'attribut salle d'alice contient donc la référence d'une salle. C'est salle contient d'ailleurs la référence d'un monstre. Du coup, on peut aller le chercher directement en tapant ceci :

>>> alice.salle.monstre <__main__.Monstre object at 0x7f2a81e5ee80>

On voit bien qu'on y a stocké la référence d'un monstre valant 7f2a81e5ee80 en hexadécimal. Mais lequel ? Comme les monstres possèdent un attribut nom, il suffit de le demander poliment à Python :

>>> alice.salle.monstre.nom 'Squelette'

Comme vous le voyez, un objet peut posséder des références vers un autre objet. C'est très pratique lors de la réalisation d'un jeu ou d'un système où certaines choses sont rattachées à d'autres.

Ici :

  • les instances des aventuriers contiennent la référence de la salle où ils sont.
  • les instances des salles contiennent la référence des monstres qu'elles contiennent

Connaissant la référence d'un aventurier, on peut donc trouver la référence de sa salle et la référence du monstre qui est dans cette salle !

11° Créer une méthode decrire() de la classe Salle : elle doit renvoyer un string contenant la description de la salle et une description éventuelle du monstre présent.

Notez bien que chaque instance de Salle possède un attribut monstre contenant la référence éventuelle du monstre présent. Si la référence existe bien, on peut accéder à sa description facilement puisqu'il possède un attribut description.

Cela peut donner quelque chose de ce type :

1 2 3 4 5 6 7 8 9 10 11 12 13 14
def decrire(self): '''Renvoie une description de la salle et le monstre éventuellement présent :: param self(Salle) :: une instance de Salle :: return (str) :: un string contenant la description globale ''' reponse = "" # ... à vous de récupérer la description de la salle "vide"... # ... et ce code permet d'y rajouter la description du monstre éventuel ... if self.monstre: # cela veut dire si self.monstre existe (n'est pas None ou 0) reponse = reponse + f"\nLa salle contient également : {self.monstre.description}" return reponse

4 - Interaction entre les objets (4 points)

Relations entre les objets
Image libre de droit

12° Créer une méthode observer() de la classe Aventurier : elle doit aller chercher la référence de la salle où se trouve l'aventurier puis y appliquer la méthode decrire() dont il faudra afficher la réponse avec un print().

13° Modifier la méthode combattre() de la classe Aventurier : le paramètre recevant la référence du monstre doit posséder maintenant une valeur par défaut de None. Dans ce cas, la méthode va alors chercher la référence de la salle dans l'attribut salle de l'instance d'Aventurier et va y chercher le monstre présent en utilisant l'attribut monstre de la Salle.

De cette façon, un combat devrait pouvoir se faire automatiquement entre l'aventurier et le monstre présent dans la salle de l'aventurier.

Dernière modification : lorsque le monstre est mort, il faut le faire disparaitre de la salle en zappant sa référence :

1
self.salle.monstre = None

Du coup, on notera que pour savoir si la salle de l'aventurier contient un monstre, il suffit depuis une méthode de la classe Aventurier de tester ceci :

1
self.salle.monstre == None

Une évaluation à True veut dire qu'il n'y a pas de monstre.

Ou ceci en logique inverse :

1
self.salle.monstre != None

Une évaluation à True veut dire qu'il y a un monstre.

14° Créer une méthode nord() de la classe Aventurier.

Si la salle contient un monstre, on informe l'utilisateur qu'il doit d'abord le combattre.

Si la salle ne contient pas de chemin vers le nord, on en informe l'utilisateur.

Si la salle ne contient plus de monstre et que l'attribut nord de sa salle actuelle ne contient pas None, la méthode modifiera l'attribut salle de l'aventurier en y plaçant la salle au nord de la salle actuelle. La méthode doit informer que le déplacement a fonctionné et afficher la description de la nouvelle salle (on pourra utiliser la méthode decrire()).

15° Créer les méthodes sud(), est() et ouest()..

Vous devriez obtenir maintenant un système presque correct où les méthodes d'interface de l'aventurier lui permettent de regarder la pièce où il se trouve, combattre le monstre de la pièce, se déplacer et connaître son état.

5 - Finalisation et notation

Si tout est bon, on atteint 16/20.

Maintenant que vos classes sont réalisées, il vous reste à finaliser votre compte-rendu pour optimiser votre note :

  • (+1 point) Fournir votre programme muni:
    • de documentation intégrée aux fonctions (préconditions, postconditions et exemples d'utilisation pouvant être utilisés par le module doctest)
    • d'indications sur la nature des méthodes (interface ? publique ? privée ? IHM car intègre un afficher via un print() ?)
    • de quelques commentaires sur les points délicats dans les méthodes
    • d'un programme principal permettant de créer un manoir
    • d'un exemple d'utilisation de votre jeu : il suffit de jouer en utilisant la console de Thonny et de fournir ensuite une copie des instructions et réponses obtenues.

Si tout cela est effectué, cela vous amène à une note de 17/20.

Pour compléter la note, il vous restera à rajouter quelques fonctionnalités supplémentaires : 1 point pour une fonctionnalité facile à rajouter, 3 points pour une fonctionnalité compliquée. Par exemple :

  • Une classe Tresor qui contient une description et un gain en habilite ou en endurance ou en force lorsqu'on le ramasse.
  • Une méthode ramasse() qui permet à l'aventurier de ramasser l'objet et d'activer son pouvoir. Le trésor doit alors disparaître de la pièce, son effet doit modifier l'état du personnage et un texte descriptif doit expliquer ce qui se passe.
  • Un système de coup critique qui provoque plus de points de dégats lorsqu'on tire un 6 sur le dé par exemple
  • Un attribut Protection qui permet de simuler une armure qui diminue les dégats ?
  • Une façon de finir le jeu ? Trouver la sortie ou parvenir à ramener à un objet particulier dans une pièce particulière pour déclencher la fin du jeu ?
  • Un affichage de l'allure de la salle en ASCII Art ? Un affichage d'image PNG ou JPEG lorsqu'on rentre dans une salle ?
  • ...

Ne réalisez pas de véritable jeu où le joueur n'a qu'à faire des choix via un système de input() : le but est bien de vous faire taper des instructions dans la console en mode OBJET.

Certaines sont faciles à implémenter, d'autres moins. La note ne tiendra compte que du fait qu'au moins deux nouvelles fonctionnalités existent.

6 - FAQ

Comment afficher une image (png, jpg...) ?

Le plus facile est d'installer le module nommé Pillow qui est un fork d'un vieux projet nommé PIL. Pour cela, allez dans le menu Gestion des paquets de Thonny ou installer directement le paquet avec la commande python3 -m pip install --user Pillow dans la console système.

Une fois le paquet installé, on peut aller très facilement faire afficher une image en utilisant le logiciel de visualisation standard de l'ordinateur sur lequel on se trouve.

1 2 3
from PIL import Image as Img objet_image = Img.open("linux.jpg") objet_image.show()

Attention à la localisation de l'image par rapport au fichier Python : si vous ne fournissez que le nom de l'image, les deux fichiers doivent être au même endroit.

Voici pour ce petit premier projet qui visait à vous faire manipuler les objets.

Activité publiée le 05 09 2021
Dernière modification : 05 09 2021
Auteur : ows. h.