Attack vs. Defense i kto jest zwycięzcą? [Zamknięte]

12

Jestem w trakcie tworzenia nowej prostej gry na urządzenia mobilne i poświęciłem kilka dni na następną część.

Dla uproszczenia załóżmy, że mam dwóch wojowników. Jedynym ich atrybutem jest Atak i Obrona. Kiedy pierwsze ataki, jedyne, co się liczy, to atak jego i obrona przeciwnika. I wzajemnie.

Nie mają wyposażenia, przedmiotów, wytrzymałości ani zdrowia. Just Attack vs. Defense.

Przykład:

  • Myśliwiec 1:

    Atak: 50, Obrona: 35

  • Myśliwiec 2:

    Atak 20, Obrona: 80

Proces walki będzie tylko jednym atakiem, który określi zwycięzcę. Więc nie ma wielu ataków ani rund. Nie chcę, aby było to deterministyczne, ale dodaję lekką wersję nieoczekiwanego. Wojownik o niższym ataku będzie w stanie wygrać innego wojownika o wyższej obronie (ale oczywiście nie za każdym razem)

Moim pierwszym pomysłem było uczynienie go liniowym i wywołanie jednolitego generatora liczb losowych.

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

Przykład z atakiem 50 i obroną 80, atakujący wojownik będzie miał około 38% do wygrania. Wydaje mi się jednak, że to, co nieoczekiwane, jest zbyt dalekie i najgorsi wojownicy dużo wygrają.

Zastanawiałem się, jak pracowałeś w podobnych sytuacjach.

PS Dużo szukałem w tym QnA i innych źródłach i znalazłem podobne pytania, które wymieniono jako zbyt ogólne dla SE. Ale miały one wiele atrybutów, broni, przedmiotów, klas itp., Co może sprawić, że będzie to zbyt skomplikowane. Myślę, że moja wersja jest znacznie prostsza w dopasowaniu do stylu QnA SE.

Tasos
źródło
1
Jakich przypadków szukasz? Na jaki zakres wartości ataku i obrony patrzysz i czy jakieś dwie liczby w tych zakresach powinny kiedykolwiek mieć ustalony wynik? Na przykład, czy wojownik z atakiem 10 może pokonać wojownika w obronie 90?
Niels
@ user2645227 Mógłbym powiedzieć, że zakres wynosi od 1 do 400. Nie, nie chcę podejmować żadnych deterministycznych decyzji i dać możliwość ataku 1 wygrać obronę 400, ale w naprawdę rzadkich przypadkach.
Tasos
1
Więc jeśli weźmiesz Att (min) -def (max) i Att (max) -def (min), co daje zakres 800 od -400 do +400. Będziesz chciał, aby Twój losowy zasięg obejmował cały zakres. Obrona - Atak da ci margines skalowania w postaci progu, który musisz trafić, aby wygrać. To powinno nieco zmniejszyć losowość. Aby jeszcze bardziej scentralizować wyniki, możesz użyć przykładu Philippsa lub grzebać w dowolnym miejscu, dopóki nie osiągniesz krzywej, której szukasz.
Niels

Odpowiedzi:

24

Jeśli chcesz, aby twoje wyniki walki były bardziej przewidywalne, ale nie całkowicie deterministyczne, wybierz najlepszy system n .

Powtórz nczasy walki (gdzie npowinna być nierówna liczba) i ogłoś walczącego zwycięzcą, który wygrał częściej. Im większa wartość, ntym mniej niespodzianek wygrywasz i przegrywasz.

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
} 

Ten system działa tylko w szczególnym przypadku, gdy walka jest prostym binarnym wynikiem wygranej lub przegranej. Gdy walka ma bardziej złożone wyniki, na przykład gdy zwycięzca wciąż traci punkty życia w zależności od tego, jak blisko była wygrana, takie podejście już nie działa. Bardziej ogólnym rozwiązaniem jest zmiana sposobu generowania liczb losowych. Gdy wygenerujesz wiele liczb losowych, a następnie weźmiesz średnią, wyniki zostaną zgrupowane w pobliżu środka zakresu, a bardziej ekstremalne wyniki będą rzadsze. Na przykład:

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

będzie miał taką krzywą dystrybucji:

Dystrybucja 3d20 / 3

(zdjęcie dzięki uprzejmości Anydice - naprawdę przydatne narzędzie do projektowania formuł mechaniki gier, które wymagają losowości, nie tylko w przypadku gier stołowych)

W moim obecnym projekcie korzystam z funkcji pomocniczej, która pozwala ustawić dowolną wielkość próbki:

double averagedRandom(int averageness) {
     double result = 0.0;
     for (var i = 0; i < averageness; i++) {
         result += Random();
     }
     return result / (double)averageness;
}
Philipp
źródło
Wydaje się lepsze podejście. Jedno pytanie. Czy w funkcji averagedRandom3 () należy używać +zamiast tego, *czy źle zrozumiałem, co robi?
Tasos
@Tasos tak, powinno być +, nie *. Mam również funkcję losową, która zwielokrotnia wiele próbek. Daje to funkcję liczb losowych z silnym nastawieniem na niższe wartości, co może być również przydatne w niektórych sytuacjach.
Philipp
1
Pozostawię pytanie otwarte przez 1-2 dni, a jeśli nie otrzymam innej odpowiedzi, wybiorę twoje. Poparłem to, ale chcę dać szansę na inne odpowiedzi, jeśli nie masz nic przeciwko.
Tasos
Myślę, że ta odpowiedź ma już wystarczającą liczbę głosów, dzięki czemu można ją oznaczyć jako odpowiedź: P
Hamza Hasan
1
Byłbym również ciekawy, czy niektórzy ludzie wymyślą alternatywne podejścia. Jedna osoba zanegowała tę odpowiedź. Może chcieliby zapewnić alternatywny.
Philipp
8

