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 @property pour l'ensemble des caractéristiques ainsi rendues protégées.
  • Pour l'attribut Personnage.richesse :

    • renommez-le en _richesse
    • créez un accesseur richesse avec le décorateur @property
    • créez un accesseur richesse_totale avec le décorateur property (pour rappel, la richesse totale est la somme des valeurs des objets détenus plus la valeur de l'attribut _richesse)

Questions

  1. Pourquoi est-il nécessaire de préfixer ces attributs de _ ? Aurait-on peu s'en passer ?
  2. Est-ce que le fait d'introduire ces décorateurs "casse" du code par ailleurs ? Analysez pourquoi, si c'est effectivement le cas.
  3. 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 :

  1. RandomObjet(Objet) qui a pour effet d'aléatoirement ajouter -1, 0 ou 1 aux caractéristiques force, charisme, etc. du Personnage qui en fait l'acquisition. (tests unitaires)
  2. ObjetMortel(Objet) qui met toutes les caractéristiques ainsi que les points de vie à zéro du Personnage qui 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.

  1. Il est considéré comme infecté.
  2. Lorsqu'un Personnage en fait l'acquisition, il perd 1 point de vie du fait d'être en contact avec l'infection.
  3. L'objet transmet son infection à l'ensemble des objets du Personnage au 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:

  1. elle fait appel à la fonction Objet.effetAcquisition() parente;
  2. elle fait appel à une fonction propre fonctionInfection() qui provoque les effets de l'infection sur le Personnage passé en argument;
  3. 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 :

  1. appeler effetAcquisition() de l'objet passé en argument ;
  2. appliquer fonctionInfection() du virus qui a infecté l'objet au Personnage qui vient d'en faire l'acquisition ;
  3. 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.