Cum pot modela o aruncare de zaruri fudge cu re-rolls în Anydice?

Iată o soluție alternativă:

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

Funcția ar trebui să fie în mare parte auto-explicativă; singura parte care poate necesita explicații este {1 .. #ROLL-N}@ROLL, care însumează toate elementele N din secvența ROLL, mai puțin ultimele N. În mod implicit, AnyDice ordonează rulourile de zaruri în ordine numerică descrescătoare, astfel încât ultimele elemente sunt cele mai mici.

În modul grafic, ieșirile acestui program arată astfel:

Graph

Rețineți cum diferențele dintre nivelurile de îndemânare 2, 3 și 4 sunt destul de mici, deoarece a arunca trei sau patru -1 pe 4dF este destul de puțin probabil pentru început.

BTW, programul de mai sus presupune, așa cum spuneți la sfârșitul întrebării dumneavoastră, că jucătorii sunt conservatori și că vor reface doar rostogolirile negative. Dacă jucătorilor tăi le place să își asume riscuri, ei ar putea decide să refacă și zerouri, caz în care rezultatele ar arăta în schimb așa:

Grafic

Rețineți cum mediile sunt în continuare aceleași, dar rezultatele pentru abilitățile mai mari au o variație mult mai mare. În special, probabilitățile de a scoate un patru perfect cu o abilitate pozitivă sunt mult mai mari în acest fel.

(Singura diferență între programele folosite pentru a genera cele două grafice de mai sus este că cel de-al doilea folosește în loc de .)

În special, dacă jucătorii dvs. încearcă să arunce împotriva unui număr țintă minim specific, ar putea avea sens ca ei să arunce doar atâtea zerouri câte zerouri sunt necesare pentru a-și maximiza șansele de a atinge ținta.

Strategia optimă în aceste cazuri depinde de faptul dacă jucătorii pot rostogoli zarurile unul câte unul și pot decide după fiecare aruncare dacă vor să continue rostogolirea sau dacă trebuie să decidă mai întâi ce zaruri vor să rostogolească și apoi să le rostogolească pe toate deodată.

În primul caz (i.e. rostogoliri secvențiale), procesul decizional optim poate fi simulat cu o funcție recursivă AnyDice:

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

Aici, funcția principală ROLL reroll up to SKILL target TARGET returnează 1 dacă aruncarea dată este egală sau mai mare decât ținta și 0 dacă este mai mică decât ținta și nu este posibilă nicio îmbunătățire (adică nu mai sunt zaruri rămase în fondul de zaruri, nu mai sunt permise alte rostogoliri sau cel mai mic zar este deja un +1). În caz contrar, acesta elimină cel mai mic zar din fondul de zaruri (folosind o funcție de ajutor, deoarece AnyDice nu are o funcție adecvată încorporată în el), scade numărul de rostogoliri rămase cu unu, scade 1dF din valoarea țintă pentru a simula o singură rostogolire și apoi se apelează singur în mod recursiv.

Legătura de ieșire a acestui program este puțin ciudată pentru a fi analizată din vizualizarea normală a graficului de bare / linii din AnyDice, așa că, în schimb, am exportat-o și am rulat-o prin scriptul Python din acest răspuns anterior pentru a o transforma într-o grilă bidimensională frumoasă pe care aș putea să o import în Google Sheets. Rezultatele, ca hartă termică și ca grafic cu mai multe bare, arată astfel:

Captură de ecran

În al doilea caz (adică toate reluările deodată) trebuie mai întâi să ne dăm seama care este de fapt strategia optimă. O clipă de gândire ne arată că:

  • Ar trebui să se refacă întotdeauna orice -1, deoarece acest lucru nu poate diminua niciodată rezultatul. Din moment ce rezultatul mediu așteptat al unei reîntoarceri este 0, media așteptată după reîntoarcerea tuturor -1-urilor este egală cu numărul de +1-uri din aruncarea inițială.

  • Întoarcerea unui zero nu schimbă rezultatul mediu așteptat, dar mărește varianța, adică face ca rezultatul real să fie mai probabil să fie mai departe de medie în ambele direcții. Astfel, ar trebui să se rostogolească zerouri doar dacă rezultatul mediu așteptat după ce se rostogolesc toate -1 (adică numărul de +1 din aruncarea inițială) este sub numărul țintă.

Aplicând această logică în AnyDice rezultă ceva de genul acestui 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 " }}

Exportând rezultatul acestui script și rulându-l prin același script Python și foaie de calcul se obține următoarea hartă termică și grafic de bare:

Captură de ecran

După cum puteți vedea, rezultatele nu sunt, de fapt, atât de diferite de cazul reluărilor secvențiale. Cele mai mari diferențe apar în cazul abilităților ridicate și al numerelor țintă intermediare: de exemplu, cu o abilitate de 4, posibilitatea de a efectua reluările pe rând și de a se opri în orice moment crește rata medie de succes de la 75,3% la 81% pentru o țintă de +1, sau de la 51,6% la 58,3% pentru o țintă de +2.

Ps. Am reușit să găsesc o modalitate de a face AnyDice să colecteze valorile „ratei de succes față de țintă” din cele două programe de mai sus într-o singură distribuție pentru fiecare valoare a abilității, permițând ca acestea să fie desenate direct de AnyDice sub formă de diagrame de bare sau grafice cu linii (în modul „cel puțin”) fără a fi nevoie să folosesc Python sau foi de calcul.

Din păcate, codul AnyDice pentru a face acest lucru este orice, dar nu este deloc simplu. Cea mai dificilă(!) parte s-a dovedit a fi găsirea unei modalități de a face AnyDice să scadă două probabilități (de exemplu, 1/2 – 1/3 = 1/6). Cel mai bun mod pe care îl cunosc pentru a îndeplini această sarcină aparent banală în AnyDice implică manipularea non-trivială a probabilităților condiționate și o buclă iterată. Și se blochează AnyDice dacă încercați să calculați 0 – 0 cu el.*

În orice caz, doar pentru a fi complet, iată codul AnyDice pentru calcularea și reprezentarea grafică a distribuției „celei mai înalte ținte de bătut” pentru diferite niveluri de îndemânare (și pentru fiecare dintre cele două mecanisme de reîntoarcere descrise mai sus), cu câteva comentarii adăugate pentru lizibilitate:

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

și o captură de ecran a rezultatului (în modul „cel puțin” grafic de linii):

Graph

O notă privind interpretarea rezultatului generat de programul de mai sus: Distribuțiile de probabilitate afișate pe graficul de mai sus nu corespund rezultatelor unei singure strategii de aruncare a zarurilor; mai degrabă, acestea sunt distribuții construite în mod artificial (adică „zaruri personalizate” în jargonul AnyDice) astfel încât probabilitatea de a arunca cel puțin \$N\$ la o singură aruncare a zarului personalizat este egală cu probabilitatea ca jucătorul să poată arunca cel puțin \$N\$ la 4dF cu mecanica de reîntoarcere dată (una câte una vs. toate deodată) și numărul maxim dat de rostogoliri, presupunând că jucătorul folosește strategia optimă de rostogolire pentru acea anumită țintă \$N\$.

Cu alte cuvinte, analizând rezultatul în modul „cel puțin”, putem vedea că un jucător cu nivelul de îndemânare 4 are o șansă de 51,62% de a reuși să scoată cu succes +2 sau mai mult (folosind mecanismul de rostogolire toate deodată) dacă își folosește rostogolirea disponibilă în modul care maximizează această șansă specială. Rezultatul arată, de asemenea, în mod corect că același jucător are o șansă de 75,28% de a obține +1 sau mai mult dacă alege să optimizeze pentru asta în schimb, dar vor avea nevoie de strategii de rostogolire diferite pentru a atinge aceste două obiective.

Și „probabilitatea” de 23,65% de a obține exact +1 pe zarul personalizat descris mai sus nu are cu adevărat nicio semnificație sensibilă, cu excepția faptului că este (aproximativ, din cauza rotunjirii) diferența dintre 75,28% și 51,62%. Ceea ce cred că este motivul pentru care este atât de greu de calculat cu AnyDice. 😛 Presupun că ai putea să o interpretezi ca pe o măsură a cât de greu este de atins o țintă de +2 folosind abilitatea dată și mecanismul de rostogolire decât o țintă de +1, într-un anumit sens, dar cam asta este tot.

*) Acest accident ar putea fi legat de ceea ce sunt destul de sigur că este un bug în AnyDice pe care l-am găsit în timp ce dezvoltam acest cod, ceea ce a făcut ca unul dintre primele mele programe de testare să genereze o ieșire foarte ciudată cu lucruri precum 97284.21% probabilități(!). De asemenea, programul de testare se blochează în cele din urmă dacă creșteți numărul de iterații în continuare.