Атака против обороны, а кто победитель? [закрыто]

12

Я нахожусь в процессе создания новой простой игры для мобильных устройств, и я потратил несколько дней на следующую часть.

Для простоты, скажем, у меня есть два бойца. Единственный их атрибут - это атака и защита. При первой атаке важно только нападение на него и защита противника. И наоборот.

У них нет снаряжения, предметов, выносливости или здоровья. Просто атака против обороны.

Пример:

  • Боец 1:

    Атака: 50, Защита: 35

  • Боец 2:

    Атака 20, Оборона: 80

Боевой процесс будет только одной атакой, которая определит победителя. Таким образом, нет многократных атак или раундов. Я не хочу делать это детерминированным, но добавлю легкую версию неожиданного. Боец с более низкой атакой сможет выиграть другого бойца с более высокой защитой (но, конечно, не каждый раз)

Моей первой идеей было сделать его линейным и вызвать универсальный генератор случайных чисел.

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

Например, с атакой 50 и защитой 80 у атакующего бойца будет около 38% на победу. Однако мне кажется, что неожиданность слишком далека, и худшие бойцы много выиграют.

Мне было интересно, как вы работали в подобных ситуациях.

PS Я много искал в этой QnA и других источниках, и я нашел похожие вопросы, упомянутые как слишком широкие для SE. Но у них было много атрибутов, оружия, предметов, классов и т. Д., Которые могли бы сделать его слишком сложным. Я думаю, что моя версия намного проще, чтобы соответствовать ей в стиле QnA SE.

Тасос
источник
1
Какие случаи вы ищете? Какой диапазон значений для атаки и защиты вы смотрите и должны ли какие-либо два числа в этих диапазонах иметь фиксированный результат? Например, может ли боец ​​с атакой 10 победить бойца с защитой 90?
Нильс
@ user2645227 Я мог бы сказать, что диапазон составляет от 1 до 400. Нет, я не хочу принимать какие-либо детерминированные решения и дать возможность атаковать 1, чтобы выиграть защиту 400, но в очень редких случаях.
Тасос
1
Так что если вы возьмете Att (min) -def (max) и Att (max) -def (min), то вы получите диапазон 800 от -400 до +400. Вы хотите, чтобы ваш случайный диапазон охватывал весь диапазон. Защита - атака даст вам масштабирующий запас в виде порога, который вам нужно будет использовать, чтобы победить. Это должно немного уменьшить случайность. Для дальнейшей централизации результатов вы можете использовать пример Philipps или возиться в любом месте, пока не достигнете искомой кривой.
Нильс

Ответы:

24

Если вы хотите, чтобы результаты вашего боя были более предсказуемыми, но не полностью детерминированными, лучше всего систем.

Повторите время боя n(где nдолжно быть неравное число) и объявите бойца победителем, который выиграл чаще. Чем больше ваша ценность, nтем меньше сюрпризов вы получите.

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
} 

Эта система работает только в особом случае, когда бой является простым двоичным результатом выигрыша или проигрыша. Когда бой имеет более сложные результаты, например, когда победитель все еще теряет некоторые очки жизни, в зависимости от того, насколько близок был выигрыш, этот подход больше не работает. Более общее решение состоит в том, чтобы изменить способ генерации случайных чисел. Когда вы генерируете несколько случайных чисел, а затем берете среднее значение, результаты будут группироваться вблизи центра диапазона, а более экстремальные результаты будут более редкими. Например:

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

будет иметь такую ​​кривую распределения:

Распределение 3d20 / 3

(Фото любезно предоставлено Anydice - действительно полезным инструментом для разработки формул игровой механики, которые включают случайность, а не только для настольных игр)

В моем текущем проекте я использую вспомогательную функцию, которая позволяет установить произвольный размер выборки:

double averagedRandom(int averageness) {
     double result = 0.0;
     for (var i = 0; i < averageness; i++) {
         result += Random();
     }
     return result / (double)averageness;
}
Philipp
источник
Кажется, лучший подход. Один вопрос. В функции averagedRandom3 () следует использовать +вместо *или я неправильно понял, что она делает?
Тасос
@ Тасос да, должен быть +, а не *. У меня также есть случайная функция, которая умножает несколько выборок. Это дает вам функцию случайных чисел с сильным смещением для более низких значений, что также может быть полезно в некоторых ситуациях.
Филипп
1
Я буду держать вопрос открытым в течение 1-2 дней, и если у меня не будет другого ответа, я выберу ваш. Я проголосовал за это, но хочу дать шанс и для других ответов, если вы не возражаете.
Тасос
Я думаю, что этот ответ уже набрал достаточное количество голосов, поэтому этот ответ можно пометить как ответ: P
Хамза Хасан
1
Мне также было бы любопытно, если бы некоторые люди придумали альтернативные подходы. Один человек отклонил этот ответ. Может быть, они хотели бы предоставить альтернативу.
Филипп
8

