Attack vs Defense et qui est le vainqueur? [fermé]

12

Je suis en train de créer un nouveau jeu simple sur mobile et j'ai passé plusieurs jours sur la partie suivante.

Pour simplifier, disons que j'ai deux combattants. Le seul attribut d'entre eux est l'attaque et la défense. Lors des premières attaques, la seule chose qui compte c'est l'attaque de lui et la défense de l'adversaire. Et vice versa.

Ils n'ont pas d'équipement, d'articles, d'endurance ou de santé. Juste attaque contre défense.

Exemple:

  • Combattant 1:

    Attaque: 50, Défense: 35

  • Combattant 2:

    Attaque 20, défense: 80

Le processus de combat ne sera qu'une seule attaque qui déterminera le vainqueur. Donc, pas d'attaques ou de rounds multiples. Je ne veux pas le rendre déterministe, mais ajouter une version légère d'inattendu. Un combattant avec une attaque plus faible pourra gagner un autre combattant avec une défense plus élevée (mais bien sûr pas à chaque fois)

Ma première idée a été de le rendre linéaire et d'appeler un générateur de nombres aléatoires uniforme.

If Random() < att1 / (att1 + def2) {
    winner = fighter1
} else {
    winner = fighter2
} 

Exemple avec attaque 50 et défense 80, le combattant attaquant aura environ 38% à gagner. Cependant, il me semble que l'inattendu est trop loin et que les pires combattants gagneront beaucoup.

Je me demandais comment vous avez travaillé sur des situations similaires.

PS J'ai beaucoup cherché dans cette QnA et d'autres sources et j'ai trouvé des questions similaires mentionnées comme trop larges pour SE. Mais ceux-ci ont eu de nombreux attributs, armes, objets, classes, etc. qui pourraient compliquer les choses. Je pense que ma version est beaucoup plus simple pour l'adapter au style QnA de la SE.

Tasos
la source
1
Quels sont les cas que vous recherchez? Quelle plage de valeurs d'attaque et de défense examinez-vous et les deux nombres de ces plages devraient-ils jamais avoir un résultat fixe? Par exemple, un combattant avec l'attaque 10 peut-il vaincre un combattant à la défense 90?
Niels
@ user2645227 Je pourrais dire que la plage est comprise entre 1 et 400. Non, je ne veux pas avoir de décisions déterministes et donner la possibilité d'attaquer 1 gagner la défense 400, mais dans de très rares cas.
Tasos
1
Donc, si vous prenez Att (min) -def (max) et Att (max) -def (min), cela vous donne une plage de 800 de -400 à +400. Vous voudrez que votre plage aléatoire couvre toute la plage. Défense - L'attaque vous donnera une marge de mise à l'échelle sous la forme d'un seuil que vous devrez atteindre pour gagner. Cela devrait réduire un peu le caractère aléatoire. Pour centraliser davantage les résultats, vous pouvez utiliser l'exemple de Philipps ou jouer avec n'importe quel appareil jusqu'à ce que vous atteigniez la courbe que vous recherchez.
Niels

Réponses:

24

Si vous voulez que vos résultats de combat soient plus prévisibles mais pas complètement déterministes, ayez un système best of n .

Répétez les ntemps de combat (où ndevrait être un nombre inégal) et déclarez le combattant vainqueur qui a gagné le plus souvent. Plus votre valeur est élevée, nmoins vous gagnez et perdez de surprises.

const int FIGHT_REPETITONS = 5 // best 3 of 5. Adjust to taste.

int fighter1wins = 0;
int fighter2wins = 0;

for (int i = 0; I < FIGHT_REPETITONS; I++) {

    If (Random() < att1 / (att1 + def2)) {
        fighter1wins++;
    } else {
        fighter2wins++;
    } 

}

If (fighter1wins > fighter2wins) {
    winner = fighter1
} else {
    winner = fighter2
} 

Ce système ne fonctionne que dans le cas particulier où un combat est un simple résultat binaire de gagner ou de perdre. Lorsqu'un combat a des résultats plus complexes, comme lorsque le gagnant perd toujours des points de vie en fonction de la proximité de la victoire, cette approche ne fonctionne plus. Une solution plus générale consiste à changer la façon dont vous générez des nombres aléatoires. Lorsque vous générez plusieurs nombres aléatoires et que vous prenez ensuite la moyenne, les résultats se regrouperont près du centre de la plage et les résultats plus extrêmes seront plus rares. Par exemple:

double averagedRandom3() {
    return (Random() + Random() + Random()) / 3.0;
}

aura une courbe de distribution comme celle-ci:

Distribution de 3d20 / 3

