python projet objets

Identification

Infoforall

40 - 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 Rencontre
  • La Classe Lieu
  • La Classe Manoir
  • La Classe Objet ?
  • ...

On obtiendra alors un petit jeu sympa en utilisant l'interactivité offerte par Python via la console.

Mais, pour les plus rapides, une interface graphique déjà réalisée vous permettra de faire la liaison entre les données du manoir, l'utilisateur et l'affichage à l'écran des pièces et monstres de votre manoir.

Un Manoir Hanté
Page d'accueil de l'interface graphique basique

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

1 - la Classe Monstre

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

Votre projet contiendra au final au moins trois fichiers Python qui devront être situés dans le même répertoire :

  • donnees.py qui permettra de générer les données du manoir, de ses habitants et des aventuriers qui s'y déplacent. Nous allons utiliser des Classes bien entendu puisqu'il y a aura interaction entre tous ces éléments.
  • interface.py qui permettra de générer l'interface graphique une fois que vous aurez un peu avancé sur le manoir. Vous n'aurez normalement que des modifications à la marge à faire dans ce module. La création d'une interface graphique n'est pas le but de ce projet.
  • jeu.py qui permettra de faire la liaison entre les données et l'interface graphique. C'est le centre de décision du projet.

01° Créer une classe Monstre dans un fichier nommé donnees.py.

Voici le prototype de la méthode __init__() :

def __init__(self, nom, dsc, img=None, obj=None) -> None

On doit donc envoyer entre 2 à 4 arguments au constructeur.

