Como faço para modelar um rolo de dados de fudge com re-rolos em Anydice?

Aqui está uma solução 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 "}

A função deve ser na maioria das vezes auto-explicativa; a única parte que pode requerer explicação é {1 .. #ROLL-N}@ROLL, que soma todos menos os últimos N elementos da sequência ROLL. Por padrão, AnyDice ordena os rolos de dados em ordem numérica decrescente, portanto os últimos elementos são os mais baixos.

No modo gráfico, as saídas deste programa parecem assim:

Graph

Notem como as diferenças entre os níveis de habilidade 2, 3 e 4 são bastante pequenas, uma vez que rolar três ou quatro -1s em 4dF é bastante improvável de começar.

BTW, o programa acima assume, como você diz no final da sua pergunta, que os jogadores são conservadores e só vão fazer rolls negativos. Se os seus jogadores gostam de correr riscos, eles podem decidir fazer reroll zeros também, neste caso os resultados ficariam assim:

Graph

Note como as médias ainda são as mesmas, mas os resultados para habilidades mais altas têm muito mais variância. Em particular, as probabilidades de rolar um quatro perfeito com uma habilidade positiva são muito maiores desta forma.

(A única diferença entre os programas usados para gerar os dois gráficos acima é que o segundo usa em vez de .)

Em particular, se seus jogadores estão tentando rolar contra um número mínimo específico de alvos, pode fazer sentido para eles rolarem apenas tantos zeros quantos forem necessários para maximizar suas chances de atingir o alvo.

A estratégia ideal nestes casos depende de se os jogadores podem re-pilotar os dados um a um, e decidir depois de cada jogada se querem continuar a re-pilotar, ou se têm de decidir primeiro quais os dados que querem re-pilotar e depois lançá-los todos de uma vez.

No primeiro caso (i.e. rerolls seqüenciais) o processo de decisão ideal pode ser simulado com uma função AnyDice recursiva:

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

Aqui, a função principal ROLL reroll up to SKILL target TARGET retorna 1 se o lançamento dado for igual ou maior que o alvo, e 0 se for menor que o alvo e nenhuma melhoria for possível (ou seja, não há mais dados no pool, não são permitidos mais rerolls ou o dado mais baixo já é um +1). Caso contrário, ele remove o dado mais baixo do pool (usando uma função auxiliar, já que AnyDice não tem um dado adequado incorporado), diminui o número de rerolls restantes por um, subtrai 1dF do valor alvo para simular um único reroll e então se chama recursivamente.

A saída deste programa é um pouco estranha para ser analisada a partir da vista normal do gráfico de linha/barra de AnyDice, então eu o exportei e o executei através do script Python a partir desta resposta anterior para transformá-lo em uma bela grade bidimensional que eu poderia importar para o Google Sheets. Os resultados, como um mapa de calor e como um gráfico multi-barras, parecem ser assim:

Screenshot

No segundo caso (ou seja, todos os rerolls de uma vez) precisamos primeiro descobrir qual é a estratégia ideal. Um momento de reflexão mostra que:

  • Um deve sempre repetir qualquer -1s, uma vez que fazê-lo nunca pode diminuir o resultado. Como o resultado médio esperado de uma repetição é 0, a média esperada após a repetição de todos os -1s é igual ao número de +1s na repetição inicial.

  • Inversão de um zero não altera o resultado médio esperado, mas aumenta a variância, ou seja, torna o resultado real mais provável de estar mais longe da média em qualquer direção. Assim, só se deve reescrever zeros se o resultado médio esperado após reescrever todos os -1s (ou seja, o número de +1s no rolo inicial) estiver abaixo do número alvo.

Aplicando esta lógica em AnyDice resulta em algo como este programa:

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

Exportar a saída deste script e executá-lo através do mesmo script Python e planilha eletrônica dá o seguinte mapa de calor e gráfico de barras:

Screenshot

Como você pode ver, os resultados não são na verdade tão diferentes do caso seqüencial de rerolls. As maiores diferenças ocorrem com altas habilidades e números de alvos intermediários: por exemplo, com uma habilidade de 4, ser capaz de realizar os rerolls um de cada vez e parar em qualquer ponto aumenta a taxa média de sucesso de 75,3% para 81% para um alvo de +1, ou de 51,6% para 58,3% para um alvo de +2,

Ps. Eu consegui descobrir uma maneira de fazer AnyDice coletar os valores de “taxa de sucesso vs. alvo” dos dois programas acima em uma única distribuição para cada valor de habilidade, permitindo que eles sejam desenhados diretamente por AnyDice como gráficos de barras ou gráficos de linhas (em modo “pelo menos”) sem ter que usar Python ou planilhas eletrônicas.

Felizmente, o código AnyDice para fazer isso é tudo menos simples. A parte mais difícil(!) acabou sendo encontrar uma maneira de fazer AnyDice subtrair duas probabilidades (por exemplo 1/2 – 1/3 = 1/6). A melhor maneira que conheço de executar esta tarefa aparentemente trivial em AnyDice envolve manipulação não trivial de probabilidades condicionais e um loop iterado. E ele trava AnyDice se você tentar calcular 0 – 0 com ele.*

Ainda, apenas para completar, aqui está o código AnyDice para calcular e plotar a distribuição do “alvo mais alto” para vários níveis de habilidade (e para cada uma das duas mecânicas de repetição descritas acima) com alguns comentários adicionados para a legibilidade:

\- 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 uma captura de tela da saída (em modo gráfico de linha “pelo menos”):

Graph

Uma nota sobre a interpretação da saída gerada pelo programa acima: As distribuições de probabilidade mostradas no gráfico acima não correspondem aos resultados de qualquer estratégia de lançamento de dados; ao contrário, são distribuições artificialmente construídas (isto é, “dados personalizados” no jargão AnyDice) de tal forma que a probabilidade de lançar pelo menos \$N\$ em um único lançamento do dado personalizado é igual à probabilidade do jogador poder lançar pelo menos \$N\$ em 4dF com o mecanismo de rerolar dado (um de cada vez vs. todos de uma vez) e o número máximo dado de rerolls, assumindo que o jogador usa a estratégia de rerolling ideal para aquele alvo em particular \$N\$.

Em outras palavras, olhando para a saída no modo “pelo menos”, podemos ver que um jogador com nível de habilidade 4 tem 51,62% de chance de rolar com sucesso +2 ou mais (usando o mecanismo de rerolling all-at-once) se ele estiver usando seus rerolls disponíveis da maneira que maximiza aquela chance em particular. A saída também mostra corretamente que o mesmo jogador tem 75,28% de chance de rodar +1 ou mais se ele optar por otimizar para isso, mas ele precisará de estratégias diferentes de rerolling para atingir esses dois objetivos.

E a “probabilidade” de 23,65% de rodar exatamente +1 no dado personalizado descrito acima realmente não tem nenhum significado sensato, exceto que é (aproximadamente, devido ao arredondamento) a diferença entre 75,28% e 51,62%. O que eu acho que é por isso que é tão difícil calcular com AnyDice 😛 Suponho que você poderia interpretar isso como uma medida de quanto mais difícil um alvo de +2 é encontrar usando a habilidade dada e o mecanismo de redirecionamento do que um alvo de +1, em algum sentido, mas é mais ou menos isso.

*) Essa falha pode estar relacionada ao que eu tenho certeza que é um bug em AnyDice que eu encontrei enquanto desenvolvia esse código, fazendo com que um dos meus primeiros programas de teste gerasse uma saída realmente estranha com coisas como 97284.21% de probabilidades(!). O programa de teste também eventualmente trava se você aumentar ainda mais a contagem de iteração.