(photo gracieuseté de anydice - un outil vraiment utile pour concevoir des formules de mécanique de jeu qui impliquent le hasard, pas seulement pour les jeux de table)

Dans mon projet actuel, j'utilise une fonction d'assistance qui permet de définir une taille d'échantillon arbitraire:

double averagedRandom(int averageness) {
     double result = 0.0;
     for (var i = 0; i < averageness; i++) {
         result += Random();
     }
     return result / (double)averageness;
}
Philipp
la source
Semble une meilleure approche. Une question. Dans la fonction averagedRandom3 (), devez-vous utiliser à la +place de *ou j'ai mal compris ce qu'il fait?
Tasos
@Tasos oui, devrait être +, pas *. J'ai également une fonction aléatoire qui multiplie plusieurs échantillons. Cela vous donne une fonction de nombre aléatoire avec un fort biais pour les valeurs inférieures, ce qui peut également être utile dans certaines situations.
Philipp
1
Je garderai la question ouverte pendant 1-2 jours et si je n'ai pas d'autre réponse, je choisirai la vôtre. J'ai voté pour, mais je veux aussi donner la chance à d'autres réponses si cela ne vous dérange pas.
Tasos
Je pense que cette réponse a déjà obtenu suffisamment de votes pour que cette réponse soit éligible au marquage comme réponse: P
Hamza Hasan
1
Je serais également curieux de savoir si certaines personnes proposent des approches alternatives. Une personne a rejeté cette réponse. Peut-être voudraient-ils en proposer une autre.
Philipp
8

C'est ce que j'ai utilisé pour déterminer le vainqueur d'une bataille dans mon applet Lords of Conquest Imitator. Dans ce jeu, similaire à votre situation, il n'y a qu'une valeur d'attaque et une valeur de défense. La probabilité que l'attaquant gagne est plus grande plus l'attaquant a de points et moins la défense a de points, avec des valeurs égales évaluant à 50% de chances de réussite de l'attaque.

Algorithme

  1. Lancez une pièce au hasard.

    1a. Têtes: la défense perd un point.

    1b. Queues: les têtes perdent un point.

  2. Si la défense et l'attaquant ont encore des points, revenez à l'étape 1.

  3. Celui qui est à 0 point perd la bataille.

    3a. Attaquant jusqu'à 0: l'attaque échoue.

    3b. Défense jusqu'à 0: l'attaque réussit.

Je l'ai écrit en Java, mais il devrait être facilement traduisible dans d'autres langues.

Random rnd = new Random();
while (att > 0 && def > 0)
{
    if (rnd.nextDouble() < 0.5)
        def--;
    else
        att--;
}
boolean attackSucceeds = att > 0;

Un exemple

Par exemple, disons que att = 2 et def = 2, juste pour s'assurer que la probabilité est de 50%.