Les attributs suivants sont définis en fonction des paramètres recus .

  1. nom : un string qui contient l'appelation du monstre qu'on rencontre : Squelettre, Troll, Vampire, Momie...
  2. description : un string contenant la description du monstre. Cette description va apparaître dans la console et permettra au joueur de savoir qui ou quoi il rencontre. 'Monstre pas beau' est un minimum.
  3. image : None par défaut, ou un string contenant le nom d'une image présente au même endroit que votre fichier Python. Vous verrez plus loin comment parvenir à afficher ces images.
  4. objet : None par défaut ou une instance de la Classe objet (qu'on va créer plus tard).

Les attributs suivants seront déterminés au hasard.

  1. habilite : un entier compris entre 1 et 10 (au hasard). Plus le score est grand, plus le personnage est habile au combat.
  2. endurance : un entier compris entre 1 et 10 (au hasard). Plus le score est grand, plus le monstre peut recevoir de coups.
  3. force : le nombre de dégâts provoqués par le monstre lorsqu'il parvient à vous toucher. 1 si moyen moins, 2 si dangereux et 3 pour quelques adversaires exceptionnels. Mettre 2 pour votre monstre.
  4. courage : un entier compris entre 1 et 10. Plus le score est grand, plus les chances sont minces que le monstre fuit la salle à votre arrivée ou fuit le combat s'il est blessé. 8 pour la plupart des monstres. A 10, il ne fuit jamais, à 0 il ne combattrait jamais !

Un exemple d'instantiation :

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

Créez quelques Monstres via la console et ne passez à la question suivante que si cela fonctionne.

02° Rajouter des lignes à la méthode __init__() : on veut que la deuxième série d'attributs (ceux dont les valeurs sont aléatoires pour le moment) soit fixée à des valeurs précises en fonction d'un mot contenant dans le nom du Monstre. Par exemple, on pourrait créer des monstres nommées "Squelette 1" et "Squelette 2" mais les deux auraient exactement une Force de 4 et pas des valeurs tirées au hasard. Il faut pour cela utiliser une instruction conditionnelle et le mot-clé in : sous-string in string

03° Rajouter dans le corps du module donnees.py quelques instanciations 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 module en lui-même (le "programme principal") avec un if __name__ == '__main__'

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

04° Rajouter une méthode get_description() de la classe Monstre qui renvoie le string donnant la description du monstre et une méthode get_carac() de la classe Monstre qui renvoie un string donnant les caractéristiques sous forme courte (Habilite, endurance...) : on l'utilisera pour afficher les caractéristiques du Monstre au joueur.

def get_description(self:'Monstre') -> str

def get_carac(self:'Monstre') -> str

Ce string devra informer le joueur des caractéristiques du PNJ et apparaitra à l'écran. Les f-strings sont vos amis.

Voici un exemple de réponse possible :

>>> a = Monstre("Squelette", "Un squelette semblant bouger seul et muni d'une épée.", "squelette.png") >>> a.get_description() 'Un squelette semblant bouger seul et muni d'une épée.' >>> a.get_carac() 'Squelette H3 E1 F2 C10'

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

Si le franglais vous dérange, vous pouvez remplacer get par obtenir ou recuperer, mais c'est plus long...

05° Rajouter une méthode-prédicat est_hs() de la classe Monstre qui renvoie le booléen True si le monstre n'a plus de points d'endurance.

def est_hs(self:'Monstre') -> bool

2 - la Classe Personnage

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

Cette classe va servir pour le personnage principal mais également pour d'éventuels autres personnages, pas hostiles. Le vieux sage, ce genre de choses.

06° Créer une classe Personnage qui possédera plusieurs attributs tirés au hasard. Par contre, on ne fournit qu'un seul argument au constructeur : le nom du personnage.

Voici le prototype de la méthode __init__() :

def __init__(self, nom) -> None

  • nom : le nom de l'aventurier. Pas de valeur par défaut, à fournir impérativement.
  • habilite : un entier. On ne le fournit pas, le score est aléatoire entre 6 et 9 : 5 + 1d4.
  • endurance : un entier. Le score est aléatoire entre 8 et 15 : 7 + 1d8.
  • 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.
  • charisme : un entier compris entre 1 et 10. Plus le score est grand, plus le personnage aura de chances d'obtenir des bonus de la part des autres Personnages ou de faire fuir les Monstres.
  • vigilance : un entier compris entre 1 et 10. Plus le score est grand, plus le personnage aura de chances de détecter les pièges et la présence de monstres dans les salles autour de lui par exemple.
  • discretion : la capacité du personnage à traverser une pièce sans se faire voir du monstre. On ne peut pas interagir avec la pièce mais on n'a pas à combattre le montre.
  • lieu : None au début puis la référence du Lieu où se trouve actuellement le personnage.
  • lieux_precedents : [] au début puis on empilera les destinations successives avec append() en fin de liste pour pouvoir revenir en arrière.
  • objet : None à la création puis un objet éventuel.

Le seul argument à fournir au constructeur est donc le nom du Personnage Joueur.

>>> h = Personnage("Alice") >>> h.habilite 9 >>> h.lieu None >>> h.lieux_precedents []

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

07° 3 choses à faire, attention.

Rajout 1

Rajouter une méthode subir_degats() dans la classe Personnage.

def subir_degats(self:'Personnage', degats:int) -> None

Cette méthode doit recevoir un entier et réduire l'endurance du personnage activant la méthode. Pensez à tester que l'entier reçu soit bien positif avant de tenter de le déduire de l'endurance. Sinon, c'est un soin.

De la même façon, la valeur minimale est définie à 0. Difficile d'avoir moins que 0 point de vie.

Rajout 2

Faire ensuite de même dans la Classe Monstre.

def subir_degats(self:'Monstre', degats:int) -> None

Rajout 3

Comme le personnage peut maintenant perdre des PV, rajouter la méthode est_hs() qui va nous permettre de savoir s'il est encore valide.

def est_hs(self:'Personnage') -> bool

08° Rajouter une méthode get_carac() à la classe Personnage qui renvoie un string donnant les caractéristiques sous forme courte (Habilite, endurance...) : on l'utilisera pour afficher les caractéristiques du personnage au joueur.

Si le personnage ne transporte pas d'objet, cela devra apparaître.

def get_carac(self:'Personnage') -> str

>>> a = Personnage("Alice") >>> a.get_carac() 'Alice H9 E10 F2 C8 V3 \nPas d'objet'

09° Rajouter une méthode combattre() dans la classe Personnage. 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 :

def combattre(self:'Personnage', adversaire:'Monstre') -> tuple

1
alice.combattre(reine_rouge)

La méthode va lancer un tour de combat entre le Personnage controlé par le joueur (alice ici) et le Monstre (reine_rouge) :

  • On mémorise 1d6 + 1d6 + habilite pour le Personnage (on pourrra afficher cette somme dans la console avec un print() pendant la phase de mise au point).
  • On mémorise 1d6 + 1d6 + habilite pour le Monstre (on pourrra afficher cette somme dans la console avec un print() pendant la phase de mise au point).
  • Si les deux scores sont identiques, les deux combattants perdent 1 point d'endurance : on modifie les états des deux combattants.
  • Sinon si l'un des deux combattants fait mieux que l'autre, il inflige ses dégats (force) à l'autre combattant mais ne subit aucune perte. Un petit message avec un print() en informe le joueur tant qu'on a pas créé d'interaction avec une interface graphique.
  • Coup critique : si l'un des deux combattants a fait un double avec ses deux dés, il double les dégâts qu'il inflige.

Le tuple renvoyé comporte deux indices :

  • L'indice 0 correspond à un code-string de résultat :
    • "x" si aucun des deux combattants n'abandonne ou n'est HS
    • "M" si le monstre est HS
    • "P" si le personnage est HS
    • "MP" si monstre et personnage sont HS
    • "F" si le monstre tente de fuir (on rajoutera cette possibilité plus tard)
    • ...
  • L'indice 1 correspond à un string décrivant l'échange. Il pourra être affiché à l'écran. On doit au moins apprendre qui a perdu des points, voire est mort (ou plus tard : a pris la fuite).

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

10° 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.

def soigner(self:'Personnage', soins:int) -> None

1
alice.soigner(10)

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

3 - la Classe Lieu

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

11° Créer une classe Lieu qui possédera les attributs suivants :

Voici le prototype de la méthode __init__() :

def __init__(self, nom, dsc, img=None) -> None

Voici les 3 attributs qu'on remplit à partir des paramètres.

  • nom : le nom de la pièce ou du lieu.
  • description : un string contenant la description de la salle. C'est ce texte qui servira de description au joueur.
  • image : None par défaut, ou un string contenant le nom d'une image présente au même endroit que votre fichier Python. Vous verrez plus loin comment parvenir à afficher ces images.

Ces attributs ne sont pas tranmis au constructeur, il faudra les compléter plus tard. Au départ, beaucoup contiennent juste None.

  • nord : la référence d'une instance de Lieu qu'on peut atteindre en partant au nord. None au départ.
  • sud : la référence d'une instance de Lieu qu'on peut atteindre en partant au sud. None au départ.
  • est : la référence d'une instance de Lieu qu'on peut atteindre en partant à l'est. None au départ.
  • ouest : la référence d'une instance de Lieu qu'on peut atteindre en partant à l'ouest. None au départ.
  • occupant : la référence éventuel du monstre présent. None au départ.
  • objets : un tableau vide au départ, qui contiendra les références des objets présents dans la salle.
  • action_supp : None par défaut, ou un conteneur contenant les indications et conséquences d'une autre action réalisable dans la pièce, en plus des simples changements de direction et d'attaque de monstres. T pour Tirer sur Levier, F pour Fouiller... Vous aurez à l'implémenter plus tard.
1 2
entree = Lieu('Entrée', "Une entrée délabrée et poussiéreuse. Une porte entrouverte se trouve à l'est") cuisine = Lieu("Cuisine", "Tout est très bien rangé dans cette cuisine. Un étrange plat mijote sur le feu. Une porte mène à l'ouest.")

12° Créer quelques salles dans le programme principal et créer quelques liaisons entre elles en utilisant des modifications directes des attributs nord, sud... Pas d'encapsulation imposée dans le module. Le but est d'obtenir rapidement un prototype opérationnel.

Le programme principal pourrait contenir des lignes de ce type :

1 2 3 4
entree = Lieu('Entrée', "Une entrée délabrée et poussiéreuse. Une porte entrouverte se trouve à l'est") cuisine = Lieu("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.

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

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

1 2
alice.lieu = entree entree.occupant = squelette

Le truc magique ?

Sur l'exemple précédent, l'attribut lieu d'alice contient la référence d'une instance de Lieu. Cette salle contient d'ailleurs la référence d'un monstre qu'on peut obtenir via son attribut occupant. Du coup, on peut aller le chercher directement en tapant ceci :

>>> alice.lieu.occupant <__main__.Monstre object at 0x7f2a81e5ee80>

On voit bien qu'on y a stocké l'adresse 7f2a81e5ee80 en hexadécimal. Mais quel est ce monstre ? Comme les monstres possèdent un attribut nom, il suffit de le demander poliment à Python :

>>> alice.lieu.occupant.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 !

14° Créer une méthode decrire_lieu() de la classe Lieu (attention !) : elle doit renvoyer un string contenant la description de la salle, suivie d'une description du monstre éventuellement présent. Pensez aux passages à la ligne avec \n pour éviter les phrases de plus de 80 caractères.

def decrire_lieu(self:'Lieu') -> str

Notez bien que chaque instance de Lieu possède un attribut occupant contenant soit None, soit 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_lieu(self): """Renvoie une description de la salle et le monstre éventuellement présent :: param self(Lieu) :: une instance de Lieu :: 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.occupant: # cela veut dire si self.occupant existe (n'est pas 0, vide ou None) reponse = reponse + f"\nLa salle contient également : {self.occupant.get_description()}" return reponse

15° Créer une méthode decrire_actions_possibles() de la classe Lieu : elle doit renvoyer un string contenant les différentes actions possibles, encodée chacune par une lettre à taper au clavier. Pensez aux passages à la ligne avec \n pour éviter les phrases de plus de 80 caractères.

def decrire_actions_possibles(self:'Lieu') -> str

Si le lieu possède un occupant, on doit pouvoir le combattre avec C ou fuir avec F (on verra comment gérer réellement le retour à la salle précédente plus tard).

Si le lieu ne possède pas ou plus de montre, il faudra donc regarder si le lieu possède des accès Nord, Sud, Ouest et Est et noter qu'on peut donc écrire N, S, O ou E.

Vous aurez à rajouter des choses ensuite : description des objets éventuels, ramassage d'un des objets en échange de l'objet que transporte le personnage, action supplémentaire dans cette salle...

Pour l'instant, nous allons nous contenter de permettre au personnage de se déplacer et de combattre.

16° Créer une méthode set_occupant() de la classe Lieu : elle doit peupler le lieu de None, ou du Monstre ou du Personnage fourni.

def set_occupant(self:'Lieu', occupant:'None|Monstre|Personnage') -> bool

On ne peut placer un nouvel occupant que s'il n'y a pas d'occupant pour le moment ou que l'occupant ne possède plus de PV. Dans ce cas, la méthode renvoie True.

Si on ne peut pas remplacer le monstre, on renvoie False.

4 - Interaction entre les objets

Relations entre les objets
Image libre de droit

17° Créer une méthode observer() dans la classe Personnage : si le personnage se trouve bien dans une salle, elle simplement renvoyer la réponse de la méthode decrire_lieu() en la faisant agir sur le lieu où se trouve le personnage actuellement. PRECONDITION : le personnage se trouve bien dans un Lieu.

def observer(self:'Personnage') -> str

Sur le même modèle, créer la méthode reflechir() dans la classe Personnage : si le personnage se trouve bien dans une salle, elle simplement renvoyer la réponse de la méthode decrire_actions_possibles() en la faisant agir sur le lieu où se trouve le personnage actuellement. PRECONDITION : le personnage se trouve bien dans un Lieu.

def reflechir(self:'Personnage') -> str

18° Créer la méthode combattre_monstre_actuel() de la classe Personnage : la méthode répond la réponse que va faire combattre() en lui fournissant comme référence de monstre le monstre présent dans la salle du personnage en utilisant l'attribut occupant du Lieu.

PRECONDITION : le lieu du personnage existe et contient bien un monstre.

def combattre_monstre_actuel(self:'Personnage') -> tuple

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 : avant de répondre, si le monstre est mort, il faut le faire disparaitre de la salle en zappant sa référence :

1
self.lieu.set_occupant(None)

19° Créer une méthode aller_nord() de la classe Personnage.

def aller_nord(self:'Personnage') -> bool

Si la salle actuelle ne contient pas de monstre et que l'attribut nord de sa salle actuelle ne contient pas None, la méthode effectue les tâches suivantes :

  • Elle rajoute avec append() la référence du lieu actuel du personnage dans l'attribut lieux_precedents.
  • Elle modifie l'attribut lieu du Personnage pour y mettre le destination pointée au nord.
  • Elle renvoie True pour indiquer que le déplacement est effectué.

Sinon, c'est que la salle contient un monstre ou que rien ne permet d'aller au nord. On renvoie False pour indiquer que le déplacement n'a pas pu aboutir.

Rajouter ensuite les méthodes aller_sud(), aller_est() et aller_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.

Ce qu'il nous manque, c'est un conteneur pour stocker tout notre monde : les monstres, les lieux, les futurs objets et notre personnage bien entendu.

C'est pour cela que nous allons créer une Classe Manoir.

Nous pourrons ainsi créer notre manoir et renvoyer tout ce qu'il contient au programme pour voudra l'utiliser pour en faire un jeu.

20° Rajouter la classe Manoir ainsi que la fonction peupler_manoir() à la fin de votre script donnees.py.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
class Manoir: def __init__(self): self.heros = None self.depart = None self.lieux = [] self.monstres = [] def peupler_manoir(): # Création du personnage du joueur aventurier = Personnage("Alice") # Création et agencement des lieux des = """Vous êtes devant l'entrée qui mène au nord. Une solide porte en bois est entreouverte""" exterieur = Lieu("Entrée", des, "manoir.jpg") des = "Vous êtes dans le hall d'entrée du manoir." hall = Lieu("Hall", des, "hall.jpeg") exterieur.nord = hall hall.sud = exterieur # Création et positionnement d'un monstre squelette_1 = Monstre("Squelette", "Un squelette semblant bouger seul et muni d'une épée.", "squelette.png") hall.set_occupant(squelette_1) # Affectation d'un lieu de départ à l'aventurier aventurier.lieu = exterieur # Création du manoir manoir = Manoir() manoir.heros = aventurier manoir.depart = aventurier.lieu manoir.lieux.append(exterieur) manoir.lieux.append(hall) manoir.monstres.append(squelette_1) return manoir if __name__ == '__main__': m = peupler_manoir()

Maintenant que vous avez un (petit) manoir m, créé à l'aide de la fonction peupler_manoir(), vous pouvez jouer en passant par la console.

>>> m.heros.get_carac() 'Alice : H7 E9 F2 C9 V2\nObjet : Aucun' >>> m.heros.observer() "Vous êtes devant l'entrée qui mène au nord. Une solide porte en bois est entreouverte" >>> m.heros.reflechir() 'Se déplacer vers N ' >>> m.heros.aller_nord() True >>> m.heros.observer() "Vous êtes dans le hall d'entrée du manoir.\nLa salle contient également : Un squelette semblant bouger seul et muni d'une épée." >>> m.heros.combattre_monstre_actuel() ('M', "L'aventurier prend l'avantage. Votre adversaire est vaincu.") >>> m.heros.observer() "Vous êtes dans le hall d'entrée du manoir." >>> m.heros.reflechir() 'Se déplacer vers S '

Le module donnees est encore un peu incomplet puisque nous n'avons pas donné de sens à certains attributs des monstres et des personnages (charisme, vigilance, courage...) Ce sera à vous d'utiliser ces attributs comme bon vous semble. Soyez imaginatif.

Mais avant cela, il nous reste à faire interagir nos données avec notre interface graphique.

5 - Données, interface graphique et jeu

Vous allez maintenant devoir pouvoir votre projet :

  • Partie Modèle : le programme donnees.py contenant notamment la fonction peupler_manoir(). Vous connaissez bien ce module puisque vous venez de le créer !
  • Partie Vue : le programme interface.py construit l'interface graphique en utilisant le constructeur de la classe IHM. Vous n'avez rien de particulier à connaître de ce module si ce n'est qu'il comporte 6 fonctions d'interface que nous allons utiliser dans quelques lignes :
    • Cinq fonctions d'affichage permettant d'envoyer du texte ou une image à afficher
    • Une fonction permettant de récupérer les événements qui surviendront sur l'interface graphique.
  • Partie Controleur : le programme jeu.py construit les données du manoir à l'aide de peupler_manoir() du module donnees et construit l'interface graphique en utilisant le constructeur de la classe IHM du module interface. C'est ce programme qui fait la liaison entre les données et l'interface graphique.
  • interface.py jeu.py donnees.py

Commencez par télécharger les 6 fichiers en mettant tous les fichiers dans le même répertoire.

📁 votre_repertoire

Nous allons simplement étudier le fichier jeu.py, le module d'interface graphique étant totalement fourni.

Voici les explications du programme jeu.py :

1 2 3 4
# Importations import interface # Dépendances : PIL(Pillow) et tkinter import donnees

On importe juste nos deux modules. Attention, il faudra également avoir installé le module Pillow puisqu'il faut partie des dépendances de interface.

Passons la déclaration des fonctions. Nous verrons ce qu'elles font lorsque nous en aurons besoin.

Nous atteignons alors le programme principal :

35 36 37 38 39 40 41 42 43 44
# PROGRAMME PRINCIPAL # Création des données et de l'interface graphique m = donnees.peupler_manoir() # Création des données du manoir ihm = interface.IHM() # Création de l'interface graphique # ihm_signale_evenement va récupérer les événements sur l'ihm ihm.signaler_evenement(lambda event: ihm_signale_evenement(event, m, ihm)) modifier_affichage(m, ihm)

L38 : on crée une instance du Manoir sous le nom m.

L39 : on crée une instance de l'IHM sous le nom ihm.

L42 : on précise via la méthode signaler_evenement() qu'on désire activer automatiquement la fonction ihm_signale_evenement() à chaque fois qu'on détecte qu'un événement survient sur l'IHM. La syntaxe exacte de la liaison est un peu compliquée et les fonctions lambda n'étant pas vraiment au programme, passons sur les détails. Notre fonction va recevoir l'événement, la référence du manoir et de l'IHM à chaque fois qu'un événement survient (touche enfoncée, clic souris...)

L44 : on appelle la fonction modifier_affichage() en lui transmettant les données du manoir m et la référence de l'interface graphique ihm.

Regardons cette fonction :

24 25 26 27 28 29 30 31 32
def modifier_affichage(manoir:'Manoir', ihm:'IHM') -> None: """Modifie les 5 champs de l'interface graphique""" h = manoir.heros ihm.afficher_txt_1( h.observer() ) # affiche le texte descriptif ihm.afficher_txt_2( h.reflechir() ) # affiche les actions possibles ihm.afficher_txt_3( h.get_carac() ) # affiche les caractéristiques du héros ihm.afficher_img_1( h.lieu.image ) # affiche l'image éventuelle du lieu if h.lieu.occupant: ihm.afficher_img_2( h.lieu.occupant.image) # affiche l'image éventuelle du monstre

Comme on peut le voir, on récupère le héros dans la variable h puis on va activer une à une les 5 fonctions permettant de mettre à jour les 5 zones modifiables sur l'IHM. Il suffit de transmettre des strings contenant le texte voulu ou le nom de l'image voulue.

Un Manoir Hanté
Localisation des zones texte et image

Tout ceci permet d'expliquer ce qui apparaît à l'écran lorsqu'on lance jeu.py.

Vous savez maintenant afficher ce que vous voulez sur l'IHM.

Reste à voir comment récupérer les informations en provenance de l'IHM lorsqu'elle détecte un événement. Si vous vous souvenez, nous avions associé ces événements à la fonction ihm_signale_evenement().

9 10 11 12 13 14 15 16 17 18 19 20 21 22
def ihm_signale_evenement(evenement:'tkinter.Event', manoir:'Manoir', ihm:'IHM') -> None: """Fonction où on récupère des informations sur l'événément reçu via l'IHM""" # On récupère la touche sur laquelle on vient d'appuyer touche = evenement.char print(f"\nEVENEMENT RECU : {evenement}") # Permet de voir le vrai événement print(f"CODE PERSO : {touche}") # L'IHM permet de connaitre la touche utilisée # Il faut maintenant gérer l'événement : modifier les données et redemander une affichage à l'IHM h = manoir.heros if touche == 'N' or touche == 'n': if h.aller_nord(): # Si aller au nord est possible et c'est bien passé modifier_affichage(manoir, ihm)

Que fait cette fonction ? Elle reçoit trois paramètres dont le premier est l'événement sur l'interface Tkinter.

L13 : on récupère le caractère tapé sur le clavier s'il s'agit bien d'un événément clavier.

L19 : on récupère la référence du héros dans h.

L20-21-22 : si la touche est un N, on tente de déplacer le héros et si le déplacement a bien eu lieu, on modifie l'affichage sur l'IHM.

Voici le programme dans sa totalité :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
# Importations import interface # Dépendances : PIL(Pillow) et tkinter import donnees # Déclaration des fonctions def ihm_signale_evenement(evenement:'tkinter.Event', manoir:'Manoir', ihm:'IHM') -> None: """Fonction où on récupère des informations sur l'événément reçu via l'IHM""" # On récupère la touche sur laquelle on vient d'appuyer touche = evenement.char print(f"\nEVENEMENT RECU : {evenement}") # Permet de voir le vrai événement print(f"CODE PERSO : {touche}") # L'IHM permet de connaitre la touche utilisée # Il faut maintenant gérer l'événement : modifier les données et redemander une affichage à l'IHM h = manoir.heros if touche == 'N' or touche == 'n': if h.aller_nord(): # Si aller au nord est possible et c'est bien passé modifier_affichage(manoir, ihm) def modifier_affichage(manoir:'Manoir', ihm:'IHM') -> None: """Modifie les 5 champs de l'interface graphique""" h = manoir.heros ihm.afficher_txt_1( h.observer() ) # affiche le texte descriptif ihm.afficher_txt_2( h.reflechir() ) # affiche les actions possibles ihm.afficher_txt_3( h.get_carac() ) # affiche les caractéristiques du héros ihm.afficher_img_1( h.lieu.image ) # affiche l'image éventuelle du lieu if h.lieu.occupant: ihm.afficher_img_2( h.lieu.occupant.image) # affiche l'image éventuelle du monstre # PROGRAMME PRINCIPAL # Création des données et de l'interface graphique m = donnees.peupler_manoir() # Création des données du manoir ihm = interface.IHM() # Création de l'interface graphique # ihm_signale_evenement va récupérer les événements sur l'ihm ihm.signaler_evenement(lambda event: ihm_signale_evenement(event, m, ihm)) modifier_affichage(m, ihm)

A vous maintenant de finaliser ce projet en travaillant à plusieurs. Les 8 tâches minimales à réaliser :

  1. Comment fuir dans la pièce précédente (avec la touche F ?) un combat si le monstre parait trop puissant ?
  2. Comment tenter de traverser discrétement une salle sans combattre ?
  3. Et si les monstres pouvaient eux-même fuir lorsqu'un combat se passe mal ? Si la destination possède déjà un monstre, les deux devront alors se batte à mort entre eux.
  4. Et si certaines pièces contenaient des objets ? L'aventurier ne pourrait en transporter qu'un, s'il en possède déjà un, il y aura donc une permutation de place entre les deux objets.
  5. Et si les monstres eux-mêmes pouvaient bouger aléatoirement ou vous suivre parfois lorsque vous fuyez ?
  6. Et si on enregistrait tout ce qui se passe dans un fichier texte pour obtenir le déroulé de l'aventure ?
  7. Et si on enregistrait l'état du jeu pour pouvoir le rouvrir ensuite et poursuivre l'aventure là où on l'avait laissé ?
  8. 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 ?

Vous pourrez ensuite rajouter d'autres options.

Description des 4 critères de réussite :

  1. Réalisation pratique du projet, notamment :
    • Toutes les fonctionnalités sont présentes
    • Respect scrupuleux du cahier des charges
  2. Bonnes pratiques de programmation, notamment :
    • Noms explicites
    • Utilisation limitée et raisonnée des variables globales
    • Fonctions courtes et décomposition en sous-étapes
    • Séparation entre fonctions gérant les données et l'interface graphique.
    • Présence de jeux de tests pour les fonctions gérant les données
  3. Communication (à l'écrit et à l'oral), notamment
    • Documentation des fonctions et des modules
    • Qualité de l'interaction avec l'enseignant
    • Qualité de l'interaction avec les autres membres de l'équipe
  4. Qualité de la revue de projet, notamment :
    • régularité des informations notées
    • description de qui a fait quoi
    • prototypes des fonctions avant implémentation
    • tests permettant de valider la solution avant implémentation si possible, après coup sinon

On attribue une note de 0 à 5 aux critères.

  • 5 si Bien
  • 4 si Assez bien
  • 3 si Moyen
  • 2 si Léger
  • 1 si Insuffisant
  • 0 si Rien

On fait la somme des critères.

Attention : aucun critère ne peut avoir plus que (le plus petit + 2).

6 - FAQ

Comment afficher une image sans Tkinter ?

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 : 01 09 2022
Auteur : ows. h.