Ataque vs Defensa y ¿quién es el ganador? [cerrado]

12

Estoy en el proceso de crear un nuevo juego simple en dispositivos móviles y he pasado varios días en la siguiente parte.

Por simplicidad, digamos que tengo dos luchadores. El único atributo de ellos es Ataque y Defensa. Cuando los primeros ataques, lo único que importa es el ataque de él y la defensa del oponente. Y viceversa.

No tienen equipo, artículos, resistencia o salud. Solo ataque vs defensa.

Ejemplo:

  • Luchador 1:

    Ataque: 50, Defensa: 35

  • Luchador 2:

    Ataque 20, Defensa: 80

El proceso de lucha será solo un ataque que determinará el ganador. Por lo tanto, no hay múltiples ataques o rondas. No quiero que sea determinista, pero agregue una versión ligera de inesperado. Un luchador con un ataque más bajo podrá ganar a otro luchador con una defensa más alta (pero, por supuesto, no siempre)

Mi primera idea fue hacerlo lineal y llamar a un generador uniforme de números aleatorios.

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

Ejemplo con ataque 50 y defensa 80, el luchador atacante tendrá alrededor del 38% para ganar. Sin embargo, me parece que lo inesperado está demasiado lejos y que los peores peleadores ganarán mucho.

Me preguntaba cómo has trabajado en situaciones similares.

PD. Busqué mucho en este QnA y en otras fuentes y encontré preguntas similares mencionadas como demasiado amplias para SE. Pero esos han tenido muchos atributos, armas, artículos, clases, etc. que podrían hacerlo demasiado complicado. Creo que mi versión es mucho más simple para encajar en el estilo QnA del SE.

Tasos
fuente
1
¿Cuáles son los casos que estás buscando? ¿Qué rango de valores para ataque y defensa estás mirando? ¿Debería alguna vez tener un resultado fijo dos números en esos rangos? Por ejemplo, ¿puede un luchador con ataque 10 derrotar a un luchador en defensa 90?
Niels
@ user2645227 Podría decir que el rango está entre 1 y 400. No, no quiero tomar decisiones deterministas y dar la posibilidad de atacar 1 y ganar la defensa 400, pero en casos realmente raros.
Tasos
1
Entonces, si toma Att (min) -def (max) y Att (max) -def (min) eso le da un rango de 800 de -400 a +400. Querrás que tu rango aleatorio cubra todo el rango. Defensa: el ataque te dará un margen de escala en forma de umbral que deberás golpear para ganar. Esto debería reducir un poco la aleatoriedad. Para centralizar aún más los resultados, puede usar el ejemplo de Philipps o jugar en cualquier momento hasta que llegue a la curva que está buscando.
Niels

Respuestas:

24

Si desea que los resultados de su pelea sean más predecibles pero no completamente deterministas, tenga lo mejor de n sistema.

Repita los ntiempos de pelea (donde ndebería haber un número desigual) y declare al combatiente el ganador que ganó con más frecuencia. Cuanto mayor sea su valor por nmenos sorpresas, ganará y perderá.

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
} 

Este sistema solo funciona en el caso especial donde una pelea es un simple resultado binario de ganar o perder. Cuando un combate tiene resultados más complejos, como cuando el ganador todavía pierde algunos puntos de golpe dependiendo de cuán cerca estuvo la victoria, este enfoque ya no funciona. Una solución más general es cambiar la forma de generar números aleatorios. Cuando genera múltiples números aleatorios y luego toma el promedio, los resultados se agruparán cerca del centro del rango y los resultados más extremos serán más raros. Por ejemplo:

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

tendrá una curva de distribución como esta:

Distribución de 3d20 / 3

(Imagen cortesía de anydice : una herramienta realmente útil para diseñar fórmulas mecánicas de juego que implican aleatoriedad, no solo para juegos de mesa)

En mi proyecto actual estoy usando una función auxiliar que permite establecer un tamaño de muestra arbitrario:

double averagedRandom(int averageness) {
     double result = 0.0;
     for (var i = 0; i < averageness; i++) {
         result += Random();
     }
     return result / (double)averageness;
}
Philipp
fuente
Parece un mejor enfoque. Una pregunta. En la función averagedRandom3 (), ¿debería usar en +lugar de *o no entendí lo que hace?
Tasos
@Tasos sí, debería ser +, no *. También tengo una función aleatoria que multiplica múltiples muestras. Esto le da una función de número aleatorio con un fuerte sesgo para valores más bajos, que también puede ser útil en algunas situaciones.
Philipp
1
Mantendré la pregunta abierta durante 1-2 días y si no tengo otra respuesta, elegiré la suya. Lo he votado pero quiero dar la oportunidad de otras respuestas también si no te importa.
Tasos
Creo que esta respuesta ya tiene suficientes votos que hacen que esta respuesta sea elegible para marcarla como respuesta: P
Hamza Hasan
1
También me gustaría saber si a algunas personas se les ocurren enfoques alternativos. Una persona rechazó esta respuesta. Quizás les gustaría ofrecer una alternativa.
Philipp
8

