Ataque vs Defesa e quem é o vencedor? [fechadas]

12

Estou no processo de criar um novo jogo simples no celular e passei vários dias na parte seguinte.

Por simplicidade, digamos que tenho dois lutadores. O único atributo deles é Ataque e Defesa. Quando os primeiros ataques, a única coisa que importa é o ataque dele e a defesa do oponente. E vice versa.

Eles não têm equipamentos, itens, resistência ou saúde. Apenas ataque contra defesa.

Exemplo:

  • Lutador 1:

    Ataque: 50, Defesa: 35

  • Lutador 2:

    Ataque 20, Defesa: 80

O processo de luta será apenas um ataque que determinará o vencedor. Portanto, não há múltiplos ataques ou rodadas. Não quero torná-lo determinístico, mas adicione uma versão leve do inesperado. Um lutador com ataque mais baixo será capaz de vencer outro lutador com maior defesa (mas é claro que nem sempre)

Minha primeira idéia foi torná-lo linear e chamar um gerador de números aleatórios uniforme.

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

Exemplo no ataque 50 e defesa 80, o lutador atacante terá cerca de 38% para vencer. No entanto, parece-me que o inesperado é longe demais e os piores lutadores ganharão muito.

Fiquei me perguntando como você trabalhou em situações semelhantes.

PS Pesquisei bastante neste QnA e em outras fontes e encontrei perguntas semelhantes mencionadas como muito amplas para o SE. Mas esses tiveram muitos atributos, armas, itens, classes, etc, que podem tornar muito complicado. Eu acho que minha versão é muito mais simples de encaixar no estilo QnA do SE.

Tasos
fonte
1
Quais são os casos que você está procurando? Que faixa de valores para ataque e defesa você está observando e quaisquer dois números nessas faixas devem ter um resultado fixo? Por exemplo, um lutador com ataque 10 pode derrotar um lutador na defesa 90?
Niels
@ user2645227 Eu poderia dizer que o intervalo está entre 1 e 400. Não, não quero tomar nenhuma decisão determinística e dar a possibilidade de atacar uma vitória na defesa 400, mas em casos muito raros.
Tasos
1
Portanto, se você usar Att (min) -def (max) e Att (max) -def (min), você terá um intervalo de 800 a -400 a +400. Você deseja que seu intervalo aleatório cubra todo o intervalo. Defesa - O ataque fornecerá uma margem de expansão na forma de um limite que você precisará atingir para vencer. Isso deve reduzir um pouco a aleatoriedade. Para centralizar ainda mais os resultados, você pode usar o exemplo de Philipps ou mexer em qualquer dado até atingir a curva que está procurando.
Niels

Respostas:

24

Se você deseja que os resultados da luta sejam mais previsíveis, mas não completamente determinísticos, tenha o melhor sistema de n .

Repita os ntempos de luta (onde ndeve haver um número desigual) e declare o combatente o vencedor que venceu com mais frequência. Quanto maior o seu valor, nmenos surpresas você terá.

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 funciona apenas no caso especial em que uma luta é um resultado binário simples de vitória ou derrota. Quando um combate tem resultados mais complexos, como quando o vencedor ainda perde alguns pontos de vida, dependendo da proximidade da vitória, essa abordagem não funciona mais. Uma solução mais geral é mudar a maneira como você gera números aleatórios. Quando você gera vários números aleatórios e calcula a média, os resultados se agrupam perto do centro do intervalo e resultados mais extremos serão mais raros. Por exemplo:

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

terá uma curva de distribuição como esta:

Distribuição do 3d20 / 3

(imagem cortesia de anydice - uma ferramenta realmente útil para projetar fórmulas mecânicas de jogos que envolvem aleatoriedade, não apenas para jogos de mesa)

No meu projeto atual, estou usando uma função auxiliar que permite definir um tamanho de amostra arbitrário:

double averagedRandom(int averageness) {
     double result = 0.0;
     for (var i = 0; i < averageness; i++) {
         result += Random();
     }
     return result / (double)averageness;
}
Philipp
fonte
Parece uma abordagem melhor. Uma questão. Na função averagedRandom3 (), você deve usar em +vez de *ou entendi mal o que faz?
Tasos
@Tasos sim, deve ser +, não *. Eu também tenho uma função aleatória que multiplica várias amostras. Isso fornece uma função de número aleatório com forte viés para valores mais baixos, o que também pode ser útil em algumas situações.
Philipp
1
Manterei a pergunta em aberto por 1-2 dias e, se não tiver outra resposta, escolherei a sua. Eu votei positivo, mas quero dar a chance de outras respostas também se você não se importa.
Tasos
Eu acho que essa resposta já conseguir votos suficientes que faz com que esta resposta elegível para marcá-lo como resposta: P
Hamza Hasan
1
Eu também ficaria curioso se algumas pessoas apresentarem abordagens alternativas. Uma pessoa votou negativamente nesta resposta. Talvez eles gostariam de fornecer uma alternativa.
Philipp
8

