TP - Jeu de personnages

Introduction

Dans ce TP nous allons programmer un jeu de type RPG avec des personnages ayant certaines caractéristiques et effectuant certaines actions. Un personnage, représenté par la classe Personnage ci-dessous, dispose d'un nombre de caractéristiques (intelligence, force, charisme, obstination et empathie) déterminés de façon aléatoire. Il a également un nom, des points de vie et une certaine richesse. Il dispose, par ailleurs d'un ensemble d'amis que l'on représente par un dictionnaire. Les clés du dictionnaire sont des Personnage et la valeur associée sera le niveau d'amitié représenté par un float dans l'intervalle [-1.0, 1.0].

class Personnage(object):

    def __init__(self, n: str):
        self.nom = n
        self.amis = {}

        self.pv = 100
        self.richesse = 100.0

        self.intelligence = randint(3,18)
        self.force = randint(3,18)
        self.charisme = randint(3,18)
        self.obstination = randint(3,18)
        self.empathie = randint(3,18)

        super().__init__()

    def __str__(self) -> str:
        return f'{self.nom} a {self.pv} points de vie et {self.richesse} de richesse'

Les personnages peuvent également manipuler des objets représentés par la classe Objet. Un Objet a un nom et une valeur.

class Objet(object):

    def __init__(self, n: str = None, v: int = 10):
        super().__init__()

        self.nom = n
        self.valeur = v

1. Mise en place des actions

Un Personnage peut effectuer les actions suivantes : vendre, acheter, donner ou prendre un objet d'un autre Personnage. Au-delà des effets propres de chacune des actions, elles auront également une influence sur l'état d'amitié des personnages. Lorsqu'un personnage interagit avec un autre qu'il ne connaît pas encore, celui-ci sera ajouté à sa liste d'amis avec un niveau d'amitIé qui dépendra de l'action en question. Lorsque le personnage est déjà dans la liste d'amis, son niveau d'amitié peut changer. Chacune des actions renvoie un bool indiquant si elle a réussi ou non.

Les objets

Modifier la classe Personnage de sorte que ses instances disposent d'un attribut objets de type liste d'Objet.

Par ailleurs :

  • modifiez le constructeur pour que lors de la création d'un Personnage on puisse fournir une liste d'Objet. Ce paramètre doit être optionnel. Lorsque aucun Objet n'est fourni, la liste des objets du Personnage sera initialisée à vide ;
  • créez les méthodes ajoutObjet et getObjects qui permettent respectivement d'ajouter un Objet à ceux détenus par le personnage et de récupérer la liste de ses objets. getObjects doit respecter les règles d'encapsulation.

Les tests unitaires pour valider votre code se trouvent ici

L'action vendre vendre(obj: Objet, autre) -> bool

Un Personnage vend l'un de ses propres objets (passé en argument) à un autre Personnage (passé également en argument, nommé ici autre). Cette action échoue lorsque l'objet passé en argument n'appartient pas au Personnage vendeur, ou si le Personnage acheteur ne dispose pas de suffisamment de richesse pour payer la valeur de l'Objet vendu.

À l'issue de la vente :

  • la richesse du vendeur a augmenté de la valeur de l'Objet;
  • la richesse de l'acheteur a diminué de la valeur de l'Objet;
  • l'Objet n'est plus présent dans les objets du vendeur ;
  • l'Objet est présent dans les objets de l'acheteur ;
  • le vendeur est ajouté à la liste des amis de l'acheteur avec un niveau d'amitié égal à zéro s'il n'y était pas déjà ;
  • l'acheteur est ajouté à la liste des amis du vendeur avec un niveau d'amitié égal à zéro s'il n'y était pas déjà.

Les tests unitaires pour valider Personnage.vendre() se trouvent ici

L'action acheter acheter(obj: Objet, autre) -> bool

La méthode acheter() est totalement symétrique de la méthode vendre(). On peut donc l'implémenter comme suit :

def acheter(self, obj: Objet, autre) -> bool:
    return autre.vendre(obj, self)

Les tests unitaires pour valider Personnage.acheter() se trouvent ici

L'action donner donner(obj: Objet, autre: Personnage) -> bool

Un Personnage donne l'un de ses propres objets (passé en argument) à un autre Personnage (passé également en argument, nommé ici autre). Cette action échoue lorsque l'objet passé en argument n'appartient pas au Personnage donateur.

À l'issue du don :

  • l'Objet n'est plus présent dans les objets du donateur ;
  • l'Objet est présent dans les objets du récipiendaire ;
  • le donateur est ajouté à la liste des amis du récipiendaire avec un niveau d'amitié égal à 0.5 s'il n'y était pas déjà ; s'il y était déjà, son niveau d'amitié augmente de 0.5 sans néanmoins pouvoir dépasser 1.0;
  • le récipiendaire est ajouté à la liste des amis du donateur avec un niveau d'amitié égal à O s'il n'y était pas déjà.