Esto es lo que solía determinar el ganador de una batalla en mi applet Imitator de Lords of Conquest. En este juego, similar a tu situación, solo hay un valor de ataque y un valor de defensa. La probabilidad de que el atacante gane es mayor cuantos más puntos tenga el atacante, y menos puntos tenga la defensa, con valores iguales que evalúan un 50% de probabilidad de que el ataque tenga éxito.

Algoritmo

  1. Lanza una moneda al azar.

    1a. Jefes: la defensa pierde un punto.

    1b. Colas: las cabezas pierden un punto.

  2. Si tanto la defensa como el atacante aún tienen puntos, regrese al paso 1.

  3. Quien tenga 0 puntos pierde la batalla.

    3a. Atacante hasta 0: el ataque falla.

    3b. Defensa hasta 0: el ataque tiene éxito.

Lo escribí en Java, pero debería ser fácilmente traducible a otros idiomas.

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

Un ejemplo

Por ejemplo, supongamos que att = 2 y def = 2, solo para asegurarse de que la probabilidad sea del 50%.

La batalla se decidirá en un máximo de n = att + def - 1lanzamientos de monedas, o 3 en este ejemplo (es esencialmente el mejor de 3 aquí). Hay 2 n combinaciones posibles de lanzamientos de monedas. Aquí, "W" significa que el atacante ganó el lanzamiento de la moneda, y "L" significa que el atacante perdió el lanzamiento de la moneda.

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

El atacante gana en 4/8, o 50% de los casos.

Las matemáticas

Las probabilidades matemáticas que surgen de este algoritmo simple son más complicadas que el algoritmo mismo.

El número de combinaciones donde exactamente x Ls viene dado por la función de combinación:

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

El atacante gana cuando hay entre 0y att - 1Ls. El número de combinaciones ganadoras es igual a la suma de combinaciones de a 0través att - 1, una distribución binomial acumulativa:

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

La probabilidad de que el atacante ganadora es w divide por 2 n , una probabilidad acumulativa binomial:

p = w / 2^n

Aquí está el código en Java para calcular esta probabilidad para valores arbitrarios atty def:

/**
 * 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;
}

Código de prueba:

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));
}

Salida:

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

Observaciones

Las probabilidades son 0.0si el atacante tiene 0puntos, 1.0si el atacante tiene puntos pero la defensa tiene 0puntos, 0.5si los puntos son iguales, menos que 0.5si el atacante tiene menos puntos que la defensa, y mayores que 0.5si el atacante tiene más puntos que la defensa .

Tomando att = 50y def = 80, necesitaba cambiar a BigDecimals para evitar el desbordamiento, pero tengo una probabilidad de aproximadamente 0.0040.

Puede acercar la probabilidad a 0,5 cambiando el attvalor para que sea el promedio de los valores atty def. Att = 50, Def = 80 se convierte en (65, 80), lo que arroja una probabilidad de 0.1056.

rgettman
fuente
1
Otro enfoque interesante. El algoritmo también podría visualizarse fácilmente, lo que podría parecer bastante emocionante.
Philipp
5

Puede modificar el ataque mediante un número aleatorio muestreado de una distribución normal. De esta manera, la mayoría de las veces el resultado será el esperado, pero ocasionalmente un ataque más alto perderá contra una defensa más baja o un ataque más bajo ganará contra una defensa más alta. La probabilidad de que esto ocurra será menor a medida que aumente la diferencia entre ataque y defensa.

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

La función norm(x0, sigma)devuelve un flotador muestreado de una distribución normal centrada en x0, con desviación estándar sigma. La mayoría de los lenguajes de programación proporcionan una biblioteca con dicha función, pero si desea hacerlo usted mismo, eche un vistazo a esta pregunta . Tendría que ajustar sigma de modo que 'se sienta bien', pero un valor de 10-20 podría ser un buen lugar para comenzar.

Para algunos valores sigma, la probabilidad de victoria para un dado se att1 - def2ve así: Probabilidad de victoria

Robar
fuente
También podría valer la pena señalar que los valores distribuidos normales no tienen límites reales, por lo que cuando se usan valores aleatorios distribuidos normalmente en un juego, puede tener sentido fijar el resultado para evitar la situación improbable pero no imposible de generar valores muy extremos que Podría romper el juego.
Philipp