5
\$\begingroup\$

I trying to calculate probabilities for this dice mechanic, but I'm not sure that anydice can calculate that at all. The dice pool mechanic works like this:

Players assemble a dice pool with green and red dice. Any dice can be d4, d6, d8, d10 or d12. Green dice represent player skill and advantages, red dice - difficulty and disadvantages. The minimum number of dice in every dice pool is 4: two green and two red. There is no maximum number of dice in a pool, but for sanity sake it will be probably capped at 6 green and 6 red dice. A player rolls a dice pool and takes the 2 highest dice from it. After that he is looking at their color. If they are both red it's fail, if they both green it's full success, if one green and one red then it's success with complication. If green and red dice after a roll have the same number, they are removed from the result. If after all removals there is only red dice, then it's fail. If after all removals there is only green dice, then it's full success. If after all removals there is no dice left, then it's success with complication.

I want to know probabilities of full success, success with complication and fail in that dice pool mechanic. Is that even possible with anydice? And if not, what kind of instrument or math approximation I can use?

Here the same dice pool mechanic with examples:

  1. Player has 3 green dice (2d4,1d8) and 2 red dice (1d4,1d6). He rolls the dice pool and get results: 1 (r), 1 (r), 3 (g), 5 (g), 7 (g). He takes 2 highest dice - 5 and 7. They are both green so the roll is full success.
  2. Player has 3 green dice (1d8,2d10) and 3 red dice (1d4,1d6,1d12). He rolls the dice pool and get results: 3 (r), 4 (r), 10 (r), 3 (g), 4 (g), 10 (g). All dices cancel each other out so the roll is success with complication.
\$\endgroup\$
3
  • \$\begingroup\$ Welcome to RPG.SE! Take the tour if you haven't already, and check out the help center for more guidance. \$\endgroup\$
    – V2Blast
    Commented May 23, 2022 at 17:36
  • \$\begingroup\$ If there are two green of the same number as a red result are all three dice removed? Or is it more of a one-to-one elimination? \$\endgroup\$
    – Someone_Evil
    Commented May 23, 2022 at 17:41
  • \$\begingroup\$ It is one-to-one elimination @Someone_Evil \$\endgroup\$
    – Awertum
    Commented May 23, 2022 at 20:22

2 Answers 2

3
\$\begingroup\$

Efficient computation

The trouble with AnyDice is that it works by enumerating all possible multisets of numbers rolled. While this is faster than enumerating all ordered (equivalently, unsorted) rolls and is convenient in that it gives you the entire rolled sequence at once, the number of possible multisets can still grow quickly as you add dice. Mixing different dice also makes things much more difficult.

Fortunately, it turns out that, as long as you can express the dice mechanic in terms of an incremental calculation where you're told how many dice rolled each number in each pool for one number at a time without carrying too much information from number to number, it's possible to compute the solution more efficiently. It's also possible to handle mixed standard dice without much loss in efficiency. I've implemented this approach in my Icepool Python package. Here's a script:

import icepool
from icepool import d4, d6, d8, d10, d12, EvalPool

class GreenRed(EvalPool):
    def next_state(self, state, outcome, green, red):
        # State is the number of top-two dice for green and red.
        top_green, top_red = state or (0, 0)
        # If there are remaining places in the top two...
        remaining_top_two = 2 - (top_green + top_red)
        if remaining_top_two > 0:
            # Compute the number of non-eliminated dice that rolled this outcome.
            net = green - red
            # Then add them to the winning team's top two.
            if net > 0:
                top_green += min(net, remaining_top_two)
            elif net < 0:
                top_red += min(-net, remaining_top_two)
        return top_green, top_red
    
    def final_outcome(self, final_state, *pools):
        top_green, top_red = final_state
        if (top_green > 0) and not (top_red > 0): return 2
        elif (top_red > 0) and not (top_green > 0): return 0
        else: return 1
    
    def direction(self, *_):
        # See outcomes in descending order.
        return -1
    
green_red = GreenRed()
# The argument lists are implicitly cast to pools.
print(green_red.eval([d10, d8], [d6, d8]))

Denominator: 3840

Outcome Weight Probability
0 265 6.901042%
1 2784 72.500000%
2 791 20.598958%

We can handle even much larger pools. Here's two entire dice sets (10 dice total) on each side:

print(green_red.eval([d12, d10, d8, d6, d4, d12, d10, d8, d6, d4], [d12, d10, d8, d6, d4, d12, d10, d8, d6, d4]))

Denominator: 281792804290560000

Outcome Weight Probability
0 67701912081930556 24.025423%
1 146388980126698888 51.949155%
2 67701912081930556 24.025423%

You can try this in your browser using this JupyterLite notebook. Fair warning, though, I'm currently doing a major revision to the package.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you for putting code to Jupyter notebook, so I can try this out! This solution is exactly what I need to test out this dice pool mechanic. Also I what to mention that your articles about dice pools and Cortex Prime dice pool calculator was an inspiration for this mechanic :) Thanks again! \$\endgroup\$
    – Awertum
    Commented May 24, 2022 at 7:20
  • 1
    \$\begingroup\$ Glad to help! For others of you reading, here's a shameless plug: that Cortex Prime calculator; a comparison of several ternary-outcome mechanics. \$\endgroup\$ Commented May 24, 2022 at 7:29
