Hvordan modellerer jeg et fudge terningekast med re-rolls i Anydice?

Her er en alternativ løsning:

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 "}

Funktionen burde for det meste være selvforklarende; den eneste del, der måske kræver en forklaring, er {1 .. #ROLL-N}@ROLL, som summerer alle undtagen de sidste N elementer af sekvensen ROLL. Som standard sorterer AnyDice terningekast i faldende numerisk rækkefølge, så de sidste elementer er de laveste.

I graftilstand ser output fra dette program således ud:

Graph

Bemærk, hvordan forskellene mellem færdighedsniveau 2, 3 og 4 er ret små, da det er ret usandsynligt at kaste tre eller fire -1’er på 4dF til at starte med.

BTW, programmet ovenfor antager, som du siger i slutningen af dit spørgsmål, at spillerne er konservative og kun vil genrulle negative kast. Hvis dine spillere kan lide at tage risici, kan de beslutte sig for også at rerulle nuller, og i så fald ville resultaterne i stedet se sådan ud:

Graph

Bemærk, hvordan gennemsnittene stadig er de samme, men resultaterne for højere færdigheder har en del større varians. Især sandsynligheden for at kaste en perfekt fire med en positiv færdighed er meget højere på denne måde.

(Den eneste forskel mellem de programmer, der er brugt til at generere de to ovenstående grafer, er, at den anden bruger i stedet for .)

Især hvis dine spillere forsøger at kaste mod et specifikt minimumsmåltal, kan det give mening for dem kun at kaste så mange nuller som nødvendigt for at maksimere deres chance for at nå målet.

Den optimale strategi i disse tilfælde afhænger af, om spillerne kan genkaste terningerne én efter én og efter hvert kast beslutte, om de vil fortsætte med at genkaste, eller om de først skal beslutte, hvilke terninger de vil genkaste, og derefter kaste dem alle sammen på én gang.

I det første tilfælde (dvs. sekventielle omrulninger) kan den optimale beslutningsproces simuleres med en rekursiv AnyDice-funktion:

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 " }}

Her returnerer hovedfunktionen ROLL reroll up to SKILL target TARGET 1, hvis det givne kast er lig med eller større end målet, og 0, hvis det er mindre end målet, og ingen forbedring er mulig (dvs. der er ikke flere terninger tilbage i puljen, der er ikke flere omrulninger tilladt, eller den laveste terning er allerede en +1). Ellers fjerner den den laveste terning fra puljen (ved hjælp af en hjælpefunktion, da AnyDice ikke tilfældigvis har en passende funktion indbygget), reducerer antallet af resterende rerolls med én, trækker 1dF fra målværdien for at simulere en enkelt reroll og kalder derefter sig selv rekursivt.

Outputtet af dette program er lidt besværligt at parse fra AnyDices normale bar/linjediagramvisning, så jeg eksporterede det i stedet og kørte det gennem Python-scriptet fra dette tidligere svar for at lave det om til et pænt todimensionelt gitter, som jeg kunne importere i Google Sheets. Resultaterne, som et varmekort og som en multibar-graf, ser således ud:

Screenshot

I det andet tilfælde (dvs. alle rerolls på én gang) skal vi først finde ud af, hvad den optimale strategi egentlig er. Et øjebliks eftertanke viser, at:

  • Man bør altid rerulle alle -1’ere, da det aldrig kan mindske resultatet. Da det forventede gennemsnitlige resultat af et genrul er 0, er det forventede gennemsnit efter genrulning af alle -1’ere lig med antallet af +1’ere i det oprindelige kast.

  • Rerulning af et nul ændrer ikke det forventede gennemsnitlige resultat, men det øger variansen, dvs. det gør det faktiske resultat mere sandsynligt, at det er længere væk fra gennemsnittet i en af retningerne. Derfor bør man kun genrulle nuller, hvis det forventede gennemsnitlige resultat efter at have genrullet alle -1’ere (dvs. antallet af +1’ere i det oprindelige kast) er under måltallet.

Anvendelse af denne logik i AnyDice resulterer i noget i retning af dette 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 " }}

Eksport af output fra dette script og kørsel af det gennem det samme Python-script og regneark giver følgende varmekort og søjlediagram:

Screenshot

Som du kan se, er resultaterne faktisk ikke så forskellige fra det sekventielle rerolls tilfælde. De største forskelle opstår med høje færdigheder og mellemliggende måltal: Hvis man f.eks. med en færdighed på 4 kan udføre rerolls én ad gangen og stoppe på et hvilket som helst tidspunkt, stiger den gennemsnitlige succesrate fra 75,3 % til 81 % for et mål på +1, eller fra 51,6 % til 58,3 % for et mål på +2.

Ps. Det lykkedes mig at finde ud af en måde at få AnyDice til at samle “succesrate vs. mål”-værdierne fra de to ovenstående programmer i en enkelt fordeling for hver færdighedsværdi, så de kan tegnes direkte af AnyDice som søjlediagrammer eller linjediagrammer (i “mindst”-tilstand) uden at skulle bruge Python eller regneark.

Det er desværre alt andet end enkelt, at AnyDice-koden til at gøre det. Den sværeste(!) del viste sig at være at finde en måde at få AnyDice til at subtrahere to sandsynligheder (f.eks. 1/2 – 1/3 = 1/6). Den bedste måde, jeg kender til at udføre denne tilsyneladende trivielle opgave i AnyDice, indebærer ikke-triviel manipulation af betingede sandsynligheder og en itereret løkke. Og den får AnyDice til at gå ned, hvis man forsøger at beregne 0 – 0 med den.*

Her er AnyDice-koden til at beregne og plotte fordelingen af det “højeste slåbare mål” for forskellige færdighedsniveauer (og for hver af de to rerollingmekanikker beskrevet ovenfor) med nogle kommentarer tilføjet for at gøre det lettere at læse:

\- 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"}

og et skærmbillede af output (i “mindst” linjediagramtilstand):

Graph

En bemærkning om fortolkningen af det output, der genereres af ovenstående program: De sandsynlighedsfordelinger, der vises på grafen ovenfor, svarer ikke til resultaterne af en enkelt terningekaststrategi; de er snarere kunstigt konstruerede fordelinger (dvs. “brugerdefinerede terninger” i AnyDice-jargon), således at sandsynligheden for at kaste mindst \$N\$ på et enkelt kast med den brugerdefinerede terning er lig med sandsynligheden for, at spilleren er i stand til at kaste mindst \$N\$ på 4dF med den givne genkastningsmekanik (en ad gangen mod en ad gangen mod en anden). alle på én gang) og det givne maksimale antal rerolls, forudsat at spilleren bruger den optimale reroll-strategi for det pågældende mål \$$N\$.