La bataille sera décidée en un maximum de n = att + def - 1lancers de pièces, ou 3 dans cet exemple (c'est essentiellement le meilleur des 3 ici). Il y a 2 n combinaisons possibles de lancer de pièces. Ici, "W" signifie que l'attaquant a gagné le lancer de pièces et "L" signifie que l'attaquant a perdu le lancer de pièces.

L,L,L - Attacker loses
L,L,W - Attacker loses
L,W,L - Attacker loses
L,W,W - Attacker wins
W,L,L - Attacker loses
W,L,W - Attacker wins
W,W,L - Attacker wins
W,W,W - Attacker wins

L'attaquant gagne dans 4/8, soit 50% des cas.

Les maths

Les probabilités mathématiques résultant de cet algorithme simple sont plus compliquées que l'algorithme lui-même.

Le nombre de combinaisons où il y a exactement x Ls est donné par la fonction de combinaison:

C(n, x) = n! / (x! * (n - x)!)

L'attaquant gagne lorsqu'il y a entre 0et att - 1Ls. Le nombre de combinaisons gagnantes est égal à la somme des combinaisons de à 0travers att - 1, une distribution binomiale cumulative:

    (att - 1)
w =     Σ     C(n, x)
      x = 0

La probabilité que l'attaquant gagne est w divisée par 2 n , une probabilité cumulée binomiale:

p = w / 2^n

Voici le code en Java pour calculer cette probabilité d'arbitraire attet de defvaleurs:

/**
 * Returns the probability of the attacker winning.
 * @param att The attacker's points.
 * @param def The defense's points.
 * @return The probability of the attacker winning, between 0.0 and 1.0.
 */
public static double probWin(int att, int def)
{
    long w = 0;
    int n = att + def - 1;
    if (n < 0)
        return Double.NaN;
    for (int i = 0; i < att; i++)
        w += combination(n, i);

    return (double) w / (1 << n);
}

/**
 * Computes C(n, k) = n! / (k! * (n - k)!)
 * @param n The number of possibilities.
 * @param k The number of choices.
 * @return The combination.
 */
public static long combination(int n, int k)
{
    long c = 1;
    for (long i = n; i > n - k; i--)
        c *= i;
    for (long i = 2; i <= k; i++)
        c /= i;
    return c;
}

Code de test:

public static void main(String[] args)
{
    for (int n = 0; n < 10; n++)
        for (int k = 0; k <= n; k++)
            System.out.println("C(" + n + ", " + k + ") = " + combination(n, k));

    for (int att = 0; att < 5; att++)
        for (int def = 0; def < 10; def++)
            System.out.println("att: " + att + ", def: " + def + "; prob: " + probWin(att, def));
}

Production:

att: 0, def: 0; prob: NaN
att: 0, def: 1; prob: 0.0
att: 0, def: 2; prob: 0.0
att: 0, def: 3; prob: 0.0
att: 0, def: 4; prob: 0.0
att: 1, def: 0; prob: 1.0
att: 1, def: 1; prob: 0.5
att: 1, def: 2; prob: 0.25
att: 1, def: 3; prob: 0.125
att: 1, def: 4; prob: 0.0625
att: 1, def: 5; prob: 0.03125
att: 2, def: 0; prob: 1.0
att: 2, def: 1; prob: 0.75
att: 2, def: 2; prob: 0.5
att: 2, def: 3; prob: 0.3125
att: 2, def: 4; prob: 0.1875
att: 2, def: 5; prob: 0.109375
att: 2, def: 6; prob: 0.0625
att: 3, def: 0; prob: 1.0
att: 3, def: 1; prob: 0.875
att: 3, def: 2; prob: 0.6875
att: 3, def: 3; prob: 0.5
att: 3, def: 4; prob: 0.34375
att: 3, def: 5; prob: 0.2265625
att: 3, def: 6; prob: 0.14453125
att: 3, def: 7; prob: 0.08984375
att: 4, def: 0; prob: 1.0
att: 4, def: 1; prob: 0.9375
att: 4, def: 2; prob: 0.8125
att: 4, def: 3; prob: 0.65625
att: 4, def: 4; prob: 0.5
att: 4, def: 5; prob: 0.36328125
att: 4, def: 6; prob: 0.25390625
att: 4, def: 7; prob: 0.171875
att: 4, def: 8; prob: 0.11328125

Observations

Les probabilités sont 0.0si l'attaquant a des 0points, 1.0si l'attaquant a des points mais la défense a des 0points, 0.5si les points sont égaux, moins que 0.5si l'attaquant a moins de points que la défense, et plus grand que 0.5si l'attaquant a plus de points que la défense .

En prenant att = 50et def = 80, je devais passer à BigDecimals pour éviter un débordement, mais j'obtiens une probabilité d'environ 0,0040.

Vous pouvez rapprocher la probabilité de 0,5 en modifiant la attvaleur pour qu'elle soit la moyenne des valeurs attet def. Att = 50, Def = 80 devient (65, 80), ce qui donne une probabilité de 0,1056.

rgettman
la source
1
Une autre approche intéressante. L'algorithme pourrait également être facilement visualisé, ce qui pourrait sembler très excitant.
Philipp
5

Vous pouvez modifier l'attaque par un nombre aléatoire échantillonné à partir d'une distribution normale. De cette façon, la plupart du temps, le résultat sera ce que vous attendez, mais parfois une attaque plus élevée perdra contre une défense inférieure ou une attaque inférieure gagnera contre une défense plus élevée. La probabilité que cela se produise diminue à mesure que la différence entre l'attaque et la défense augmente.

if (att1 + norm(0, sigma) - def2 > 0) {
  winner = fighter1;
}
else {
  winner = fighter2;
}

La fonction norm(x0, sigma)renvoie un flottant échantillonné à partir d'une distribution normale centrée sur x0, avec un écart-type sigma. La plupart des langages de programmation fournissent une bibliothèque avec une telle fonction, mais si vous voulez la faire vous-même, jetez un œil à cette question . Vous devrez ajuster le sigma de telle sorte qu'il se sente bien, mais une valeur de 10-20 pourrait être un bon point de départ.

Pour quelques valeurs sigma, la probabilité de victoire pour une donnée att1 - def2ressemble à ceci: Probabilité de victoire

Rob
la source
Il peut également être utile de souligner que les valeurs distribuées normales n'ont pas de limites réelles, donc lors de l'utilisation de valeurs aléatoires distribuées normalement dans un jeu, il peut être judicieux de restreindre le résultat pour éviter la situation improbable mais pas impossible de génération de valeurs très extrêmes qui pourrait casser le jeu.
Philipp