5
\$\begingroup\$

The game Neon City Overdrive uses the following resolution mechanic for checks:

  1. create a pool of Action Dice and (possibly) another pool of differently-colored Danger Dice (all d6, generally up to 5 or 6 dice in each pool)
  2. roll all the dice
  3. each Danger Die cancels out an Action Die with the same value - both are discarded
  4. the highest remaining Action Die (if there is any) is the result (the precise meaning of which is irrelevant for the purposes of this question)
    • each extra remaining Action Die showing a 6 (i.e. any second, third etc. "6" beyond the first "6" that is read as the result of the roll) provides one critical success (called a boon)

I'm struggling to find the proper way to model the probabilities of this mechanic in anydice.

I realize that a good starting point would be this answer to a very similar question regarding the mechanic in Technoir (which clearly was a source of inspiration for Neon City Overdrive). Unfortunately, despite my best efforts I can't say I fully comprehend how the code provided there works, and there's an important difference between the two games: in Technoir a single "negative die" eliminates all matching "positive dice", whereas in NCO this happens on a one-to-one basis.

I would be very grateful for any help.

\$\endgroup\$
6
\$\begingroup\$

The following anydice function will calculate the expected results for the mechanic you describe:

function: nco AD:s DD:s {
  if [count 6 in AD] > [count 6 in DD] { result: 6 + ([count 6 in AD] - [count 6 in DD] - 1)}
  loop X over {5,4,3,2,1} {
    if [count X in AD] > [count X in DD] { result: X }
  }
  result: 0
}

This function expects to be fed two dice pools (A and D a the start on the link) - the action dice, AD, and the danger dice, DD, which are cast to sequences in the function (:s) so as to fix and inspect them. The key feature is that for any given value X, we can easily determine if there are any uncancelled dice showing X in the action dice pool by counting how many dice showing X there are and comparing it to the number of similar dice in the danger dice pool. So, if [count X in AD] is greater than [count X in DD], we know there are uneliminated dice showing X in the action dice pool. The function iterates down over values of X starting at 6 down to 1 - so the first uneliminated die it finds is the result. (If all the action dice are eliminated, the result is 0.)

I've also added a special case for when the result would be 6 so we can count any possible boons - again simply calculated by comparing the number of 6s in AD to DD and adding 1 to the result for any extra 6s, so a final result of 7 should be read as 6 and 1 boon, for instance.

Unfortunately, though algorithmically simple, I've found that this function is falling afoul of anydice's 5 second calculation limit when given dice pools at the upper end of your specified ranges - the possibility space of sequences for pools of 5d6 or 6d6 is very large, and it seems to give up if both pools are 5d6 or larger. I'm not sure there is a way of optimising this function to the point that Anydice will handle those cases.

| improve this answer | |
\$\endgroup\$
4
\$\begingroup\$

This is a considerably hard analytical problem

The question you linked, although already quite hard, is easier because it can be computed as the probability of a dice being in one set, and not in the other. In your question, this is done in a one-to-one basis, as you mentioned, and this is a considerably hard problem.

Doesn't matter - do it the easy way

Unless you need the analytical expression for something, I recommend going with Monte Carlo: You basically run a random simulation a bunch of times, and then see the distribution of that. This code for MATLAB does it - I am sure someone can translate it to Python or something that doesn't need to be paid, but I'm more familiar with MATLAB haha

N_it = 10^5;

N_action = 6;
N_danger = 5;

bins = 1:6;

for i = 1:N_it

    Action_Dice = randi([1 6], N_action, 1);
    Danger_Dice = randi([1 6], N_danger, 1);

    Action_Dice_Count = hist(Action_Dice, bins);
    Danger_Dice_Count = hist(Danger_Dice, bins);

    Result_Count = Action_Dice_Count - Danger_Dice_Count;

    Result = find(Result_Count > 0, 1, 'last');

    if(Result)
        Result_Save(i) = Result;
    else
        Result_Save(i) = 0;
    end
end

So, what this code does is basically: roll a set number of action dice and danger dice, count how many of each dice you got, subtract the number of rolled danger dice from the number of rolled action dice, then find the last value which is greater than zero (i.e., the highest Action Dice remaining) and finally, if no such action dice exists, it sets the result to 0.

