Cours POO Gestion des pourcentages de réussite pour une attaque

App_Console.zip (145,8 Ko)

Bonjour à tous

Je suis le cours de programmation orientée objet Swift.
Je viens de programmer un petit bout de code sur la gestion de plusieurs types d’attaques avec un pourcentage de réussite.

J’aimerais avoir votre avis et peut être une autre façon de gérer les pourcentage de réussite.
Merci d’avance.

 func strengthAttackChoice(bot:Bot) {
        var userChoice:Int
        userChoice = Utilisateur.choisirOptionMenu(message: "Selectionner votre attaque :\n1 - Force divisee par 2 -> 100% de reussite\n2 - Normal -> 75% de reussite\n3 - Force doublee - > 50% de reussite", max: 3)
        switch userChoice {
            case 1:
                //Attaque faible avec 75% de reussite
                let hitResult = hitStrength(bot: bot, factor: 0.5)
                print("\(self.pseudo) assene un coup sur \(bot.pseudo) avec une force de \(hitResult)")
            case 2:
                //Attaque normal avec 75% de reussite
                let sucessRate:Int = Int.random(in: 1...4)
                if sucessRate != 4 {
                    let hitResult = hitStrength(bot: bot, factor: 1)
                    print("Attaque normale reussie !!!\n\(self.pseudo) assene un coup sur \(bot.pseudo) avec une force de \(hitResult)")
                } else {
                    print("Oups, Votre attaque a echoue!!")
                }
            case 3:
                //Super attaque avec 50% de reussite
                let sucessRate:Int = Int.random(in: 1...4)
                if sucessRate == 4 || sucessRate == 3 {
                    let hitResult = hitStrength(bot: bot, factor: 2)
                    print("Attaque super forte reussie !!!\n\(self.pseudo) assene un coup sur \(bot.pseudo) avec une force de \(hitResult)")
                } else {
                    print("Votre attaque a echoue!!")
                }
            default: break
        }
    }

???

// Gestion tirage 50%
if Int.random(in: 1...100) <= 50 {
  // Réussite tirage 50%
  // ....
} else {
  // Echec tirage 50%
  // ...
}

Si une information n’est pas utile pour le déroulement du programme, il ne sert à rien de la stocker dans une variable intermédiaire.

Et la meilleure manière de gérer un tirage de probabilité c’est de tirer un chiffre aléatoire entre 1 et 100.

EDIT : Ceci dis, l’utilisation de variables entières n’est pas appropriée pour les pourcentages, qui sont fréquemment exprimés avec des chiffres à virgule. Par exemple 23,67 %.

Il faut utiliser des variables flottantes à virgule.

// Gestion tirage 50%
if Float.random(in: 1.0...100.0) <= 50.0 {
  // Réussite tirage 50%
  // ....
} else {
  // Echec tirage 50%
  // ...
}

(code tapé à l’arrache, sans vérification avec Xcode)

L’exemple de Maxime utilise des entiers parce qu’il s’est inspiré du générateur aléatoire le plus simple qui soit : le pile ou face d’une pièce de monnaie, donc 1 ou 2.

1 « J'aime »

Bonjour,
Désolé pour la présentation du message, merci @mbritto pour la modification. Je ferai plus attention la prochaine fois.
En effet c’est beaucoup plus simple, facile a lire et beaucoup plus léger. J’ai cherché encore une fois quelque chose de trop compliqué.
Merci beaucoup @Draken, ça m’aide beaucoup.

T’inquiète, tous les débutants font la même chose ! ça disparait après avoir réalisé pas mal d’exercices.

2 « J'aime »

Pas de soucis, tu avais bien pensé à tenter une mise en forme pour le code, c’était juste pas la bonne car tu avais pris l’outil pour les citations au lieu de celui pour le code :wink:

Petite amélioration : l’un des principes de la programmation objet est de cacher les détails techniques. Le code gérant les événements n’a pas besoin de savoir comment tirer une probabilité, juste d’accéder à un mécanisme le faisant à sa place.