Les tests unitaires pour valider Personnage.donner() se trouvent ici

L'action prendre prendre(obj: Objet, autre) -> bool

Un Personnage prend l'un des objets appartenant à l'autre Personnage (tous les deux passés en argument). Cette action échoue notamment lorsque l'objet passé en argument n'appartient pas au Personnage volé.

Par ailleurs, la prise réussit si l'agresseur a une force strictement supérieure à celle de la victime ou, lors de forces égales, si l'agresseur a une obstination strictement supérieure à celle de la victime.

À l'issue de la prise :

  • l'Objet n'est plus présent dans les objets de la victime ;
  • l'Objet est présent dans les objets de l'agresseur ;
  • l'agresseur est ajouté à la liste des amis de la victime avec un niveau d'amitié égal à -1.0 s'il n'y était pas déjà ; s'il y était déjà, son niveau d'amitié est mis à -1.0;
  • la victime est ajoutée à la liste des amis de l'agresseur avec un niveau d'amitié égal à O s'il n'y était pas déjà ;
  • les points de vie de chaque Personnage sont diminués de la différence entre leurs forces respectives en valeur absolue ; Par exemple, si l'agresseur a une force de 15 et la victime une force de 5 les points de vie des deux sont diminués de 10 sans néanmoins pouvoir dépasser zéro.

Les tests unitaires pour valider Personnage.prendre() se trouvent ici

La méthode choisir_action(autre: Personnage) -> bool

Ajouter la méthode suivante à la classe Personnage. Analysez et commentez son fonctionnement. Vous remarquerez que vous aurez besoin d'ajouter un accesseur getObjets() à la classe.

from random import choice

class Personnage(object):

    ...

    def choisir_action(self, autre):
        i = randint(0,4)
        if i == 0:
            if autre.getObjets():
                return self.acheter(choice(autre.getObjets()), autre)
        elif i == 1:
            if autre.getObjets():
                return self.prendre(choice(autre.getObjets()), autre)
        elif i == 2:
            if self.objets:
                return self.vendre(choice(self.objets), autre)
        elif i == 3:
            if self.objets:
                return self.donner(choice(self.objets), autre)
        else:
            return True

        return False

Testez votre code

Exécutez le code ci-dessous plusieurs fois. Observez ce qui se passe.

if __name__ == "__main__":

    o1 = Objet("serviette", 30)
    o2 = Objet()
    o3 = Objet("cacahuète", 10)

    p1 = Personnage("Arthur")
    p2 = Personnage("Trillian",[o1, o2, o3])

    for _ in range(10):
        p1.choisir_action(p2)
        p2.choisir_action(p1)

    print(p1)
    print(p2)

Modifiez la méthode Personnage.__str__() pour afficher également les objets et les amitiés.

2. Les personnages

Nous allons maintenant introduire des personnages avec un comportement spécifique. Ceci sera obtenu en créant des classes dérivées de Personnage.

Chacune des classes a des propriétés particulières sur ses caractéristiques, à prendre en compte lors de leur création. Par ailleurs, ils auront des comportements différenciés, ce qui se caractérisera par une surcharge de certaines méthodes.

Modifiez la classe Personnage de telle façon qu'aucune action ne puisse avoir lieu avec un personnage dont les points de vie sont à zéro.

Le test unitaire se trouve ici.

Le charmeur

Lors de sa création, un personnage de type Charmeur se voit augmenter son charisme et son empathie de 30%. En revanche, sa force et son obstination sont diminuées de 30%.

Lorsqu'un Charmeur choisit une action il suit les règles suivantes :

  • lorsque son interlocuteur lui est inconnu, il lui donnera un de ses Objets s'il en possède ou ne fera rien s'il n'en possède pas ;
  • lorsque son interlocuteur lui est connu, il lui laisse choisir l'action, sauf si l'autre est un Charmeur également, auquel cas il appliquera le choix d'action de la classe Personnage.

Le test unitaire pour Charmeur est ici.

La brute

Lors de sa création, un personnage de type Brute se voit augmenter sa force et son obstination de 30%. En revanche, son intelligence et son charisme sont diminués de 30%.

Lorsqu'une Brute choisit une action il suit les règles suivantes :

  • lorsque son interlocuteur lui est inconnu, ou si son niveau d'amitié est inférieur ou égal à zéro, il lui prend un de ses objets aléatoirement, sauf s'il s'agit d'un Charmeurauquel cas il ne fera rien ;
  • lorsque son niveau d'amitié avec son interlocuteur est strictement supérieur à zéro, il choisira une action aléatoire entre l'achat, la vente ou ne rien faire, sauf s'il s'agit d'un Charmeur avec un niveau de charisme supérieur au sien, auquel cas il lui fera un don.

Le test unitaire pour Brute est ici.

Le négociateur

Lors de sa création, un personnage de type Négociateur se voit augmenter son intelligence et son obstination de 30%. En revanche, son empathie et sa force sont diminuées de 30%.