As an example, for 6 Action Dice and 5 Danger Dice, this is what it looks like: enter image description here

Why does it look like this? In the example, I used 6 action dice and 5 danger dice. The probability that there is no remaining action dice is, evidently, zero, since we will always have one remaining dice. But not only that, there is a high probability on 6, why? Because every time there is a 6 remaining, that 6 will be chosen. This is actually an easier analytical problem. Let \$X\$ denote the number of sixes appearing in the Action Dice pool, and \$Y\$ be the number of sixes appearing in the Danger Dice pool. We are interested in \$P(X > Y)\$. But what is nice in this case is that we know the exact distribution of both \$X\$ and \$Y\$, which are binomial distributions with the number of trials being equal to the number of dice in the pool, and probability in each trial equal to 1/6.

We can then compute \$P(X - Y)\$, which can be done by the convolution of \$P(X)\$ and \$P(-Y)\$, and finally compute \$P(X - Y > 0)\$. I won't bother with all this computation here, but if you do it, you will find out that the probability is 0.383367984110654. The probability found by code was 0.386, so, close enough, the code seems to make sense.

Note that this same strategy of analytically computing the other probabilities will not help for other values than 6, since, not only you need that, for example, the number of fives in the Action pool should be larger than the number in the Danger pool, you also need that the number of sixes in the action pool is smaller than or equal to the number in the danger pool. For 5 it is already a bunch of complicated conditional probabilities, and it will get harder and harder for lower numbers, since each time you need to consider the numbers above it.

Computing the Boons

The previous analytical concept can be used to find the probability of Boons. \$B\$ boons happens when \$X = (Y + B + 1)\$, that is, the number of sixes in the Action pool is larger than the number of sixes in the Danger pool by \$B\$. This can actually be computed the same way. So, in this example, we have the probability of one boon being 0.103599655450346, two boons being 0.024868242039980, three boons being 0.003491966042856, and others being irrelevant. Finding a general analytical expression is still hard, but for any given number of action dice and danger dice, it is easy to find the analytical expression.

Validating the Code

I can only think about one trivial case, so we can check whether the code makes sense for that case at least. Consider only 1 Action Dice and 1 Danger Dice. In this case, the probability that they are equal is 1/6, and in that case the result is 0 (i.e., no action dice remaining). Otherwise, we have an equal probability for any dice, which is 5/36. enter image description here

The values resulting from the code are, in fact, approximately 1/6 for 0 and 5/36 for the remaining.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ @Trish Why is that? Since we are choosing the highest remaining dice, it is expected that the higher values have a higher probability. Since, in the example, there are more action dice than danger dice, we won't ever have a non-remaining action dice (thus p(0) = 0). \$\endgroup\$ – HellSaint Jul 4 at 15:41
  • \$\begingroup\$ @Trish I have analytically computed the probability for Result = 6, and it does match the result given by the code. I hope that alone is enough for you to trust the remaining results, because the other values are really hard to compute and it would require lots of time and writing down equations. \$\endgroup\$ – HellSaint Jul 4 at 16:13
1
\$\begingroup\$

I came across this post via a google search for something unrelated, but even though I'm not an RPGer, it's an interesting problem.

This is easily calculated directly for your specified domain using the multinomial distribution.

Taking the weak compositions of size (number of faces n) for (number of dice d), and letting the sequence of categories in the multinomial represent the faces {1, 2, 3..., n}, we subtract over all pairs of weak compositions, with pair members representing the action and danger dice respectively. We note the PMF of each pair member and multiply these for a total probability, saving the result along the the result of the subtraction clipped between [0,d].

It is then a simple walk over the {total probability, clipped result} pairs, picking the largest non-zero category (and if that category is the maximum face value n, noting that the category value less one is the number of boon).

Finally, we gather those results by same maximums&boons, and sum the probabilities within.

Less than a couple of seconds for the 5D6 & 6D6 cases for example, and not a problem for larger cases.

These are read as left numbers top->maximum face value remaining, left numbers bottom->number of boon, followed left-to-right with exact probability and same rounded.

5D6:

enter image description here

6D6:

enter image description here

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.