5
\$\begingroup\$

Anydice can almost do this

There are three main parts to doing this in Anydice (or any system really): using a mixed dice pool, testing for two highest, and eliminating pairs of dice.

Using mixed dice isn't too hard, we just have to cast each type as one sequence and then combine those. Basically, our function have to take multiple A:s parameters which we then make into one pool as GREEN: [sort {A, B}]. That means the number of different possible dice will be part of the function definition, but it's fairly easy to expand and quickly turns into a non-issue.

To test the success/failure conditions, we can recognize that the highest two being green is equivalent to the 2nd highest green being greater than the highest red. And vice versa. If neither of those are true, then the highest two must be of mixed colours.

The difficult step with Anydice is dealing with eliminations. I can't find a way around it which works with your success/failure condition, but it is doable. The trick is to build new sequences with the surviving dice, over manipulating the existing sequences. The big issue we're running into here is computation time. Anydice is limited to 5 seconds, and we rapidly hit that with too large pools (depends on number and size of dice). To help cut computation down, we can test success/failures as often as possible, but even then it struggles around 5-6 dice.

In the following implementation, failure is 0, complication 1 and success 2.

function: elimpool A:s B:s vs C:s D:s
{
  INITGREEN: [sort {A, B}]
  INITRED: [sort {C, D}]

  if 2@INITGREEN > 1@INITRED {result:2}
  if 2@INITRED > 1@INITGREEN {result:0}

  GREEN: {}
  RED: {}

  loop X over [reverse {1..[highest of 1@INITGREEN and 1@INITRED]}]
  { 
     DIFF: [count X in INITGREEN]-[count X in INITRED]
     GREEN: {GREEN, X:DIFF}
     RED: {RED, X:-DIFF}
     if 2@GREEN > 1@RED {result:2}
     if 2@RED > 1@GREEN {result:0}
  }
  GREEN: [sort GREEN]
  RED: [sort RED]
 
  if #RED = 0 & #GREEN = 0 {result: 1}
  if #RED = 0 {result: 2}
  if #GREEN = 0 {result: 0}

  if 2@GREEN > 1@RED {result:2}
  if 2@RED > 1@GREEN {result:0}
  result: 1
}

output [elimpool 1d10 1d8 vs 1d6 1d8]

Monte-Carlo

If you're willing to sacrifice finding an analytical solution, running Monte-Carlo simulations is fairly straight forward (assuming you can do some basic coding). The basic premise is to write a function which actually does all the rolling and manipulation that a player would, then run that enough times to get a good approximation.

In principle we need those same three parts, but we get a fair bit more freedom since we're now using a fully fledged language. By preference I like giving mixed pools to be rolled as a list and then calling the roll function for each element, the success logic is exactly the same (if we set the same sort order), and if we're careful with the logic we can remove elements from lists (assuming our language supports that).

Anyway, you can use my Python code either as is or as inspiration. It produces same (or close enough given the inherent variance) results as Anydice, but it'll deal with as large dice pools as you're willing to give it CPU time. And if you want higher accuracy, just increase the count parameter at the cost of same.

import random

def d(N):
    return random.randint(1, N)

def RollPool(greenDice, redDice):
    
    greenRoll = list(map(d, greenDice))
    greenRoll.sort(reverse=True)
    redRoll = list(map(d, redDice))
    redRoll.sort(reverse=True)
    
    if greenRoll[1] > redRoll[0]: return 2
    if redRoll[1] > greenRoll[0]: return 0
    
    i = 0
    while i < len(greenRoll) and i >= 0:
        j = 0
        while j < len(redRoll) and j >= 0:
            if greenRoll[i] == redRoll[j]:
                greenRoll.pop(i)
                redRoll.pop(j)
                i -= 1 
                j = -2
            else:
                j+=1
        i+=1
    
    if len(greenRoll) == 0 and len(redRoll) == 0:
        return 1
    if len(redRoll) == 0:
        return 2
    if len(greenRoll) == 0:
        return 0
    
    if len(redRoll) >= 2:
        if redRoll[1] > greenRoll[0]: return 0
    if len(greenRoll) >= 2:
        if greenRoll[1] > redRoll[0]: return 2
    return 1

def runMC(greenDice, redDice, count):
    data = [-1]*count
    for i in range(count):
        data[i] = RollPool(greenDice, redDice)
    
    return data.count(0)/count, data.count(1)/count, data.count(2)/count

print(runMC([10, 8], [6, 8], 100000))
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your detailed answer! The code example for anydice working great with small dice pools and helped me to understand how to solve this problem mathematically. \$\endgroup\$
    – Awertum
    Commented May 24, 2022 at 7:16

You must log in to answer this question.

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