Lorsqu'un Négociateur choisit une action il suit les règles suivantes :

  • Si son intelligence est supérieure à celle de son interlocuteur, ou, à intelligence égale, si son obstination est strictement supérieure, il vendra ou achètera aléatoirement avec une marge de 15% en sa faveur s'il s'agit d'un ami dont le niveau d'amitié est supérieur à zéro, ou avec une marge de 30% en sa faveur lorsqu'il s'agit d'un inconnu ou un ami avec un niveau d'amitié inférieur ou égal à zéro.
  • Dans tous les cas, si son interlocuteur est un Charmeur il achètera avec une marge de 0%.
  • Sinon, dans les autres situations, il achètera ou vendra avec une marge de 0%.

La marge est calculée à partir de la valeur de l'objet vendu ou acheté. Lors d'un achat, le Négociateur achète au prix égal à la valeur moins la marge, lors d'une vente, il vend au prix égal à la valeur plus la marge.

Les tests unitaires pour Negociateur sont ici et ici.

Le calculateur

Lors de sa création, un personnage de type Calculateur se voit augmenter son intelligence et son obstination de 30%. En revanche, son empathie et sa force sont diminuées de 30%.

Lorsqu'un Calculateur choisit une action il suit les règles suivantes :

  • Si ses points de vie sont inférieurs à 10 il ne choisira jamais l'action prendre ;
  • si sa force est supérieure à celle de son adversaire, il prendra un de ses objets ;
  • sinon, si son interlocuteur est un Charmeur il lui vendra un objet ;
  • sinon, si son interlocuteur lui est connu et son niveau d'amitié est inférieur à zéro, il lui fera un don ;
  • sinon il ne fait rien dans 50% des cas ou prendra une action aléatoire dans les 50% d'autres cas.

Les tests unitaires pour Calculateur sont ici.

3. Tester le tout

Travail préalable

Vous trouverez ci-dessous du code permettant de tester votre travail.

Vous aurez au préalable à définir les accesseurs suivants :

class Personnage:
    def getAmis(self) -> Dict[Personnage, int]:
        # Retourne le dictionnaire des amis et leur niveau d'amitié d'un Personnage
        pass

    def getPV(self) -> float:
        # Retourne les points de vie d'un Personnage
        pass

    def getRichesse(self) -> float:
        # Retourne la totalité des richesses d'un Personnage
        # la totalité des richesses est définie par la somme des valeurs des objets
        # que possède un Personnage ainsi que la valeur de l'attribut self.richesse 
        pass

Les tests unitaires pour valider les accesseurs ci-dessus se trouvent ici

Un peu de lecture

Analysez le code fourni ci-dessous et complétez les commentaires en documentant correctement les trois fonctions et en complétant leur type de retour.

def get_popularite(pers: list): # Type de retour à fournir
    # <à compléter avec description de la fonction>
    popularite = { p:0 for p in pers }

    for p in pers:
        amis_de_p = p.getAmis()
        for autre in amis_de_p:
            popularite[autre] += amis_de_p[autre]

    return {k: v for k, v in sorted(popularite.items(), key=lambda item: item[1])}


def get_sante(pers: list): # Type de retour à fournir
    # <à compléter avec description de la fonction>
    sante = { p:round(p.getPV(),1) for p in pers }
    return {k: v for k, v in sorted(sante.items(), key=lambda item: item[1])}


def get_richesse(pers: list): # Type de retour à fournir
    # <à compléter avec description de la fonction>
    richesse = { p:round(p.getRichesse(),1) for p in pers }
    return {k: v for k, v in sorted(richesse.items(), key=lambda item: item[1])}

À vous de jouer !

Voici du code permettant de créer des personnages et de leur donner des objets. Le jeu va consister à tirer mille rencontres aléatoires entre les personnages et de leur faire exécuter une action quelconque.

À la fin du jeu, on affiche différents résultats.

if __name__ == "__main__":

    list_obs = [ Objet(f'Obj_{i}', 15) for i in range(50) ]

    alice = Brute("Alice", list_obs[:10])
    bob = Charmeur("Bob", list_obs[10:20])
    claire = Personnage("Claire", list_obs[20:30])
    daniel = Calculateur("Daniel", list_obs[30:40])
    elise = Negociateur("Elise", list_obs[40:])

    persos = [alice, bob, claire, daniel, elise]

    for _ in range(1000):
        perso1 = choice(persos)
        perso2 = perso1
        while perso2 == perso1:
            perso2 = choice(persos)

        perso1.choisir_action(perso2)

    for p in persos:
        print(p)

    print('Popularité')
    print(get_popularite(persos))

    print('Santé')
    print(get_sante(persos))

    print('Richesse')
    print(get_richesse(persos))

An observant plusieurs exécutions du jeu, quelles sont les meilleures stratégies à déployer pour jouer ? Quel type de personnage devient le plus souvent riche, lequel devient le plus populaire et lequel reste le plus longtemps en bonne santé ?