How do I model a fudge dice roll with re-rolls in Anydice?
Here’s an alternative solution:
FUDGE: {-1, 0, +1}function: ROLL:s reroll up to SKILL:n { N: ] result: NdFUDGE + {1 .. #ROLL-N}@ROLL}loop SKILL over {0..4} { output named "skill "}
The function should be mostly self-explanatory; the one part that may require explanation is {1 .. #ROLL-N}@ROLL
, which sums all but the last N
elements of the sequence ROLL
. Domyślnie AnyDice sortuje rzuty kostką w malejącej kolejności numerycznej, więc ostatnie elementy są najniższe.
W trybie grafu, wyniki tego programu wyglądają następująco:
Zauważ, że różnice między poziomami umiejętności 2, 3 i 4 są dość niewielkie, ponieważ rzucenie trzech lub czterech -1 na 4dF jest dość mało prawdopodobne na początek.
BTW, powyższy program zakłada, jak powiedziałeś na końcu swojego pytania, że gracze są konserwatywni i będą tylko reerollować negatywne rolki. Jeśli twoi gracze lubią podejmować ryzyko, mogą zdecydować się na przetasowanie zera, w tym przypadku wyniki będą wyglądały następująco:
Zauważ jak średnie są wciąż takie same, ale wyniki dla wyższych umiejętności mają dużo więcej wariancji. W szczególności, prawdopodobieństwo wyrzucenia idealnej czwórki z pozytywną umiejętnością jest dużo wyższe w ten sposób.
(Jedyna różnica między programami użytymi do wygenerowania dwóch powyższych wykresów jest taka, że drugi używa zamiast
.)
W szczególności, jeśli twoi gracze próbują wyrzucić przeciwko konkretnej minimalnej liczbie docelowej, może mieć sens dla nich wyrzucenie tylko tylu zer, ile potrzeba, aby zmaksymalizować ich szansę na osiągnięcie celu.
Optymalna strategia w tym przypadku zależy od tego, czy gracze mogą przeliczyć kości jedna po drugiej i zdecydować po każdym rzucie, czy chcą kontynuować przerzucanie, czy też muszą najpierw zdecydować, które kości chcą przerzucić, a następnie rzucić je wszystkie naraz.
W pierwszym przypadku (tj.
FUDGE: {-1, 0, +1}function: first N:n of SEQ:s { FIRST: {} loop I over {1..N} { FIRST: {FIRST, I@SEQ} } result: FIRST}function: ROLL:s reroll up to SKILL:n target TARGET:n { if ROLL + 0 >= TARGET { result: 1 } \- success -\ if #ROLL = 0 | SKILL = 0 | #ROLL@ROLL = 1 { result: 0 } \- failure -\ FIRST: result: \- reroll -\}loop TARGET over {-3..4} { loop SKILL over {0..4} { output named "target , skill " }}
Tutaj główna funkcja ROLL reroll up to SKILL target TARGET
zwraca 1, jeśli dany rzut jest równy lub większy od celu, i 0, jeśli jest mniejszy od celu i nie ma możliwości poprawy (tzn. nie ma już więcej kości w puli, nie ma możliwości ponownego obrotu lub najniższa kość jest już +1). W przeciwnym razie usuwa najniższą kość z puli (używając funkcji pomocniczej, ponieważ AnyDice nie ma odpowiedniej wbudowanej), zmniejsza liczbę pozostałych rerolli o jeden, odejmuje 1dF od wartości docelowej, aby zasymulować pojedynczy reroll, a następnie wywołuje siebie rekurencyjnie.
Wyjście tego programu jest trochę niewygodne do parsowania z normalnego widoku AnyDice’s bar / line graph, więc zamiast tego wyeksportowałem go i przepuściłem przez skrypt Pythona z tej wcześniejszej odpowiedzi, aby przekształcić go w ładną dwuwymiarową siatkę, którą mogłem zaimportować do Google Sheets. Wyniki, jako mapa ciepła i jako wykres wielobarkowy, wyglądają tak:
W drugim przypadku (tj. Wszystkie rerolle na raz) musimy najpierw dowiedzieć się, jaka jest optymalna strategia. Chwila zastanowienia pokazuje, że:
-
Należy zawsze rerollować wszystkie -1, ponieważ nie może to nigdy zmniejszyć wyniku. Ponieważ oczekiwany średni wynik ponownego turlania wynosi 0, oczekiwana średnia po ponownym turlaniu wszystkich -1 jest równa liczbie +1 w początkowym turlaniu.
-
Ponowne turlanie zera nie zmienia oczekiwanego średniego wyniku, ale zwiększa wariancję, tj. sprawia, że rzeczywisty wynik jest bardziej prawdopodobny, aby być dalej od średniej w obu kierunkach. Tak więc, powinno się ponownie rzucać zera tylko wtedy, gdy oczekiwany średni wynik po ponownym rzuceniu wszystkich -1 (tj. liczba +1 w początkowym rzucie) jest poniżej liczby docelowej.
Zastosowanie tej logiki w AnyDice skutkuje czymś takim jak ten program:
FUDGE: {-1, 0, +1}function: ROLL:s reroll up to SKILL:n target TARGET:n { if >= TARGET { N: ] } else { N: ] } result: (NdFUDGE + {1 .. #ROLL-N}@ROLL) >= TARGET}loop TARGET over {-3..4} { loop SKILL over {0..4} { output named "target , skill " }}
Wyeksportowanie danych wyjściowych tego skryptu i uruchomienie go przez ten sam skrypt Pythona i arkusz kalkulacyjny daje następującą mapę cieplną i wykres słupkowy:
Jak widać, wyniki nie różnią się aż tak bardzo od przypadku sekwencyjnego rerolla. Największe różnice pojawiają się przy wysokich umiejętnościach i pośrednich liczbach celów: na przykład, przy umiejętności 4, możliwość wykonywania rerolli po kolei i zatrzymania się w dowolnym momencie podnosi średni współczynnik sukcesu z 75.3% do 81% dla celu +1, lub z 51.6% do 58.3% dla celu +2.
Ps. Udało mi się wymyślić sposób, aby AnyDice zebrał wartości „współczynnika sukcesu w stosunku do celu” z dwóch powyższych programów w jeden rozkład dla każdej wartości umiejętności, co pozwala na rysowanie ich bezpośrednio przez AnyDice jako wykresów słupkowych lub liniowych (w trybie „przynajmniej”) bez konieczności używania Pythona lub arkuszy kalkulacyjnych.
Niestety, kod AnyDice, który to robi, nie jest prosty. Najtrudniejszą(!) częścią okazało się znalezienie sposobu, aby AnyDice odjął dwa prawdopodobieństwa (np. 1/2 – 1/3 = 1/6). Najlepszy znany mi sposób na wykonanie tego pozornie trywialnego zadania w AnyDice wymaga nietrywialnej manipulacji prawdopodobieństwami warunkowymi i pętli iterowanej. I powoduje awarię AnyDice, jeśli spróbujesz obliczyć 0 – 0 za jej pomocą.*
Anyway, tak dla uzupełnienia, oto kod AnyDice do obliczania i wykreślania rozkładu „najwyższego możliwego do pokonania celu” dla różnych poziomów umiejętności (i dla każdej z dwóch mechanik rerollingu opisanych powyżej) z kilkoma komentarzami dodanymi dla czytelności:
\- predefine a fudge die -\FUDGE: d{-1, 0, +1}\- miscellaneous helper functions used in the code below -\function: first N:n of SEQ:s { FIRST: {} loop I over {1..N} { FIRST: {FIRST, I@SEQ} } result: FIRST}function: exclude RANGE:s from ROLL:n { if ROLL = RANGE { result: d{} } else { result: ROLL }}function: sign of NUM:n { result: (NUM > 0) - (NUM < 0)}function: if COND:n then A:d else B:d { if COND { result: A } else { result: B }}\- a helper function to subtract two probabilities (given as {0,1}-valued dice) -\function: P:d minus Q:d { DIFF: P - Q loop I over {1..20} { TEMP: DIFF: (DIFF != 0) * } result: }\- this function calculates the probability of meeting or exceeding the target -\- value, assuming that each die in the initial roll can be rerolled once and -\- that the player may stop rerolling at any point -\function: ROLL:s reroll one at a time up to SKILL:n target TARGET:n { if ROLL + 0 >= TARGET { result: 1 } \- success -\ if #ROLL = 0 | SKILL = 0 | #ROLL@ROLL = 1 { result: 0 } \- failure -\ FIRST: \- remove last (=lowest) original roll -\ TNEW: TARGET - 1dFUDGE \- adjust target value depending on reroll -\ result: \- reroll -\}\- this function calculates the probability of meeting or exceeding the target -\- value, assuming that each die in the initial roll can be rerolled once but -\- the player must decide in advance how many of the dice they'll reroll; the -\- optimal(?) decision rule in this case is to always reroll all -1s and to -\- also reroll 0s if and only if the number of +1s in the initial roll is less -\- than the target number -\function: ROLL:s reroll all at once up to SKILL:n target TARGET:n { if >= TARGET { N: ] } else { N: ] } result: (NdFUDGE + {1 .. #ROLL-N}@ROLL) >= TARGET}\- this function collects the success probabilities given by the two functions -\- above into a single custom die D, such that the probability that D >= N is -\- equal to the probability of the player meeting or exceeding the target N; -\- the SEQUENTIAL flag controls which of the functions above is used -\function: collect results for SKILL:n from MIN:n to MAX:n sequential SEQUENTIAL:n { BOGUS: MAX + 1 DIST: 0 PREV: 1 loop TARGET over {MIN..MAX} { if SEQUENTIAL { PROB: } else { PROB: } DIST: then TARGET else BOGUS]] PREV: PROB } result: }\- finally we just loop over possible skill values and output the results -\loop SKILL over {0..4} { output named "skill , one at a time"}loop SKILL over {0..4} { output named "skill , all at once"}
oraz zrzut ekranu z wynikami (w trybie „przynajmniej” wykresu liniowego):
Uwaga na temat interpretacji wyników wygenerowanych przez powyższy program: Rozkłady prawdopodobieństwa pokazane na powyższym wykresie nie odpowiadają wynikom jakiejkolwiek pojedynczej strategii rzucania kośćmi; są to raczej sztucznie skonstruowane rozkłady (tj. „custom dice” w żargonie AnyDice), takie, że prawdopodobieństwo rzucenia co najmniej ∗$ w pojedynczym rzucie custom die równa się prawdopodobieństwu gracza, który jest w stanie rzucić co najmniej ∗$ w 4dF z daną mechaniką reerollingu (jeden na raz vs. wszystkie naraz) i daną maksymalną maksymalną liczbą rzutów. i podanej maksymalnej liczbie rerolli, zakładając, że gracz używa optymalnej strategii rerolli dla tego konkretnego celu.
Innymi słowy, patrząc na wynik w trybie „co najmniej”, możemy zobaczyć, że gracz z poziomem umiejętności 4 ma 51.62% szans na pomyślne wyrzucenie +2 lub więcej (używając mechaniki rerolli „wszystko za jednym razem”), jeśli używa swoich dostępnych rerolli w sposób, który maksymalizuje tę szansę. Wyjście poprawnie pokazuje również, że ten sam gracz ma 75.28% szans na to, że uda mu się rzucić +1 lub więcej, jeśli zdecyduje się zoptymalizować to zamiast tego, ale będą potrzebować różnych strategii rerollingu, aby osiągnąć te dwa cele.
A „prawdopodobieństwo” 23.65% na toczenie dokładnie +1 na niestandardowej matrycy opisanej powyżej naprawdę nie ma żadnego sensownego znaczenia, poza tym, że jest to (w przybliżeniu, z powodu zaokrąglenia) różnica między 75.28% i 51.62%. Przypuszczam, że dlatego jest tak trudna do obliczenia w AnyDice 😛 Przypuszczam, że można ją interpretować jako miarę tego, o ile trudniej jest osiągnąć cel +2 przy użyciu danej umiejętności i mechaniki rerollingu niż cel +1, w pewnym sensie, ale to wszystko.
*) Ta awaria może być związana z tym, co jestem pewien, że jest błędem w AnyDice, który znalazłem podczas tworzenia tego kodu, powodując, że jeden z moich wczesnych programów testowych generował naprawdę dziwne dane wyjściowe z rzeczami takimi jak 97284.21% prawdopodobieństwa(!). Program testowy również ostatecznie zawiesza się, jeśli zwiększysz liczbę iteracji dalej.