4
\$\begingroup\$

I'm trying to generate a complex probability table for a dice pool with between 1--14 D6. The table will contain the probability of success and of critical failure, given a number of D6 rolled and a difficulty level between 1--10.

Difficulty = Minimum number of successful rolls that must be obtained, where success is a roll of 4+ on one die (50% chance per die).

Degree of Success: if you succeed, you count the number of 6s rolled in the dice pool (up to 4). This determines the "level" of your success, where 0=minimally acceptable and 4=outstanding.

Critical Fail: This happens if you fail, and if you roll more 1s than 4s,5s,6s combined.

Automatic Success: If you roll more than 12 dice, every dice beyond the 12th is an automatic success (but does not count toward degree of success).

Here are my goals:

  • Calculate probability of overall success for N of dice in the pool between 1--14, and difficulty levels 1--10 (dice beyond 12th are automatic successes).
  • Calculate probability of botching for same N of dice 1--14 and levels of difficulty 1--10 (botching is when you fail and roll more 1s than 4,5,6s combined).
  • Extra credit goal: calculate probability of each level of success for every combination of N dice and difficulty level (level of success=N of 6s, conditional on succeeding).

Is there a neat way to accomplish these goals with combinatorics? Excel would be great, but maybe AnyDice or another program?

\$\endgroup\$
4
  • \$\begingroup\$ So just to clarify, the most dice you ever roll is 12, so your 14 example would be 12 die rolls +2 regular succeses? \$\endgroup\$
    – Alan
    Mar 3, 2023 at 0:15
  • \$\begingroup\$ @Alan, that's correct! \$\endgroup\$
    – ThetaHanse
    Mar 3, 2023 at 2:42
  • \$\begingroup\$ I'm missing where imaginary probability factors in here. aip.scitation.org/doi/10.1063/1.531906 \$\endgroup\$
    – Yakk
    Mar 3, 2023 at 14:44
  • \$\begingroup\$ @Yakk, sorry about that, using the layman's definition of complex, nothing actually complex or exotic =) \$\endgroup\$
    – ThetaHanse
    Mar 4, 2023 at 5:28

5 Answers 5

5
\$\begingroup\$

Spreadsheet at here

It's set for view only but you can make a copy for your own use from the file menu.

This isn't that hard to do exactly with combinatorics. The sheet is for 1-12 dice, for dice beyond 12 just lower the effective difficulty by the bonus dice

We can simplify the situation by breaking it down into "Do we succeed or fail" and then "Is this failure a critical failure" or "How many degrees is this success"?

Both of these are Bernoulli trials. Cell A2 you put the # of dice from 1-12, cell B2 you put the difficulty (subtracting any bonus successes you get for dice over 12)....that little bit can be added to the code easily if desired.

Formula explanations

Just doing the top cell for each one, the rest all copy down for the numbers of success

Cell B4: =IF(A4<=$A$2,BINOM.DIST(A4,$A$2,0.5,FALSE),0)

This first checks to see if the number in our 0-12 row makes sense for the # of dice we are rolling. If it does, then it gives the chance of exactly the number in A4's successes coming up out of the number of dice we are rolling, this is repeated down the column. If we are doing a number more than the number of dice possible, it gives 0

Cell C4= =IF(A4<$B$2,IF($A$2-A4>A4,BINOM.DIST(A4,$A$2-A4,1/3,TRUE),1))