Med andre ord kan vi ved at se på resultatet i “mindst”-tilstand se, at en spiller med færdighedsniveau 4 har 51,62 % chance for at få succes med at kaste +2 eller mere (ved hjælp af reroll-mekanikken med alle på én gang), hvis han bruger sine tilgængelige rerolls på den måde, der maksimerer denne særlige chance. Output viser også korrekt, at den samme spiller har 75,28% chance for at kaste +1 eller mere, hvis de vælger at optimere for det i stedet, men de skal bruge forskellige rerolling-strategier for at nå disse to mål.

Og “sandsynligheden” på 23,65% for at kaste præcis +1 på den brugerdefinerede terning, der er beskrevet ovenfor, har egentlig ingen fornuftig betydning, bortset fra at det (omtrentligt, på grund af afrunding) er forskellen mellem 75,28% og 51,62%. Det er vel derfor, det er så svært at beregne med AnyDice 😛 Man kan vel tolke det som et mål for, hvor meget sværere et mål på +2 er at opfylde med den givne færdighed og rerolling mekanik end et mål på +1, i en vis forstand, men det er sådan set det hele.

*) Dette nedbrud kan være relateret til, hvad jeg er ret sikker på er en fejl i AnyDice, som jeg fandt under udviklingen af denne kode, hvilket fik et af mine tidlige testprogrammer til at generere virkelig mærkelige output med ting som 97284.21% sandsynligheder(!). Testprogrammet går også til sidst ned, hvis man øger iterationstallet yderligere.