TP - Jeu de personnages, suite
Ce TP poursuit le travail entamé précédemment et suppose que l'ensemble des classes et fonctions du TP 2 a été réalisé et est opérationnel.
1. Du bon usage de @property
Dans la classe Personnage :
- Rendre les 6 caractéristiques principales (force, charisme ...) ainsi que
pv(et seulement ceux-là) pseudo-protégés en les renommant avec un préfixe_. Rappel : ceci n'est qu'une convention de nommage qui ne rend pas les attributs réellement protégés. - Créer des accesseurs en lecture avec le décorateur
@propertypour l'ensemble des caractéristiques ainsi rendues protégées. -
Pour l'attribut
Personnage.richesse:- renommez-le en
_richesse - créez un accesseur
richesseavec le décorateur@property - créez un accesseur
richesse_totaleavec le décorateurproperty(pour rappel, la richesse totale est la somme des valeurs des objets détenus plus la valeur de l'attribut_richesse)
- renommez-le en
Questions
- Pourquoi est-il nécessaire de préfixer ces attributs de
_? Aurait-on peu s'en passer ? - Est-ce que le fait d'introduire ces décorateurs "casse" du code par ailleurs ? Analysez pourquoi, si c'est effectivement le cas.
- Corriger le code et relever les types d'erreur rencontrés (notamment l'absence de setter)
Les tests unitaires pour cette partie se trouvent ici. Ces tests auront besoin de la
fonction create_random_personnage() accessibile ici.
2. Modification de la classe Objet
Ajouter à la classe Objet les méthodes suivantes. Chacune d'entre elles renvoient comme résultat une fonction qui prend des arguments nommés quelconques en argument.
def effetAcquisition(self, p):
# Renvoie la fonction à exécuter lorsqu'un Personnage `p` acquiert l'objet `self`
def effet(**kwargs):
# A compléter
pass
return effet
def effetCession(self, p):
# Renvoie la fonction à exécuter lorsqu'un Personnage `p` cède l'objet `self`
def effet(**kwargs):
# A compléter
pass
return effet
Le comportement par défaut lors d'une acquisition est que l'objet acquis est ajouté à la liste des objets du Personnage p.
De façon similaire, lors d'une cession, l'objet disparait de la liste d'objets de p.
Par ailleurs, si l'on passe un argument nommé prix à la fonction effet lors d'une acquisition (resp. cession)
p._richesse est diminué (resp. augmenté) de prix. Si aucun argument nommé n'est passé, prix correspond à la valeur
de l'objet.
Les tests unitaires pour cette partie se trouvent ici.
3. Modification de la classe Personnage
Modifiez les méthodes acheter(), vendre(), donner() et prendre() de la classe Personnage pour qu'elles prennent en compte les actions à déclencher lors du passage d'un Objet d'un Personnage à un autre.
Testez votre code. (Les tests unitaires pour cette partie se trouvent ici)
4. Création d'Objets particuliers.
Par héritage et en surchargeant correctement les méthodes effetAcquisition() et effetCession() de sorte qu'elles cumulent les effets de base de la classe Objet ainsi que leurs effets propres, définissez les classes suivantes :
RandomObjet(Objet)qui a pour effet d'aléatoirement ajouter -1, 0 ou 1 aux caractéristiquesforce,charisme, etc. duPersonnagequi en fait l'acquisition. (tests unitaires)ObjetMortel(Objet)qui met toutes les caractéristiques ainsi que les points de vie à zéro duPersonnagequi en fait l'acquisition (tests unitaires).
Testez votre code. Faites notamment en sorte qu'un Personnage dont les points de vie sont à zéro puisse être pillé (prendre())
par un autre Personnage et observez ce qui se passe lorsque celui-ci, par malchance, lui prend un ObjetMortel.
Le test pourrait ressembler à ceci :
if __name__ == "__main__":
k = ObjetMortel('kill',0)
alice = Personnage("Alice", [k])
bob = Personnage("Bob")
alice.donner(k, bob)
alice.prendre(k, bob)
Bob (Personnage) a 0 PV et possède 100.0 de richesse vient d'être tué par un objet mortel
Alice (Personnage) a 0 PV et possède 100.0 de richesse vient d'être tué par un objet mortel
5. Création de l'Objet Ultime.
Il s'agit ici de fabriquer un virus qui se propage à travers un ObjetViral(Objet).
Son principe de fonctionnement est le suivant.
- Il est considéré comme infecté.
- Lorsqu'un
Personnageen fait l'acquisition, il perd 1 point de vie du fait d'être en contact avec l'infection. - L'objet transmet son infection à l'ensemble des objets du
Personnageau moment de l'acquisition. Ces objets deviennent donc à leur tour infectés et reproduiront le même effet lorsqu'ils seront acquis par d'autres joueurs.
Attention cela signifie donc que l'on va modifier le comportement des autres objets qui deviendront également viraux !
Première étape : créer la classe ObjetViral(Objet)
La classe ObjetViral(Objet) devra ressembler à ceci, et est similaire à RandomObjet et ObjetMortel pour ce qui est
de l'appel au constructeur et de la surcharge de la méthode effetAcquisition().
La fonction ObjetViral.effetAcquisition() produit une fonction effet() qui opère en trois étapes, comme écrit ci-dessus:
- elle fait appel à la fonction
Objet.effetAcquisition()parente; - elle fait appel à une fonction propre
fonctionInfection()qui provoque les effets de l'infection sur lePersonnagepassé en argument; - elle fait appel à une fonction propre
fonctionPropagation()qui va se faire propager le caractère infectieux à tous les autres objets.
Pour l'instant l'appel à cette fonction est commentée. Elle sera activée plus tard.
class ObjetViral(Objet):
def __init__(self, n: str = None, v: int = 10):
super().__init__(n, v)
def effetAcquisition(self, p: Personnage):
def effet(**kwargs):
super(self.__class__, self).effetAcquisition(p)(**kwargs)
self.fonctionInfection(p)(**kwargs)
# self.fonctionPropagation(p)(self, p,**kwargs)
print(f"{p} vient d'être infecté par un objet viral")
return effet
def fonctionInfection(self, p: Personnage):
# à développer
pass
def fonctionPropagation(self, p: Personnage):
# à développer
pass
Mise en bouche :ObjetViral.fonctionInfection()
Comme vous pouvez le déduire du code, ObjetViral.fonctionInfection() est une méthode qui prend un seul argument de
type Personnage et qui renvoie une fonction qui prend des arguments nommés.
Implémentez cette fonction, de sorte qu'elle soustrait 1 aux points de vie du Personnage passé en argument.
Voici du code pour tester votre approche
if __name__ == "__main__":
k = ObjetViral('virus',0)
alice = Personnage("Alice", [k])
bob = Personnage("Bob")
alice.donner(k, bob)
bob.donner(k, alice)
alice.donner(k, bob)
bob.donner(k, alice)
et dont la sortie devra ressembler à ceci :
Bob (Personnage) a 99 PV et possède 100.0 de richesse vient d'être infecté par un objet viral
Alice (Personnage) a 99 PV et possède 100.0 de richesse vient d'être infecté par un objet viral
Bob (Personnage) a 98 PV et possède 100.0 de richesse vient d'être infecté par un objet viral
Alice (Personnage) a 98 PV et possède 100.0 de richesse vient d'être infecté par un objet viral
Plat de résistance : ObjetViral.fonctionPropagation()
Comme vous pouvez le déduire du code, ObjetViral.fonctionPropagation() est une méthode qui prend un seul argument
de type Personnage et qui renvoie une fonction qui prend plusieurs autres arguments : un Objet, un Personnage
et des arguments nommés. Pour l'instant, l'appel à cette fonction est toujours commenté dans effet().
Le but de cette fonction est donc de propager l'effet infectieux à tous les autres objets du Personnage qui en
fait l'acquisition.
Ce que nous allons donc faire dans la suite est de développer un vrai virus informatique, sous guise de jeu, certes, mais utilisant de vraies techniques virales. Cette partie fait appel à une compréhension technique de Python. Assurez-vous d'avoir bien lu (et compris) la partie cours sur le Python avancé.
Il est rappelé ici qu'il est formellement interdit, et passable de poursuites judiciaires, tout développement de virus informatique nuisant à autrui ou aux infrastructures. Dans le cas de ce TP, la viralité reste contenue au jeu développé.
- Démarrage en douceur
Dans un premier temps, la méthode fonctionPropagation définira une sous-fonction prop(obj, p, **kwargs) qu'elle
retournera.
La fonction prop() fonction parcourra l'ensemble des Objet appartenant au Personnage p et leur ajoutera un attribut nommé
_viral qui sera initialisé à True. prop() affichera un message lorsqu'un objet est ainsi infecté.
def fonctionPropagation(self, p: Personnage):
def prop(obj, p, **kwargs):
# à compléter
pass
return prop
Voici du code pour tester votre approche (il faudra activer l'appel à self.fonctionPropagation(p) en enlevant le
commentaire commentant dans effet())
if __name__ == "__main__":
k = ObjetViral('Virus',0)
alice = Personnage("Alice", [k, Objet('Obj3', 10)])
bob = Personnage("Bob", [Objet('Obj1',10), Objet('Obj2',10)])
alice.donner(k, bob)
alice.acheter(k, bob)
et dont la sortie devra ressembler à ceci :
Attention Virus (0) infecte Obj1 (10) !
Attention Virus (0) infecte Obj2 (10) !
Bob (Personnage) a 99 PV et possède 120.0 de richesse vient d'être infecté par un objet viral
Attention virus (0) infecte Obj3 (10) !
Alice (Personnage) a 99 PV et possède 110.0 de richesse vient d'être infecté par un objet viral
On constate d'une part que l'infection provoque l'affichage des messages d'infection des objets, et que d'autre part
les Personnage, par l'effet infectieux, perdent un point de vie.
- Peut mieux faire
Le code précédent infecte bien les objets du Personnage qui reçoit l'ObjetViral mais les objets en question ne deviennent pas contaminants pour autant.
On peut l'observer avec le code ci-dessous.
if __name__ == "__main__":
k = ObjetViral('Virus',0)
o1 = Objet('Obj1', 10)
o2 = Objet('Obj2', 10)
alice = Personnage("Alice", [k])
bob = Personnage("Bob", [o1])
claire = Personnage("Claire", [o2])
# Alice infecte Bob avec l'objet viral
alice.donner(k, bob)
# Bob rend l'objet à Alice (et l'infecte)
bob.donner(k, alice)
# Bob donne un de ses objets infectés à Claire
bob.donner(o1, claire)
Attention Virus (0) infecte Obj1 (10) !
Bob (Personnage) a 99 PV et possède 110.0 de richesse vient d'être infecté par un objet viral
Alice (Personnage) a 99 PV et possède 100.0 de richesse vient d'être infecté par un objet viral
L'objet o1 est bien infecté, mais lorsqu'il est transmis à Claire aucun message n'est affiché.
Ceci est dû au fait que o1 est de type Objet et qu'il applique la méthode effetAcquisition() propre à sa classe et
qui ne comporte pas d'action d'infection ni de propagation.
Pour que l'infection se propage, l'objectif devient donc de modifier dynamiquement le code de la méthode effetAcquisition() de l'objet infecté pour y injecter la capacité de se propager.
Étape 1 - Créer une fonction qui imite effetAcquisition()
Créer une imitation d'une fonction quelconque n'est pas difficile, il suffit de prendre les mêmes arguments, et
d'exécuter la fonction à imiter. Ici, on cherche à imiter la méthode effetAcquisition() d'un Objet.
Nous appellerons la copie imitation. Et elle prendra les mêmes arguments que la méthode effetAcquisition() :
un Objet et un Personnage. De même, comme la fonction qu'elle imite, elle renvoie une fonction prenant en argument des
arguments nommés quelconques. Le canevas est donné en dessous.
La fonction renvoyée devra :
- appeler
effetAcquisition()de l'objet passé en argument ; - appliquer
fonctionInfection()du virus qui a infecté l'objet auPersonnagequi vient d'en faire l'acquisition ; - appliquer la méthode de propagation
prop().
def fonctionPropagation(self, p: Personnage):
def imitation(objet, p: Personnage):
def effet(**kwargs):
# à compléter
pass
return effet
def prop(obj, p, **kwargs):
# à compléter
pass
return prop
Ainsi, la fonction imitation() est en tout point similaire à la fonction effetAcquisition()
mais elle effectue en plus les actions d'infection et de propagation du virus initial.
Étape 2 - Substituer effetAcquisition() par son imitation.
Comme il est possible d'ajouter ou de modifier des attributs d'un objet en Python, et comme il est possible de redéfinir des fonctions par affectation, il est possible d'affecter des nouvelles méthodes à un objet.
Pour cela, il est néanmoins nécessaire de convertir la fonction que l'on cherche à ajouter à un objet en un type méthode
(les méthodes sont des fonctions particulières en Python, notamment en raison de la présence et la gestion de l'argument d'appel self)
La conversion de la fonction imitation() en une méthode d'un objet o se fait par la conversion en type MethodType.
On peut ensuite affecter la méthode ainsi créée à l'objet o comme suit.
methode = types.MethodType(imitation, o)
o.nouvelle_methode = methode
Modifiez la fonction prop() de sorte qu'elle modifie la méthode effetAcquisition() de chaque Objet possédé par
p en la remplaçant par imitation.
Étape 3 - Tomber dans le piège et corriger son erreur.
Soit, vous êtes très fort et l'exécution du test précédent fonctionne sans faille. Félicitations !
Soit, ce qui est plus probable, le test de votre code mène à une récursion infinie. Analysez pourquoi, corrigez et observez le bon fonctionnement de votre virus.
6. (Partie optionnelle) Développer un vaccin
Concevez un objet vaccin qui détecterait la présence d'objets viraux et qui serait capable de les réparer en restaurant leur fonctionnement d'origine et en enlevant tout contamination virale.
Version simple
Dans un premier temps envisagez que l'objet en question ne s'active que lors de son acquisition ou de sa cession.
Version élaborée
Ajoutez dans la classe Objet une méthode utiliser(**kwargs) qui permet d'activer des effets particuliers sans
qu'ils ne soient associés à une acquisition ou une cession.
Surchargez cette méthode pour que le vaccin puisse être utilisé à n'importe quel moment pour désinfecter un objet.