This first checks to see if the # of 4,5,6s was below the difficulty threshold. If so, it is a failure and a potential botch.
It then checks to see if a botch is possible by making sure the number of failure dice is more than the number of success dice. If it is possible, it then checks to see if a botch DIDN'T occur. This will happen if the total number of 1's out of the failure dice was less than or equal to the number of successful dice. This is a cumulative distribution, each of the failure dice (Total # of dice-number successful) has a 1 in 3 chance of being a 1 (vs a 2 or 3). We check the probability that the total number of 1s is equal to or less than the number of successes rolled (a4). If so, not a botch. There's a 100% chance not a botch if there just aren't enough dice to overcome the numbers of successes (on the case of n=5, d=4, 3 successes means the most 1s you could have are 2), so to cover that case there's a default 1=100% not a botch.

I didn't bother to put error catching in for the "false" lines as false evaluates as 0, so it cancels later on any time it is used in multiplication.

Cell D4==IF(C4,1-C4,0)

anything other than 0 evaluates as true, so if there is a nonzero chance of it being a not botch, this calculates the botch as the complement event, 1-the botch percent. Otherwise we are in one of those false cases, so output 0 to make the math easier later on in cancelling excess rows not needed

Cell E4===IF($A$4<$B$2,B4*D4,0)

This first checks to make sure we are on a failure line. If so, it multiplies the botch percent * the percent chance of that particular outcome coming up in the first place, otherwise outputs 0

Cell F4==B4*C4

This gives the percentage of times that row of failures is a regular botch.

Cell G4==IF(A4<$B$2,0,B4)

This just puts a 0 in if that row is a failure and otherwise the percent chance if that row was a success

Cell H4 through L4,

These just multiply the chance of us being on that row on a success * the chance worked out later on for "if it were a success, how many degrees would it be"

Cell M4-P4, example M4==IF($A4<$B$2,0,BINOM.DIST(M$3,$A4,1/3,FALSE))

This first makes sure we are on a success (otherwise 0). If we are on a success, it does a Bernoulli frequency distribution on the number of successes we had, looking for the 1 in 3 chance that each successful die was a 6.

Cell Q4==IF($A4<$B$2,0,1-sum(M4:P4))

We get 4 or more if we aren't in the 0-3 column

Then the topline results are just summations.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ This is cool—one thing you might want to note is that the spreadsheet is read-only, so anyone who wants to play around with it will need to make a copy (which is in the file menu). Also, I spot-checked a few of the results, and so far your spreadsheet and my simulations agree, so yay reproducibility 👍🏻 \$\endgroup\$
    – nben
    Mar 3, 2023 at 2:18
  • \$\begingroup\$ @nben Added the comment, and woot when theory and practice actually match :). \$\endgroup\$
    – Alan
    Mar 3, 2023 at 3:35
  • \$\begingroup\$ Assumption is correct, and that's a great way to think about it (each D6 that would be rolled over 12 lowers effective difficulty by 1)! Breaking the logic down into steps rather than a giant nested statement makes the solution clearer. I'm working through this. Only thing I noticed right now is that formula in cell L4 references Q3 (should be Q4?). \$\endgroup\$
    – ThetaHanse
    Mar 3, 2023 at 3:47
  • \$\begingroup\$ @ThetaHanse woops, yeah, fixed. \$\endgroup\$
    – Alan
    Mar 3, 2023 at 4:10
5
\$\begingroup\$

Here's a script using my Icepool Python library:

from icepool import Die

die = Die({
    (0, 0, 1) : 1,  # 1: bad
    (0, 0, 0) : 2,  # 2-3: nothing
    (1, 0, 0) : 2,  # 4-5: success
    (1, 1, 0) : 1,  # 6: trigger
})

extra = Die({
    (1, 0, 0) : 1,  # success
})

def score(successes, triggers, bads):
    return successes, min(triggers, 4), bads > successes
    
def degenesis(n):
    raw = min(n, 12) @ die + max(n - 12, 0) @ extra
    result = raw.map(score)
    print(result)
    for triggers in [4, 3, 2, 1, 0]:
        area(result.map(lambda s, t, b: s if t >= triggers and not b else -1), f'{n} dice: +{triggers}')
    output(result.map(lambda s, t, b: s if b else 100), f'{n} dice: clean')

limit(0, 5)
degenesis(5)

Example graph.

The dice have vector values, represented by tuples (4+s, 6s, 1s). The @ operator means roll the right side a number of times equal to the left side, and add them up. Then the map method is used to convert the raw result to what we want to know.

This computes the full joint distribution of (successes, degree, may_botch), which is stored in result. Then it plots the chance of getting at least 0...4 degree while also reaching at least a given degree of success, with the difficulty achieved on the x-axis. It also plots the chance of getting a "clean" roll (no botch if failed).

\$\endgroup\$
4
  • \$\begingroup\$ I was not aware of this package, thanks! \$\endgroup\$
    – nben
    Mar 3, 2023 at 5:21
  • \$\begingroup\$ A CDF might be of more use to visualize this; the chance of 4+ success, 3+ success, 2+ success, 1+ success, 0+ success, failure-or-better, and 100% (aka, crit-fail or better). \$\endgroup\$
    – Yakk
    Mar 3, 2023 at 14:48
  • \$\begingroup\$ But then we'd have to input each difficulty separately. Perhaps the solution for joint distributions is another type of graph, such as a treemap. \$\endgroup\$ Mar 4, 2023 at 4:39
  • \$\begingroup\$ After some more thought, I see more of what you're saying. Though it will require plots that sum to less than 100%, which will in turn require some careful API design as a general feature. \$\endgroup\$ Mar 5, 2023 at 3:38