Это то, что я использовал, чтобы определить победителя в моем апплете Lords of Conquest Imitator. В этой игре, как и в вашей ситуации, есть только значение атаки и значение защиты. Вероятность того, что атакующий выиграет, тем больше, чем больше очков у атакующего, и чем меньше, тем больше очков у защиты, при равных значениях вероятности успеха атаки 50%.

Алгоритм

  1. Переверните случайную монету.

    1a. Головы: защита теряет очко.

    1б. Хвосты: головы теряют очко.

  2. Если и у защиты, и у атакующего все еще есть очки, вернитесь к шагу 1.

  3. Тот, кто до 0 очков проигрывает битву.

    3a. Атакующий до 0: атака не удалась.

    3b. Защита до 0: атака успешна.

Я написал это на Java, но он должен быть легко переведен на другие языки.

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

Пример

Например, предположим, что att = 2 и def = 2, просто чтобы убедиться, что вероятность составляет 50%.

Битва будет решена с максимальным количеством n = att + def - 1подбрасываний монет или 3 в этом примере (это, по сути, лучший из 3 здесь). Есть 2 п возможных комбинаций монет переворачиваются. Здесь «W» означает, что атакующий выиграл бросок монеты, а «L» означает, что атакующий потерял бросок монеты.

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

Атакующий выигрывает в 4/8, или в 50% случаев.

Математика

Математические вероятности, вытекающие из этого простого алгоритма, являются более сложными, чем сам алгоритм.

Количество комбинаций, где точно x Ls, определяется функцией комбинации:

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

Атакующий побеждает, когда есть между 0и att - 1Ls. Количество выигрышных комбинаций равно сумме комбинаций от 0сквозного att - 1, кумулятивного биномиального распределения:

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

Вероятность атакующего выигрыш ш делится на 2 л , кумулятивной вероятности биномиального:

p = w / 2^n

Вот код в Java , чтобы вычислить эту вероятность для произвольного attи 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;
}

Код тестирования:

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

Выход:

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

наблюдения

Скорее всего, 0.0если у атакующего есть 0очки, 1.0если у атакующего есть очки, но у защиты есть 0очки, 0.5если очки равны, меньше, чем 0.5если у атакующего меньше очков, чем у защиты, и больше, чем0.5 если у атакующего больше очков, чем у защиты. ,

Принимая att = 50и def = 80, мне нужно было переключиться на BigDecimals, чтобы избежать переполнения, но я получаю вероятность около 0,0040.

Вы можете сделать вероятность ближе к 0,5, изменив attзначение на среднее значение attи def. Att = 50, Def = 80 становится (65, 80), что дает вероятность 0,1056.

rgettman
источник
1
Еще один интересный подход. Алгоритм также может быть легко визуализирован, что может выглядеть довольно увлекательно.
Филипп
5

Вы можете изменить атаку случайным числом, выбранным из нормального распределения. Таким образом, в большинстве случаев результатом будет то, что вы ожидаете, но иногда более высокая атака будет проигрывать против более низкой защиты, или более низкая атака будет выигрывать у более высокой защиты. Вероятность этого будет уменьшаться с увеличением разницы между атакой и защитой.

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

Функция norm(x0, sigma)возвращает число с плавающей точкой, отобранное из нормального распределения с центром в точке x0, со сигма стандартного отклонения. Большинство языков программирования предоставляют библиотеку с такой функцией, но если вы хотите сделать это самостоятельно, взгляните на этот вопрос . Вы должны настроить сигму таким образом, чтобы она выглядела «правильно», но значение 10-20 может быть хорошим началом.

Для нескольких значений сигма вероятность победы для данного att1 - def2выглядит следующим образом: Вероятность победы

обкрадывать
источник
Также стоит отметить, что нормальные распределенные значения не имеют реальных границ, поэтому при использовании случайных значений с нормальным распределением в игре может иметь смысл ограничить результат, чтобы избежать маловероятной, но не невозможной ситуации генерирования очень экстремальных значений, которые может сломать игру.
Филипп