Właśnie tego określałem zwycięzcę bitwy w moim aplecie Lords of Conquest Imitator. W tej grze, podobnie jak w twojej sytuacji, jest tylko wartość ataku i wartość obrony. Prawdopodobieństwo, że atakujący wygra, jest tym większe, im więcej punktów ma atakujący, i tym mniej punktów ma obrona, przy równych wartościach stanowiących 50% szans na udany atak.

Algorytm

  1. Rzuć losową monetą.

    1a. Głowy: obrona traci punkt.

    1b. Ogony: głowy tracą punkt.

  2. Jeśli zarówno obrona, jak i atakujący nadal mają punkty, wróć do kroku 1.

  3. Kto ma 0 punktów, przegrywa bitwę.

    3a. Atakujący do 0: Atak kończy się niepowodzeniem.

    3b. Obrona do 0: Atak się udaje.

Napisałem to w Javie, ale powinno być łatwe do przetłumaczenia na inne języki.

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

Przykład

Załóżmy na przykład, że att = 2 i def = 2, aby upewnić się, że prawdopodobieństwo wynosi 50%.

Bitwa zostanie rozstrzygnięta w maksymalnej n = att + def - 1liczbie rzutów monetą lub 3 w tym przykładzie (w zasadzie jest to najlepsza z 3 tutaj). Istnieją 2 n możliwych kombinacji rzutów monetą. Tutaj „W” oznacza, że ​​atakujący wygrał rzut monetą, a „L” oznacza, że ​​atakujący stracił rzut monetą.

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

Atakujący wygrywa w 4/8, czyli 50% przypadków.

Matematyka

Prawdopodobieństwa matematyczne wynikające z tego prostego algorytmu są bardziej skomplikowane niż sam algorytm.

Liczba kombinacji, w których dokładnie x Ls jest podana przez funkcję kombinacji:

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

Atakujący wygrywa, gdy są pomiędzy 0i att - 1L. Liczba zwycięskich kombinacji jest równa sumie kombinacji od 0do att - 1, skumulowanego rozkładu dwumianowego:

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

Prawdopodobieństwo atakującego wygranej w podzielonej przez 2 n , skumulowanego dwumianowego prawdopodobieństwa:

p = w / 2^n

Oto kod w Javie do obliczenia tego prawdopodobieństwa dla wartości arbitralnych atti defwartości:

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

Kod testowy:

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

Wynik:

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

Spostrzeżenia

Prawdopodobieństwa są, 0.0jeśli atakujący ma 0punkty, 1.0jeśli atakujący ma punkty, ale obrona ma 0punkty, 0.5jeśli punkty są równe, mniej niż 0.5jeśli atakujący ma mniej punktów niż obrona, i większy niż, 0.5jeśli atakujący ma więcej punktów niż obrona .

Biorąc att = 50i def = 80musiałem przełączyć się na BigDecimals, aby uniknąć przepełnienia, ale dostaję prawdopodobieństwo około 0,0040.

Możesz zbliżyć prawdopodobieństwo do 0,5, zmieniając attwartość na średnią z wartości atti def. Att = 50, Def = 80 staje się (65, 80), co daje prawdopodobieństwo 0,1056.

rgettman
źródło
1
Kolejne interesujące podejście. Algorytm można również łatwo wizualizować, co może wyglądać dość ekscytująco.
Philipp
5

Możesz zmodyfikować atak losową liczbą próbkowaną z normalnego rozkładu. W ten sposób przez większość czasu wynik będzie zgodny z oczekiwaniami, ale czasami wyższy atak przegra przeciwko niższej obronie lub niższy atak wygra przeciwko wyższej obronie. Prawdopodobieństwo takiego zdarzenia zmniejszy się wraz ze wzrostem różnicy między atakiem a obroną.

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

Funkcja norm(x0, sigma)zwraca liczbę zmiennoprzecinkową próbkowaną z rozkładu normalnego wyśrodkowanego na x0, ze sigma odchylenia standardowego. Większość języków programowania udostępnia bibliotekę z taką funkcją, ale jeśli chcesz sprawić, by to zrobiłaś sama, spójrz na to pytanie . Trzeba by dostosować sigma tak, aby „czuł się dobrze”, ale wartość 10–20 może być dobrym początkiem.

Dla kilku wartości sigma prawdopodobieństwo zwycięstwa dla danego att1 - def2wygląda następująco: Prawdopodobieństwo zwycięstwa

Obrabować
źródło
Warto również zauważyć, że normalne wartości rozproszone nie mają rzeczywistych granic, więc przy stosowaniu losowych wartości o rozkładzie normalnym w grze warto zablokować wynik, aby uniknąć nieoczekiwanej, ale nie niemożliwej sytuacji generowania bardzo ekstremalnych wartości, które może przerwać grę.
Philipp