3
\$\begingroup\$

While I have no doubt that exact answers can be worked out for this question, by far the easiest way to do this is via simulation. This means that the probabilities are approximate, not exact, but with a reasonable number of simulated rolls, the precision should be far greater than anything required to design or enjoy a game. Below is code in Mathematica for simulating dice rolls as you describe then for tabulating the probabilities followed by the results I got with 1,000,000 simulated rolls.

For what it's worth, I may have misunderstood your rolling schema, so if you can verify the accuracy of any of these cells, that would be useful (of course, I could also have made a mistake in the code). If you are a programmer and use some other language like Python, let me know and I'll translate this.

Here is some code that sets up the machinery we will want for simulating:

(* Generate simulated dice rolls. If more than 12 dice are requested,
   the dice past the first 12 are always 6s. *)
Roll[ndice_, nsim_ : 1000000] := If[ndice > 12,
   Join[
    RandomInteger[{1, 6}, {12, nsim}],
    ConstantArray[5, {ndice - 12, nsim}]],
   RandomInteger[{1, 6}, {ndice, nsim}]];
(* Only 1s count toward botches, and only 4/5/6 count against;
   we can sum -1s and 1s (per this key) to see if there are
   enough success rolls to avoid a botch (sum of translated values
   is >= 0) or if there are too many 1s (sum of translated values
   is < 0). *)
onesKey = {1, 0, 0, 0, 0, 0};
(* If there isn't a botch, all that matters for success is whether 
   the values are 1/2/3 or 4/5/6. *)
successKey = {0, 0, 0, 1, 1, 1};
sixesKey = {0, 0, 0, 0, 0, 1};
(* The function that counts up the total number of successes in each
   simulated roll of the dice. Returns totals for {success rolls, ones, sixes}. *)
Score[rolls_] := With[
  {flatrolls = Flatten[rolls],
   nsim = Last@Dimensions[rolls]},
  {Total@Partition[successKey[[flatrolls]], nsim], (*Successes*)
   Total@Partition[onesKey[[flatrolls]], nsim], (*Ones*)
   Total@Partition[sixesKey[[flatrolls]], nsim]}] (*Sixes*)

We can then build up a table of probabilities for each dice-count between 1 and 14 and for each difficulty between 1 and 10:

table = Table[
   With[
    {scores = N@Score@Roll[ndice]},
    With[
     {n = Length[scores[[1]]],
      nSucc = scores[[1]],
      nOnes = scores[[2]],
      nSixes = scores[[3]]},
     Table[
      With[
       {success = Unitize[Sign[nSucc - diff] + 1],
        botch = 1 - Unitize[Sign[nOnes - nSucc] - 1]},
       Table[
        (1/n)*Which[
          lev === None,(*Botches*)
          Total[(1 - success)*botch],
          lev === All,(*Any success*)
          Total[success],
          lev == 4,(*4 or more*)
          Total[success*Unitize[Sign[nSixes - 4] + 1]],
          True,(*A specific level of success ()*)
          Total[success*(1 - Unitize@Sign[nSixes - lev])]],
        {lev, {None, 0, 1, 2, 3, 4, All}}]],
      {diff, 1, 10}]]],
   {ndice, 1, 14}];

Here are the tables; each cell in the table gives the probability of success for the given difficulty (by column) and the given number of dice rolled (by row).

enter image description here

\$\endgroup\$
7
  • \$\begingroup\$ The exact answers are very easy via formulas in this case, just need to figure out how to show the formulas. \$\endgroup\$
    – Alan
    Mar 3, 2023 at 1:34
  • \$\begingroup\$ Yeah, I figured they were. I think SE supports latex format; if you post an answer, I can help you format them. \$\endgroup\$
    – nben
    Mar 3, 2023 at 1:43
  • \$\begingroup\$ I did it in a google sheet with public access, but both for dead-linkage and for explanation, I went ahead and explained the code. Should probably include a picture of it, but I hate doing image editting :). \$\endgroup\$
    – Alan
    Mar 3, 2023 at 2:14
  • \$\begingroup\$ Simulation is a great idea! This is a great contribution. The only thing that jumps out to me atm is the botch rule-- a botch only takes place if an action was failed, therefore should it also empirically vary to some extent based on the difficulty? \$\endgroup\$
    – ThetaHanse
    Mar 3, 2023 at 3:16
  • \$\begingroup\$ Aha! I misunderstood that about the rules (I counted a botch whenever the 1s was greater than the successes). I'll edit the simulation. Anything else you notice? \$\endgroup\$
    – nben
    Mar 3, 2023 at 3:19