Foi isso que eu usei para determinar o vencedor de uma batalha no meu applet Lords of Conquest Imitator. Neste jogo, semelhante à sua situação, há apenas um valor de ataque e um valor de defesa. A probabilidade de o atacante vencer é maior, mais pontos o atacante tem e menos pontos a defesa tem, com valores iguais avaliando a chance de 50% do ataque ter êxito.

Algoritmo

  1. Jogue uma moeda aleatória.

    1a. Cabeças: a defesa perde um ponto.

    1b. Caudas: as cabeças perdem um ponto.

  2. Se a defesa e o atacante ainda tiverem pontos, volte para a etapa 1.

  3. Quem está abaixo de 0 pontos perde a batalha.

    3a. Atacante até 0: o ataque falha.

    3b Defesa até 0: O ataque foi bem-sucedido.

Eu escrevi em Java, mas deve ser facilmente traduzível para outras línguas.

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

Um exemplo

Por exemplo, digamos que att = 2 e def = 2, apenas para garantir que a probabilidade seja de 50%.

A batalha será decidida no máximo de n = att + def - 1lançamentos de moedas, ou 3 neste exemplo (é essencialmente o melhor de 3 aqui). Existem 2 n combinações possíveis de moeda vira. Aqui, "W" significa que o atacante ganhou o lançamento da moeda e "L" significa que o atacante perdeu o lançamento da moeda.

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

O atacante vence em 4/8, ou 50% dos casos.

A matemática

As probabilidades matemáticas decorrentes desse algoritmo simples são mais complicadas do que o próprio algoritmo.

O número de combinações em que exatamente x Ls é dado pela função de combinação:

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

O atacante vence quando há entre 0e att - 1Ls. O número de combinações vencedoras é igual à soma das combinações de 0through att - 1, uma distribuição binomial cumulativa:

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

A probabilidade de o atacante vencer é w dividida por 2 n , uma probabilidade binomial cumulativa:

p = w / 2^n

Aqui está o código em Java para calcular essa probabilidade para valores arbitrários atte 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 teste:

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

Resultado:

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

Observações

As probabilidades são: 0.0se o atacante tiver 0pontos, 1.0se o atacante tiver pontos, mas a defesa tiver 0pontos, 0.5se os pontos forem iguais, menor que 0.5se o atacante tiver menos pontos que a defesa e maior que 0.5se o atacante tiver mais pontos que a defesa .

Tomando att = 50e def = 80, eu precisava mudar para BigDecimals para evitar transbordamento, mas tenho uma probabilidade de cerca de 0,0040.

Você pode tornar a probabilidade mais próxima de 0,5 alterando o attvalor para ser a média dos valores atte def. Att = 50, Def = 80 se torna (65, 80), o que gera uma probabilidade de 0,1056.

rgettman
fonte
1
Outra abordagem interessante. O algoritmo também pode ser facilmente visualizado, o que pode parecer bastante emocionante.
Philipp
5

Você pode modificar o ataque por um número aleatório amostrado de uma distribuição normal. Dessa forma, na maioria das vezes o resultado será o que você espera, mas, ocasionalmente, um ataque mais alto perde contra uma defesa mais baixa ou um ataque mais baixo vence contra uma defesa mais alta. A probabilidade disso acontecer será menor à medida que a diferença entre ataque e defesa aumentar.

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

A função norm(x0, sigma)retorna um float amostrado de uma distribuição normal centralizada em x0, com sigma de desvio padrão. A maioria das linguagens de programação fornece uma biblioteca com essa função, mas se você quiser fazer isso, dê uma olhada nessa pergunta . Você precisaria ajustar o sigma de modo que "pareça certo", mas um valor de 10 a 20 pode ser um bom lugar para começar.

Para alguns valores sigma, a probabilidade de vitória para um dado se att1 - def2parece com isso: Probabilidade de vitória

Roubar
fonte
Também vale a pena ressaltar que os valores distribuídos normais não têm limites reais; portanto, ao usar valores aleatórios distribuídos normalmente em um jogo, pode fazer sentido restringir o resultado para evitar a situação improvável, mas não impossível, de gerar valores muito extremos que pode quebrar o jogo.
Philipp