On peut faire ça en ajoutant une nouvelle fonction à la classe Player.

  // Tirage aléatoire
  func tirageAleatoire(probabilite:Double) -> Bool {
    if Double.random(in: 0.0 ... 100.0) <= probabilite {
      return true
    }
    return false
  }

J’ai ajusté la plage de probabilité de 0.0 à 100.0, car c’est mathématiquement plus exact que de commencer à 1, surtout pour des variables flottantes.

J’ai choisi le type Double pour gérer les chiffres à virgules, car c’est le type flottant le plus utilisé en Swift.

Utilisation :

//Attaque normal avec 75% de reussite
if self.tirageAleatoire(probabilite: 75.0) {
   // ... réussite
} else {
   // ... échec
}

Le code est plus lisible. Mais surtout l’algorithme de probabilité n’est présent qu’à un seul endroit dans le code. Si pour une raison quelconque, tu veux modifier la manière de calculer une probabilité, il suffit de mettre à jour la fonction tirageAleatoire() et c’est bon.

Si la méthode de calcul est présente à plusieurs endroits dans le programme, il faut modifier plusieurs lignes pour mettre le code à jour, ce qui peut sembler facile sur un code court tapé hier, mais pas forcément si le travail date de 3 mois, ou plus ! Sans même parler d’un listing de 3000 lignes. C’est une source inépuisable de bugs !

Centraliser les algorithmes, en les regroupant dans des fonctions spécialisées permet de s’éviter bien des problèmes par la suite. C’est l’un des principes de base de la bonne programmation.


Pourquoi des probabilités exprimées avec des variables flottantes et non des entiers, comme dans les exemple de Maxime ?

Parce que c’est beaucoup plus souple et évolutif. Exemple : le mécanisme actuel ne tient pas compte des dégâts reçus. Dans un combat réel, les blessures ou les dégâts diminuent les capacités offensives, sans parler de la fatigue. Il suffit de regarder un combat dans une série médiévale (non, je n’ai pas dit Games Of Thrones, je l’ai juste pensé), pour constater que les combattants sont nettement moins fringants après avoir encaissé quelques coups.

Dans le cas des boots, la violence des coups peut abimer les mécanismes internes, altérant les capacités offensifs. Sans parler d’attaques spéciales, comme des jets d’acides ou des lances-flammes.

On pourrait simuler ça en ajoutant un paramètre vitalite, calculé en fonction des points de vie maximum et de l’état de santé courant. Par exemple, un Joueur avec 100 points de vie au début, et n’ayant plus que 67 points de vie aura une vitalité de 67%.

Sa force d’attaque sera diminué, et ne sera plus que :

force théorique (100%, 75% ou 50%) * vitalite

Dans le cas d’une attaque à 75%, cela donne :

75% * 0.67 = 50.25%

D’où l’intérêt d’utiliser des variables flottantes pour avoir un résultat exact.

C’est un exemple assez violent. On peut utiliser une courbe de vitalité plus faible, pour refléter la perte de puissance offensive en cas de blessures.

On pourrais aussi gérer des bonus et malus de combat. Par exemple, le joueur pourrais perdre 10% de ses capacités offensives si le Boot lui crache de l’acide au visage.

force attaque = force théorique * (100 - malus acide)

Dans le cas d’une attaque à 75% :

force réel = 75% * (1.0-0.1) = 75% * 0.9 = 67.5%

Petit détail technique, on code généralement les pourcentages par des nombres flottants compris entre 0.0 et 1.0. C’est pratique pour simplifier les lignes de code, quand plusieurs pourcentages sont utilisés.

Pour obtenir 79% d’une valeur, il suffit de la multiplier par 0.79.

Dans certains jeux, comme les jeux de rôles ou de stratégie militaire, on peut avoir une longue liste de malus ou de bonus appliqués sur les caractéristiques de base des combattants, pour réfléter l’état du terrain, du moral, etc …

Exemple de base : un combattant chargeant l’ennemi depuis une hauteur à un gros bonus d’attaque.

2 « J'aime »

Voici une variante du tirage aléatoire, utilisant un pourcentage exprimé sous forme d’une valeur flottante comprise entre 0 et 1.