3
\$\begingroup\$

For each die there are 4 outcomes: 1, 2-3, 4-5 and 6.

For 12 dice, there are 4^12 = 16777216 outcomes. This is small enough to simply list every outcome.

If we track the log_2 number of pips in each outcome, each 1 and 6 adds 0 and each 2-3 and 4-5 adds 1. So we can perfectly calculate the probability of each outcome with a simple integer.

Categorizing each outcome is cheap; an outcome is either a crit fail, a normal fail, a marginal success, or a degree 1 through 4 success. That is 7 possibilities.

While we can get fancy and memoize this, we don't need to. 2 32 bit integers in each entry and 4^12 entries isn't a large amount of data.

Here is an example:

#include <vector>
#include <array>
#include <iostream>

enum class outcome {
  crit_fail,
  fail,
  marginal,
  degree1,
  degree2,
  degree3,
  degree4,
};

struct pool_result {
  int log2_weight = 0;
  outcome result = outcome::fail;
};

enum class die_result {
  one,
  fail,
  success,
  six,
};

die_result unpack_one(unsigned x) {
  return static_cast<die_result>(x & 3);
}

// 2 bits per die result
std::vector<die_result> unpack(unsigned x, std::size_t count) {
  std::vector<die_result> retval;
  retval.reserve(count);
  for (std::size_t i = 0; i < count; ++i) {
    retval.push_back(unpack_one(x));
    x = x >> 2;
  }
  return retval;
}

pool_result evaluate( std::vector<die_result> const& r, int difficulty, int auto_success )
{
  int ones = 0;
  int failures = 0;
  int successes = 0;
  int sixes = 0;
  for (auto x : r)
  {
    switch (x) {
      case die_result::one: ++ones; ++failures; break;
      case die_result::fail: ++failures; break;
      case die_result::success: ++successes; break;
      case die_result::six: ++successes; ++sixes; break;
    }
  }
  pool_result retval;
  retval.log2_weight = failures - ones + successes - sixes;

  bool success = (successes + auto_success) >= difficulty;

  if (!success && ones > successes)
    retval.result = outcome::crit_fail;
  else if (!success)
    retval.result = outcome::fail;
  else if (sixes == 0)
    retval.result = outcome::marginal;
  else if (sixes == 1)
    retval.result = outcome::degree1;
  else if (sixes == 2)
    retval.result = outcome::degree2;
  else if (sixes == 3)
    retval.result = outcome::degree3;
  else if (sixes >= 4)
    retval.result = outcome::degree4;
  return retval;
}

using table = std::array< pool_result, (1<<24) >;

table solve( int dice, int difficulty) {
  int bonus_dice = (std::max)(dice-12, 0);
  if (dice > 12) dice = 12;
  int table_size = (1 << (dice*2));
  table retval;
  for (int x = 0; x < table_size; ++x)
  {
    retval[x] = evaluate( unpack(x, dice), difficulty, bonus_dice );
  }
  return retval;
}


int main() {
  std::cerr << "Working...\n";
  
  auto result = solve(12, 4);
  int prob[7] = {0};
  for (auto r : result)
  {
    prob[static_cast<int>(r.result)] += (1<<r.log2_weight);
  }

  for (auto p:prob)
    std::cout << (p/double(1<<24)) << "\n";
}

it just naively looks at every possibility. It runs about as fast as you can blink.

The point is your space of problem is small enough that we can use brute force and nothing fancy to solve your problem.

\$\endgroup\$
2
\$\begingroup\$

Here's an Anydice script that implements this logic.

function: roll DICE:s DIFFICULTY:n {
  SUCCESS: DICE >= 4
  if SUCCESS >= DIFFICULTY {
    result: [lowest of (DICE = 6) and 4] \ Success, range of 0-4 \
  }
  if (DICE = 1) > SUCCESS {
    result: -2 \ Critical fail, -2 \
  }
  result: -1 \ Normal fail, -1 \
}

It's fairly simple in operation. You feed the function an Xd6 dice roll cast to a sequence :s and a target difficulty. It will output a value in the range 0-4 if the roll is successful, with the number representing the degree of success; or if the roll fails, it will output -1 for a normal fail or -2 for a critical fail.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .