Hur modellerar jag en fudge tärningskast med re-rolls i Anydice?

Här är 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 borde vara självförklarande; den enda delen som kan behöva förklaras är {1 .. #ROLL-N}@ROLL, som summerar alla utom de sista N elementen i sekvensen ROLL. Som standard sorterar AnyDice tärningskast i fallande numerisk ordning, så de sista elementen är de lägsta.

I diagramläge ser utgångarna från detta program ut så här:

Graph

Bemärk hur skillnaderna mellan färdighetsnivåerna 2, 3 och 4 är ganska små, eftersom det är ganska osannolikt att kasta tre eller fyra -1 på 4dF till att börja med.

BTW, programmet ovan förutsätter, som du säger i slutet av din fråga, att spelarna är konservativa och bara kommer att rulla om negativa kast. Om dina spelare gillar att ta risker kanske de bestämmer sig för att rerulla nollor också, och i så fall skulle resultaten se ut så här istället:

Graph

Notera hur medelvärdena fortfarande är desamma, men resultaten för högre färdigheter har mycket större varians. I synnerhet är sannolikheten att kasta en perfekt fyra med en positiv färdighet mycket högre på detta sätt.

(Den enda skillnaden mellan de program som används för att generera de två graferna ovan är att det andra använder i stället för .)

Insärskilt om dina spelare försöker kasta mot ett specifikt minsta måltal kan det vara vettigt för dem att bara kasta så många nollor som behövs för att maximera sin chans att nå målet.

Den optimala strategin i dessa fall beror på om spelarna kan rulla om tärningarna en och en och bestämma efter varje kast om de vill fortsätta rulla om, eller om de först måste bestämma vilka tärningar de vill rulla om och sedan rulla dem alla på en gång.

I det första fallet (dvs. sekventiella omkastningar) kan den optimala beslutsprocessen simuleras 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 " }}

Här returnerar huvudfunktionen ROLL reroll up to SKILL target TARGET 1 om det givna kastet är lika med eller större än målet, och 0 om det är mindre än målet och ingen förbättring är möjlig (dvs. det finns inga fler tärningar kvar i poolen, inga fler omkastningar tillåts eller den lägsta tärningen är redan en +1). I annat fall tar den bort den lägsta tärningen från poolen (med hjälp av en hjälpfunktion, eftersom AnyDice inte råkar ha en lämplig funktion inbyggd i sig), minskar antalet återstående rerolls med ett, subtraherar 1dF från målvärdet för att simulera en enda reroll och anropar sedan sig själv rekursivt.

Resultatet av det här programmet är lite besvärligt att analysera från AnyDices normala stapel-/linjediagramvy, så jag exporterade det istället och körde det genom Python-skriptet från det här tidigare svaret för att förvandla det till ett trevligt tvådimensionellt rutnät som jag kunde importera till Google Sheets. Resultaten, som en värmekarta och som ett diagram med flera staplar, ser ut så här:

Screenshot

I det andra fallet (dvs. alla rerolls på en gång) måste vi först ta reda på vad den optimala strategin faktiskt är. Ett ögonblick av eftertanke visar att:

  • Man bör alltid rerollera alla -1s, eftersom det aldrig kan minska resultatet. Eftersom det förväntade genomsnittliga resultatet av en omkastning är 0, är det förväntade genomsnittet efter att ha omkastat alla -1:or lika med antalet +1:or i det ursprungliga kastet.

  • Att omkasta en nolla förändrar inte det förväntade genomsnittliga resultatet, men det ökar variansen, det vill säga det gör att det faktiska resultatet har större sannolikhet att ligga längre bort från genomsnittet i endera riktningen. Därför bör man bara rulla om nollor om det förväntade genomsnittliga resultatet efter att alla -1:or har rullats om (dvs. antalet +1:or i den ursprungliga rullen) är lägre än målvärdet.

Användning av denna logik i AnyDice resulterar i något som liknar detta 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 " }}

Att exportera utfallet av detta skript och köra det genom samma Python-skript och kalkylblad ger följande värmekarta och stapeldiagram:

Screenshot

Som du kan se skiljer sig resultaten faktiskt inte så mycket från fallet med sekventiella omkastningar. De största skillnaderna uppstår med höga färdigheter och mellanliggande måltal: till exempel, med en färdighet på 4, om man kan utföra omkastningarna en i taget och sluta när som helst ökar den genomsnittliga framgångsfrekvensen från 75,3 % till 81 % för ett måltal på +1, eller från 51,6 % till 58,3 % för ett måltal på +2.

Ps. Jag lyckades komma på ett sätt att få AnyDice att samla in värdena för ”success rate vs. target” från de två programmen ovan till en enda fördelning för varje färdighetsvärde, vilket gör att de kan ritas direkt av AnyDice som stapeldiagram eller linjediagram (i läget ”minst”) utan att behöva använda Python eller kalkylblad.

Tyvärr är AnyDice-koden för att göra det allt annat än enkel. Den svåraste(!) delen visade sig vara att hitta ett sätt att få AnyDice att subtrahera två sannolikheter (t.ex. 1/2 – 1/3 = 1/6). Det bästa sätt som jag känner till för att utföra denna till synes triviala uppgift i AnyDice inbegriper icke-trivial hantering av villkorliga sannolikheter och en itererad slinga. Och det kraschar AnyDice om man försöker beräkna 0 – 0 med det.*

Här är AnyDice-koden för att beräkna och plotta fördelningen av det ”högsta slagbara målet” för olika färdighetsnivåer (och för var och en av de två omkastningsmekanismerna som beskrivs ovan) med några kommentarer som lagts till för läsbarhetens skull:

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

och en skärmdump av resultatet (i ”åtminstone” linjediagramläge):

Graph

En anmärkning om tolkningen av det resultat som genereras av programmet ovan: De sannolikhetsfördelningar som visas i grafen ovan motsvarar inte resultaten av någon enskild tärningsstrategi; de är snarare artificiellt konstruerade fördelningar (dvs. ”anpassade tärningar” på AnyDice-jargong) så att sannolikheten för att kasta minst \$N\$ på ett enda kast av den anpassade tärningen är lika med sannolikheten för att spelaren ska kunna kasta minst \$N\$ på 4dF med den givna rerolling-mekaniken (en i taget mot.

Med andra ord, om vi tittar på resultatet i ”minst”-läge kan vi se att en spelare med färdighetsnivå 4 har 51,62% chans att lyckas kasta +2 eller mer (genom att använda mekaniken med alla på en gång) om han/hon använder sina tillgängliga rerolls på det sätt som maximerar den speciella chansen. Utmatningen visar också korrekt att samma spelare har 75,28% chans att få +1 eller mer om de väljer att optimera för det istället, men de behöver olika strategier för omkastningar för att nå dessa två mål.

Och ”sannolikheten” på 23,65% för att få exakt +1 på den anpassade tärningen som beskrivs ovan har egentligen ingen vettig innebörd, förutom att det är (ungefärligt, på grund av avrundning) skillnaden mellan 75,28% och 51,62%. Jag antar att det är därför det är så svårt att beräkna med AnyDice 😛 Jag antar att man kan tolka det som ett mått på hur mycket svårare ett mål på +2 är att uppnå med hjälp av den givna färdigheten och omkastningsmekaniken än ett mål på +1, i någon mening, men det är ungefär allt.

*) Den kraschen kan vara relaterad till vad jag är ganska säker på är en bugg i AnyDice som jag hittade när jag utvecklade den här koden, vilket fick ett av mina tidiga testprogram att generera riktigt konstiga utdata med saker som 97284.21% sannolikheter(!). Testprogrammet kraschar också så småningom om man ökar iterationsantalet ytterligare.