// Tirage aléatoire d'un pourcentage exprimé 
// par un Double compris entre 0 et 1
  func tirageAleatoire(probabilite:Double) -> Bool {
    if Double.random(in: 0.0 ... 1.0) <= probabilite {
      return true
    }
    return false
  }

Formules pour calculer des malus et des bonus :

  func ajouterMalus(pourcentage:Double, malus:Double) -> Double {
     return pourcentage * (1-malus)
  }
  
  func ajouterBonus(pourcentage:Double, bonus:Double) -> Double {
    return pourcentage * (1+bonus)
  }

Exemple d’une attaque de 75%, avec un malus de 20% :

0,75 * (1-0,2) = 0,6
ce qui correspond à 60%

Attaque de 75%, avec un bonus de 15% :

0,75 * (1+0,15) = 0.75*1.15 = 0,8625
nouvelle probabilité de 86.25%

Certaines probabilités peuvent dépasser les 100%, ce qui peut être choquant pour un esprit humain. Mais ce n’est pas grave, puisque cela correspond à une réussite automatique au tirage aléatoire.


L’un des gros avantages de la programmation objet, est de pouvoir ajouter des propriétés à un objet pour étendre ses capacités.

J’ai modifié la classe Bot pour lui ajouter le calcul automatique de la vitalité, dont j’ai parlé dans un autre post, plus haut. A sa création un Bot a une vitalité de 1.0, qui diminue au fur et à mesure de la perte des points de vie. A la moitié des points de vie, la vitalité est de 0,5.

class Bot {
    let pseudo:String = "Bot"
    let strength:Int
    var health:Int = 100
    var maxHealth:Int
  
    // Vitalité (comprise entre 0.0 et 1.0)
    var vitalite : Double {
      get { return Double(Double(health)/Double(maxHealth)) }
    }
    
    init(strength:Int) {
        self.strength = strength
        // Mémorisation Points de Vie max à la création du Bot
        self.maxHealth = self.health
    }
    
   // ....

}

Pour ce faire, j’ai ajouté deux variables à la classe : maxHealth pour mémoriser le nombre de points de vie initiale et une variable contenant la vitalité.

Calcul vitalité = points de vie courant / points de vie max

La variable vitalite est un peu particulière. Je ne sais pas si tu as déjà rencontré cette syntaxe à ce stade de ton apprentissage. C’est une variable calculée, c’est à dire que sa valeur n’est pas stockée en mémoire, mais recalculé à chaque utilisation à partir de l’état interne de l’objet. C’est pratique, parce que la vitalité dépend du nombre de points de vie, qui peut changer à tout moment.

    // Vitalité (comprise entre 0.0 et 1.0)
    var vitalite : Double {
      get { return Double(Double(health)/Double(maxHealth)) }
    }

On peut imaginer d’autres modifications du Bot, par exemple une fonction de réparation redonnant des points de vie.

  func reparationMajeur() {
    self.health = self.maxHealth
  }

Et :

  func reparationMineur() {
    self.health += 10
    // Test pour éviter de dépasser le nb max de vie
    if (self.health > self.maxHealth) {
      self.health = self.maxHealth
    }
  }

Attention, j’ai tapé le code à l’arrache, sans faire de tests. Les principes sont corrects, mais je ne garantit pas que cela fonctionne parfaitement, du premier coups.

2 « J'aime »

Merci beaucoup @Draken.
Tes réponses m’ont ouvert beaucoup de possibilité d’amélioration et de simplification de code.
Je comprend mieux le caractère évolutif des variables flottantes, il faut toujours s’attacher à avoir le code le plus simple possible pour ne pas galérer avec les futurs évolutions du code.
J’espère que je pourrais un jour coder comme çà.
Je suis ébahie sur le fait qu’avec si peu de code vous arrivez à réaliser des choses aussi complexes.

En tout cas, je suis très content, je vais encore faire évoluer mon code et m’entrainer. Un énorme merci!

C’est vraiment passionnant

1 « J'aime »

A ton service. N’hésites pas à poser d’autres questions, si nécessaire. La programmation objet est un gros morceau à aborder pour un novice.

1 « J'aime »