Wie modelliere ich einen Fudge-Würfelwurf mit Re-Rolls in Anydice?

Hier ist eine alternative Lösung:

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

Die Funktion sollte größtenteils selbsterklärend sein; der einzige Teil, der vielleicht einer Erklärung bedarf, ist {1 .. #ROLL-N}@ROLL, der alle bis auf die letzten N Elemente der Sequenz ROLL summiert. Standardmäßig sortiert AnyDice Würfelwürfe in absteigender numerischer Reihenfolge, so dass die letzten Elemente die niedrigsten sind.

Im Graphmodus sehen die Ausgaben dieses Programms wie folgt aus:

Graph

Beachte, dass die Unterschiede zwischen den Fähigkeitsstufen 2, 3 und 4 ziemlich gering sind, da es ziemlich unwahrscheinlich ist, drei oder vier -1s auf 4dF zu würfeln.

BTW, das obige Programm geht davon aus, wie Sie am Ende Ihrer Frage sagen, dass die Spieler konservativ sind und nur negative Würfe wiederholen werden. Wenn Ihre Spieler gerne Risiken eingehen, könnten sie sich entscheiden, auch Nullen zu würfeln. In diesem Fall würden die Ergebnisse stattdessen so aussehen:

Grafik

Beachten Sie, dass die Durchschnittswerte immer noch die gleichen sind, aber die Ergebnisse für höhere Fertigkeiten eine viel größere Varianz aufweisen. Insbesondere die Wahrscheinlichkeit, eine perfekte Vier mit einer positiven Fertigkeit zu würfeln, ist auf diese Weise viel höher.

(Der einzige Unterschied zwischen den Programmen, die zur Erstellung der beiden obigen Diagramme verwendet wurden, besteht darin, dass das zweite Programm anstelle von verwendet.)

Insbesondere wenn Ihre Spieler versuchen, gegen eine bestimmte Mindestzielzahl zu würfeln, kann es für sie sinnvoll sein, nur so viele Nullen wie nötig zu würfeln, um ihre Chance, das Ziel zu erreichen, zu maximieren.

Die optimale Strategie in diesem Fall hängt davon ab, ob die Spieler die Würfel einzeln neu würfeln können und nach jedem Wurf entscheiden, ob sie weiter würfeln wollen, oder ob sie zuerst entscheiden müssen, welche Würfel sie neu würfeln wollen, und dann alle auf einmal würfeln.

Im ersten Fall (d.h.

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

Im ersten Fall (d.h. aufeinanderfolgende Würfe) kann der optimale Entscheidungsprozess mit einer rekursiven AnyDice-Funktion simuliert werden:

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

Hier gibt die Hauptfunktion ROLL reroll up to SKILL target TARGET eine 1 zurück, wenn der gegebene Wurf gleich oder größer als das Ziel ist, und eine 0, wenn er kleiner als das Ziel ist und keine Verbesserung möglich ist (d.h. es sind keine Würfel mehr im Pool, es sind keine weiteren Würfe erlaubt oder der niedrigste Würfel ist bereits eine +1). Andernfalls entfernt es den niedrigsten Würfel aus dem Pool (unter Verwendung einer Hilfsfunktion, da AnyDice nicht zufällig eine geeignete Funktion eingebaut hat), verringert die Anzahl der verbleibenden Wiederholungswürfe um eins, subtrahiert 1dF vom Zielwert, um einen einzelnen Wiederholungswurf zu simulieren, und ruft sich dann selbst rekursiv auf.

Die Ausgabe dieses Programms ist in der normalen Balken-/Liniendiagramm-Ansicht von AnyDice etwas umständlich zu parsen, also habe ich sie stattdessen exportiert und durch das Python-Skript aus dieser früheren Antwort laufen lassen, um sie in ein schönes zweidimensionales Raster zu verwandeln, das ich in Google Sheets importieren konnte. Die Ergebnisse, sowohl als Heatmap als auch als Balkendiagramm, sehen wie folgt aus:

Screenshot

Im zweiten Fall (d.h. alle Rerolls auf einmal) müssen wir zuerst herausfinden, was die optimale Strategie ist. Ein kurzer Moment des Nachdenkens zeigt, dass:

  • Man sollte immer alle -1s neu würfeln, da dies das Ergebnis niemals verringern kann. Da das erwartete durchschnittliche Ergebnis eines erneuten Wurfs 0 ist, entspricht der erwartete Durchschnitt nach dem erneuten Würfeln aller -1er der Anzahl der +1er im ursprünglichen Wurf.

  • Das erneute Würfeln einer Null ändert nicht das erwartete durchschnittliche Ergebnis, aber es erhöht die Varianz, d.h. es macht das tatsächliche Ergebnis wahrscheinlicher, dass es in beiden Richtungen weiter vom Durchschnitt entfernt ist. Daher sollte man nur dann erneut Nullen würfeln, wenn das erwartete durchschnittliche Ergebnis nach dem erneuten Würfeln aller -1er (d. h. der Anzahl der +1er im ursprünglichen Wurf) unter der Zielzahl liegt.

Wenn man diese Logik in AnyDice anwendet, ergibt sich etwa folgendes Programm:

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

Wenn man die Ausgabe dieses Skripts exportiert und durch dasselbe Python-Skript und dieselbe Tabellenkalkulation laufen lässt, erhält man die folgende Heatmap und das folgende Balkendiagramm:

Screenshot

Wie man sieht, unterscheiden sich die Ergebnisse nicht wirklich vom Fall der sequentiellen Neuwürfe. Die größten Unterschiede treten bei hohen Fertigkeiten und mittleren Zielzahlen auf: Wenn man zum Beispiel bei einer Fertigkeit von 4 in der Lage ist, die Wiederholungswürfe nacheinander durchzuführen und an einem beliebigen Punkt zu stoppen, steigt die durchschnittliche Erfolgsrate von 75,3 % auf 81 % für ein Ziel von +1 oder von 51,6 % auf 58,3 % für ein Ziel von +2.

P. Ich habe es geschafft, einen Weg zu finden, wie AnyDice die Werte für „Erfolgsrate vs. Ziel“ aus den beiden obigen Programmen zu einer einzigen Verteilung für jeden Fertigkeitswert zusammenfassen kann, so dass sie direkt von AnyDice als Balken- oder Liniendiagramme (im „mindestens“-Modus) gezeichnet werden können, ohne Python oder Tabellenkalkulationen verwenden zu müssen.

Leider ist der AnyDice-Code dafür alles andere als einfach. Der schwierigste(!) Teil bestand darin, einen Weg zu finden, AnyDice dazu zu bringen, zwei Wahrscheinlichkeiten zu subtrahieren (z.B. 1/2 – 1/3 = 1/6). Der beste Weg, den ich kenne, um diese scheinbar triviale Aufgabe in AnyDice auszuführen, beinhaltet nicht-triviale Manipulationen von bedingten Wahrscheinlichkeiten und eine iterierte Schleife. Und es bringt AnyDice zum Absturz, wenn man versucht, 0 – 0 damit zu berechnen.*

Nur der Vollständigkeit halber hier der AnyDice-Code zur Berechnung und Darstellung der Verteilung des „höchsten schlagbaren Ziels“ für verschiedene Fertigkeitsstufen (und für jede der beiden oben beschriebenen Wiederholungsmechanismen) mit einigen Kommentaren zur besseren Lesbarkeit:

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

und ein Screenshot der Ausgabe (zumindest im Liniendiagramm-Modus):

Graph

Eine Anmerkung zur Interpretation der vom obigen Programm erzeugten Ausgabe: Die im obigen Graphen dargestellten Wahrscheinlichkeitsverteilungen entsprechen nicht den Ergebnissen einer einzelnen Würfelwurfstrategie; vielmehr handelt es sich um künstlich konstruierte Verteilungen (d.h. „Custom Dice“ im AnyDice-Jargon), so dass die Wahrscheinlichkeit, bei einem einzigen Wurf des Custom Dice mindestens \$N\$ zu würfeln, der Wahrscheinlichkeit entspricht, dass der Spieler in der Lage ist, mindestens \$N\$ auf 4dF mit der gegebenen Wiederholungsmechanik (einer nach dem anderen vs. alle auf einmal) und dem gegebenen Maximum zu würfeln.

Mit anderen Worten, wenn man sich die Ausgabe im Modus „mindestens“ ansieht, kann man erkennen, dass ein Spieler mit der Fertigkeitsstufe 4 eine 51,62%ige Chance hat, +2 oder mehr zu würfeln (unter Verwendung der Mechanik des einmaligen Wiederholens), wenn er seine verfügbaren Wiederholungswürfe so einsetzt, dass er diese spezielle Chance maximiert. Die Ausgabe zeigt auch korrekt an, dass derselbe Spieler eine 75,28%ige Chance hat, +1 oder mehr zu würfeln, wenn er sich stattdessen dafür entscheidet, dies zu optimieren, aber er braucht unterschiedliche Wiederholungsstrategien, um diese beiden Ziele zu erreichen.

Und die „Wahrscheinlichkeit“ von 23,65% für das Würfeln von genau +1 auf dem oben beschriebenen benutzerdefinierten Würfel hat wirklich keine sinnvolle Bedeutung, außer dass es (ungefähr, aufgrund der Rundung) die Differenz zwischen 75,28% und 51,62% ist. Das ist wohl auch der Grund, warum es mit AnyDice so schwer zu berechnen ist 😛 Ich nehme an, man könnte es als Maß dafür interpretieren, wie viel schwieriger es ist, ein Ziel von +2 mit der gegebenen Fertigkeit und Wiederholungsmechanik zu erreichen als ein Ziel von +1, aber das war’s dann auch schon.

*) Dieser Absturz könnte mit einem Fehler in AnyDice zusammenhängen, den ich bei der Entwicklung dieses Codes gefunden habe und der dazu führt, dass eines meiner frühen Testprogramme wirklich seltsame Ausgaben mit Werten wie 97284,21% Wahrscheinlichkeit(!) erzeugt. Das Testprogramm stürzt schließlich auch ab, wenn man die Iterationszahl weiter erhöht.