Come posso modellare un lancio di dadi fudge con rilanci in Anydice?

Ecco una soluzione alternativa:

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

La funzione dovrebbe essere per lo più autoesplicativa; l’unica parte che potrebbe richiedere una spiegazione è {1 .. #ROLL-N}@ROLL, che somma tutti gli elementi della sequenza ROLL tranne gli ultimi N. Per impostazione predefinita, AnyDice ordina i lanci di dadi in ordine numerico decrescente, quindi gli ultimi elementi sono i più bassi.

In modalità grafico, i risultati di questo programma appaiono così:

Graph

Nota come le differenze tra i livelli di abilità 2, 3 e 4 siano abbastanza minori, poiché lanciare tre o quattro -1 su 4dF è abbastanza improbabile per cominciare.

BTW, il programma di cui sopra presuppone, come dici alla fine della tua domanda, che i giocatori siano conservatori e rilancino solo i lanci negativi. Se ai tuoi giocatori piace rischiare, potrebbero decidere di rilanciare anche gli zeri, nel qual caso i risultati sarebbero invece come questo:

Graph

Nota come le medie sono ancora le stesse, ma i risultati per le abilità più alte hanno molta più varianza. In particolare, le probabilità di tirare un quattro perfetto con un’abilità positiva sono molto più alte in questo modo.

(L’unica differenza tra i programmi usati per generare i due grafici sopra è che il secondo usa invece di .)

In particolare, se i vostri giocatori stanno cercando di tirare contro un numero minimo specifico di obiettivi, potrebbe avere senso per loro tirare solo tanti zeri quanti sono necessari per massimizzare le loro possibilità di raggiungere l’obiettivo.

La strategia ottimale in questi casi dipende dal fatto che i giocatori possono rilanciare i dadi uno per uno, e decidere dopo ogni lancio se vogliono continuare a rilanciare, o se devono prima decidere quali dadi vogliono rilanciare e poi lanciarli tutti insieme.

Nel primo caso (cioè Nel primo caso (cioè i rilanci sequenziali) il processo decisionale ottimale può essere simulato con una funzione ricorsiva 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 " }}

Qui la funzione principale ROLL reroll up to SKILL target TARGET restituisce 1 se il lancio dato è uguale o superiore all’obiettivo, e 0 se è inferiore all’obiettivo e nessun miglioramento è possibile (cioè non ci sono più dadi nel pool, non sono consentiti altri rilanci o il dado più basso è già un +1). Altrimenti rimuove il dado più basso dal pool (usando una funzione di aiuto, dato che AnyDice non ne ha una adatta incorporata), diminuisce il numero di rilanci rimanenti di uno, sottrae 1dF dal valore di destinazione per simulare un singolo rilancio e poi chiama se stessa ricorsivamente.

L’output di questo programma è un po’ scomodo da analizzare dal normale grafico a barre/linea di AnyDice, quindi l’ho esportato e l’ho fatto girare attraverso lo script Python di questa risposta precedente per trasformarlo in una bella griglia bidimensionale che ho potuto importare in Google Sheets. I risultati, come mappa di calore e come grafico a più barre, appaiono così:

Screenshot

Nel secondo caso (cioè tutti i reroll in una volta) dobbiamo prima capire qual è la strategia ottimale. Un attimo di riflessione mostra che:

  • Si dovrebbe sempre rerollare qualsiasi -1, poiché così facendo non si può mai diminuire il risultato. Poiché il risultato medio atteso di un rilancio è 0, la media attesa dopo aver rilanciato tutti i -1 è uguale al numero di +1 del lancio iniziale.

  • Rilanciare uno zero non cambia il risultato medio atteso, ma aumenta la varianza, cioè rende più probabile che il risultato effettivo sia più lontano dalla media in entrambe le direzioni. Quindi, si dovrebbe rilanciare gli zeri solo se il risultato medio atteso dopo aver rilanciato tutti i -1 (cioè il numero di +1 nel lancio iniziale) è inferiore al numero obiettivo.

Applicando questa logica in AnyDice si ottiene qualcosa di simile a questo programma:

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

Esportare l’output di questo script ed eseguirlo attraverso lo stesso script Python e il foglio di calcolo fornisce la seguente mappa di calore e il grafico a barre:

Screenshot

Come potete vedere, i risultati non sono poi così diversi dal caso dei rilanci sequenziali. Le maggiori differenze si verificano con abilità elevate e numeri di bersaglio intermedi: per esempio, con un’abilità di 4, essere in grado di eseguire i reroll uno alla volta e fermarsi in qualsiasi punto aumenta la percentuale media di successo dal 75,3% all’81% per un obiettivo di +1, o dal 51,6% al 58,3% per un obiettivo di +2.

Ps. Sono riuscito a trovare un modo per fare in modo che AnyDice raccolga i valori di “percentuale di successo vs. obiettivo” dai due programmi di cui sopra in una singola distribuzione per ogni valore di abilità, permettendo loro di essere disegnati direttamente da AnyDice come grafici a barre o a linee (in modalità “almeno”) senza dover usare Python o fogli di calcolo.

Purtroppo, il codice AnyDice per fare ciò è tutto fuorché semplice. La parte più difficile(!) si è rivelata essere trovare un modo per far sottrarre ad AnyDice due probabilità (ad esempio 1/2 – 1/3 = 1/6). Il modo migliore che conosco per eseguire questo compito apparentemente banale in AnyDice implica una manipolazione non banale delle probabilità condizionali e un ciclo iterato. E manda in crash AnyDice se si prova a calcolare 0 – 0 con esso.*

Ad ogni modo, solo per completezza, ecco il codice di AnyDice per calcolare e tracciare la distribuzione del “bersaglio più alto da battere” per vari livelli di abilità (e per ciascuna delle due meccaniche di rilancio descritte sopra) con alcuni commenti aggiunti per leggibilità:

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

e uno screenshot dell’output (in modalità “almeno” grafico a linee):

Graph

Una nota sull’interpretazione dell’output generato dal programma sopra: Le distribuzioni di probabilità mostrate nel grafico sopra non corrispondono ai risultati di una singola strategia di lancio dei dadi; piuttosto, sono distribuzioni costruite artificialmente (cioè “dadi personalizzati” nel gergo di AnyDice) tali che la probabilità di lanciare almeno \$N\$$ su un singolo lancio del dado personalizzato è uguale alla probabilità del giocatore di lanciare almeno \$N\$$ su 4dF con il dato meccanismo di rilancio (uno alla volta vs. tutto in una volta) e il numero massimo di rilanci dato, assumendo che il giocatore usi la strategia di rilancio ottimale per quel particolare obiettivo \N\$$.

In altre parole, guardando l’output in modalità “almeno”, possiamo vedere che un giocatore con livello di abilità 4 ha il 51.62% di possibilità di tirare con successo +2 o più (usando il meccanismo di rilancio tutto in una volta) se sta usando i suoi rilanci disponibili nel modo che massimizza quella particolare possibilità. L’output mostra anche correttamente che lo stesso giocatore ha il 75,28% di possibilità di tirare +1 o più se invece sceglie di ottimizzare per quello, ma avrà bisogno di strategie di rilancio diverse per raggiungere questi due obiettivi.

E la “probabilità” del 23,65% di tirare esattamente +1 sul dado personalizzato descritto sopra non ha davvero alcun significato sensato, tranne che è (approssimativamente, a causa degli arrotondamenti) la differenza tra il 75,28% e il 51,62%. Immagino sia il motivo per cui è così difficile da calcolare con AnyDice. 😛 Suppongo che si possa interpretare come una misura di quanto sia più difficile raggiungere un obiettivo di +2 usando la data abilità e la meccanica di rilancio rispetto a un obiettivo di +1, in un certo senso, ma questo è tutto.

*) Quel crash potrebbe essere collegato a quello che sono abbastanza sicuro sia un bug in AnyDice che ho trovato mentre sviluppavo questo codice, causando uno dei miei primi programmi di test per generare output molto strani con cose come 97284.21% di probabilità(!). Il programma di test va anche in crash se si aumenta ulteriormente il numero di iterazioni.