6
\$\begingroup\$

I've seen a couple of other posts with similar but distinctly different dice canceling mechanics so I figured I would ask for a hand.

The mechanic is this:

Players roll a pool of Xd6 Action dice and Yd6 Stress dice

Stress dice of equal or greater value than action dice will cancel each other out, removing both dice from the pool and should do so beginning with the highest values and continue in descending order. Each stress die may only cancel out a single action die and any remaining stress dice that have a lower value than the lowest action die are discarded. Action dice that remain are used for the player's turn.

ex.

player 1 has a pool of 5 action dice and 3 stress dice.

player 1 rolls 6, 5, 4, 3, 3 action and 5, 4, 2 stress

player 1 uses 5 stress to cancel 5 action and 4 stress to cancel 4 action, all 4 dice are discarded; there are no action dice with value 2 or lower, so the 2 stress is also discarded

player 1 is left with 3 action dice: 6, 3, 3

I’d love for it to end up with a result that told me how many action dice are left and what the values of those dice are as I change the number of action and stress dice in the pool, but really anything that shows this mechanic would be a big help.

Is this something that can be done?

\$\endgroup\$
3
  • \$\begingroup\$ Welcome to RPG.SE, Erbrakaderbra! You might want to take the tour if you haven't already, and check out the help center for more guidance. \$\endgroup\$
    – Jack
    Commented May 22, 2022 at 11:53
  • 1
    \$\begingroup\$ Ok. I’m 3 hours and 6 drinks into an airline lounge so I can give the complete answer. What you need is two sequences passed to a function. Build a third sequence. Work down the action sequence till you get to the highest stress number. Then work down the stress sequence knocking out action dice. Return the third sequence. \$\endgroup\$
    – Dale M
    Commented May 23, 2022 at 3:44
  • \$\begingroup\$ that sounds similar to one of the other forum results i found, and im trying to understand and tweak it for this application but i dont think i really know enough to know if its doing anything useful! haha this is what i am currently working with but it only has 2 sequences and im not sure where to go from here.. it seems to return more or less expected results when dealing with 1d6 vs 1d6. function: actionroll ACTION:s STRESS:s { loop X over {1..#ACTION} { if X@STRESS >= X@ACTION {result: 0} else {result: X@ACTION} } } output [actionroll 1d6 1d6] \$\endgroup\$ Commented May 23, 2022 at 5:08

4 Answers 4

3
\$\begingroup\$

Here's a script using my Icepool Python library. Rather than considering entire sets of rolls at once, Icepool uses the strategy of updating a running total based on how many dice in each pool rolled each number.

import icepool
from icepool import d6, EvalPool

class AllActionStress(EvalPool):
    def next_state(self, state, outcome, action, stress):
        # This is called for each possible outcome 6, 5, 4, 3, 2, 1
        # with the number of action and stress dice that rolled that number.
        # This method uses this information to update a "running total".
        
        # action_set: The set of surviving action dice.
        # leftover_stress: The number of surviving stress dice that rolled 
        # this number or higher.
        action_set, leftover_stress = state or ((), 0)
        leftover_stress += stress
        if leftover_stress >= action:
            leftover_stress -= action
        else:
            action -= leftover_stress
            action_set += (outcome,) * action
            leftover_stress = 0
        
        return action_set, leftover_stress
    
    def final_outcome(self, final_state, *pools):
        # Just return the action set; any remaining stress is ignored.
        return final_state[0]
    
    def direction(self, *_):
        # See outcomes in descending order.
        return -1

all_action_stress = AllActionStress()
result = all_action_stress.eval(d6.pool(5), d6.pool(3))

# The number of surviving action dice.
# If this is all you are interested in, it would be more efficient to only 
# count the number of surviving action dice in AllActionStress, rather than 
# computing the entire surviving action sets and counting the length at the
# end as we do here.
print(result.sub(len))

Denominator: 1679616

Outcome Weight Probability
2 1097586 65.347437%
3 432960 25.777321%
4 132915 7.913416%
5 16155 0.961827%

which matches posita's results.

result contains the distribution of full surviving action sets (a la "player 1 is left with 3 action dice: 6, 3, 3") but the number of possible such sets grows rapidly with number of action dice (e.g. this example results in 252 rows), so I won't reproduce the table here.

You can try the script in your browser here.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This is amazing, thank you so much. I appreciate the notes you wrote as well; it gives all the results i was after and then some! \$\endgroup\$ Commented May 25, 2022 at 2:09
4
\$\begingroup\$

My AnyDice Fu is lacking, so this is a dyce¹-based solution, but I believe is otherwise directly responsive and captures @Dale M's algorithmic sentiments, at least as he was able to best convey them while trapped in his airline lounge. 😋 The slightly tricky bit with AnyDice is that you need two pointers (one for each of your action and stress rolls). In the alternative, you can use a proxy (e.g., popping the max value off your stress roll as you consume it).

from dyce import P
from dyce.p import RollT

def actions_vs_stresses(action_roll: RollT, stress_roll: RollT) -> int:
    actions_not_canceled_by_stresses = []
    # We want to walk through each roll, opportunistically canceling the best action we
    # can given our maximum unspent stress. Rolls are ordered least-to-greatest, so we
    # start at the end and walk backwards, accumulating or canceling actions as we go.
    action_index = len(action_roll) - 1
    stress_index = len(stress_roll) - 1
    while action_index >= 0:
        if stress_index >= 0 and action_roll[action_index] <= stress_roll[stress_index]:
            # We have unspent stress, and our current (max unexamined) action is
            # cancelable by our current (max unspent) stress, so we decrement both
            # counters without counting the action
            action_index -= 1
            stress_index -= 1
        else:
            # Either we're out of stresses, or our current (max unexamined) action is
            # not cancelable (i.e., greater than) our current (max unspent) stress, so
            # we count that action and decrement only the action counter, leaving any
            # unspent stress for the next iteration
            actions_not_canceled_by_stresses.append(action_roll[action_index])
            action_index -= 1
    # Uncomment the following line to see the specific rolls, but this gets
    # overwhelming pretty fast. (We accumulate uncanceled actions in order of greatest-
    # to-least, above. While not strictly necessary, we reverse their order when
    # printing for consistency with roll ordering.)
    #print(f"{action_roll} vs {stress_roll} -> {actions_not_canceled_by_stresses[::-1]}")
    return len(actions_not_canceled_by_stresses)

h = P.foreach(actions_vs_stresses, action_roll=5@P(6), stress_roll=3@P(6))
print(h.format(scaled=True))

Output:

avg |    2.44
std |    0.68
var |    0.46
  2 |  65.35% |##################################################
  3 |  25.78% |###################
  4 |   7.91% |######
  5 |   0.96% |

The above focuses on readability rather than efficiency. There are certainly optimizations to be had, but probably aren't worthwhile until you get into substantially larger pools.

You can play around with a more generalized version in your browser: Try dyce [source]

While a matter of taste, I find anydyce's² "burst" graphs are well-suited to visualizing distributions. (Screenshot for select scenarios below.)


¹ dyce is my Python dice probability library.

² anydyce is my visualization layer for dyce meant as a rough stand-in for AnyDice.


anydyce burst graphs

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Hey that looks like its getting somewhere. Thanks for you efforts! I'll have to dig into this a bit as I've never used Dyce but the burst charts seem to be showing.. remaining action dice, right? 2d6 v 2d6 having a nearly 50% chance to cancel both dice is the kind of information im after! \$\endgroup\$ Commented May 23, 2022 at 15:42
3
\$\begingroup\$

Anydice will not do what you want

You can certainly determine the totals of the remaining dice, but Anydice will not help show you each dice left - it's not designed for that.

function: action A:s stress S:s {
P: {}
M: 1
loop N over {1..#A} {
  if N@A > M@S {
    P: {P, N@A}
  }
  else {
    M: M+1
  }
}
result: P
}

X: 5
Y: 3
output [action Xd6 stress Yd6]

will give the result.

However, this represents the result of \$(5+3)^6=262,144\$ individual (but not distinct) sequences. You cannot tell if, for example, a result of 10 represents 5,5 or 2,3,5 or 2,2,2,2,2 or 6,2,2 or anything else.

\$\endgroup\$
3
  • \$\begingroup\$ For pools sent to s-type parameters, AnyDice appears to only iterate over all sorted sequences rather than all ordered sequences. This is $$\binom{n + k - 1}{k - 1}$$ per pool where \$n\$ is the pool size and \$k\$ is the number of sides per die; in this case we have $$\binom{10}{5} \binom{8}{5} = 14112$$ \$\endgroup\$ Commented May 24, 2022 at 2:18
  • 1
    \$\begingroup\$ Also, instead of concatenating to P (which gets implicitly summed at the end), you can make a quasi-tuple with P: 10 * P + N@A, which puts one roll on each digit. This seems to be the AnyDice-ish way of doing things, given that the @ operator extracts digits when applied to numbers. Of course, this approach starts to break down for multi-digit elements. \$\endgroup\$ Commented May 24, 2022 at 2:26
  • 1
    \$\begingroup\$ This is also very useful, thank you; i like having the graph view on any dice, and i can compare this to what i was using to see where i went wrong! \$\endgroup\$ Commented May 25, 2022 at 2:11
2
\$\begingroup\$

AnyDice can, in fact, do this.

The tricky part, as noted in Dale M's answer, is indicating "how many action dice are left and what the values of those dice are", since AnyDice can only output probability distributions over numbers, not over sequences of numbers. But we can get around this with a helper function that encodes a sequence as the digits of a number:

function: encode SEQUENCE:s in base BASE:n {
  NUMBER: 0
  loop DIGIT over SEQUENCE {
    NUMBER: BASE * NUMBER + DIGIT
  }
  result: NUMBER
}

For example, calling this function as [encode {3,1,4,1,5,9} in base 10] will return the number 314159. Since any d6 roll fits nicely as a single base 10 digit, that works fine for our purposes here. (If you wanted to try using, say, a d12 or a d20, you could use the same encoding function, but with base 100 instead.)

With this encoding issue dealt with, we can then write our actual code. As usual, when implementing any nontrivial dice-counting mechanic in AnyDice, we need to write a function that takes a sequence parameter (or two, in this case) and let AnyDice call it for all possible rolls of a dice pool (or two):

function: ACTION:s action STRESS:s stress {
  \ Loop over the ACTION dice, canceling any with an equal or higher STRESS roll if possible, keep the rest.  \
  \ Note that this algorithm relies on the ACTION and STRESS sequences both being sorted in descending order! \
  KEEP: {}
  INDEX: 1  \ This index points to the first (i.e. highest) stress die not yet used. \
  loop DIE over ACTION {
    if INDEX <= #STRESS & INDEX@STRESS >= DIE {
      INDEX: INDEX + 1  \ Cancel this action die, move index to next stress die. \  
    } else {
      KEEP: {KEEP, DIE} \ Keep this action die, don't move index. \
    }
  }

  \ We cannot return a sequence from a function auto-summed over a die, so we must encode it as a number. \
  result: [encode KEEP in base 10]
}

output [5d6 action 3d6 stress]

This code loops over the action dice in descending numerical order (as AnyDice sorts dice pools by default), checking each one against the highest stress die not yet used. If the stress die is equal to or higher than the action die, we let those two dice cancel out (and update the INDEX variable to point to the next highest stress die); if it's not, we add the uncancelled action die to the KEEP sequence, which we return (encoded as a number) in the end.

(Note that we don't explicitly return the number of remaining dice, since you can just read that from the number of digits in the result.)

FWIW, the output of this code, exported and reformatted as a Markdown table, looks like this:

Remaining dice Probability (%)
11 5.8103161675
21 5.81263812681
22 2.90602137631
31 3.52461515013
32 2.88726708962
33 2.06797267947
41 3.03819444444
42 1.87513098232
43 2.18561861759
44 1.96842611645
51 3.52461515013
52 1.87513098232
53 1.64561423563
54 2.36274243637
55 2.46740921734
61 5.81263812681
62 2.88726708962
63 2.18561861759
64 2.36274243637
65 3.78479366712
66 4.36266384697
222 0.339958657217
322 0.606388603109
332 0.548339620485
333 0.421286770309
422 0.392946959305
432 0.621570644719
433 0.764460448102
442 0.379551040238
443 0.70016003658
444 0.498090039628
522 0.379551040238
532 0.475108596251
533 0.560842478281
542 0.475108596251
543 0.864483310471
544 0.99576331733
552 0.392946959305
553 0.560842478281
554 0.943072702332
555 0.699445587563
622 0.548339620485
632 0.621570644719
633 0.70016003658
642 0.475108596251
643 0.864483310471
644 0.943072702332
652 0.621570644719
653 0.864483310471
654 1.46462048468
655 1.70396090535
662 0.606388603109
663 0.764460448102
664 0.99576331733
665 1.70396090535
666 1.27945911446
2222 0.010121361073
3222 0.0306617703094
3322 0.0428669410151
3332 0.027982586496
3333 0.028756572931
4222 0.0226242188691
4322 0.060728166438
4332 0.060728166438
4333 0.0857338820302
4422 0.0321502057613
4432 0.0571559213535
4433 0.119670210334
4442 0.0217311575979
4443 0.078589391861
4444 0.0503686556927
5222 0.0217311575979
5322 0.0500114311843
5332 0.048225308642
5333 0.0678726566072
5422 0.048225308642
5432 0.0821616369456
5433 0.175040009145
5442 0.0500114311843
5443 0.175040009145
5444 0.158071844993
5522 0.0321502057613
5532 0.048225308642
5533 0.0982367398262
5542 0.048225308642
5543 0.167895518976
5544 0.225051440329
5552 0.0226242188691
5553 0.0678726566072
5554 0.150034293553
5555 0.0854957323579
6222 0.027982586496
6322 0.060728166438
6332 0.0571559213535
6333 0.078589391861
6422 0.048225308642
6432 0.0821616369456
6433 0.175040009145
6442 0.048225308642
6443 0.167895518976
6444 0.150034293553
6522 0.0571559213535
6532 0.0821616369456
6533 0.167895518976
6542 0.0821616369456
6543 0.278635116598
6544 0.385802469136
6552 0.060728166438
6553 0.175040009145
6554 0.385802469136
6555 0.304831580552
6622 0.0428669410151
6632 0.060728166438
6633 0.119670210334
6642 0.0500114311843
6643 0.175040009145
6644 0.225051440329
6652 0.060728166438
6653 0.175040009145
6654 0.385802469136
6655 0.450102880658
6662 0.0306617703094
6663 0.0857338820302
6664 0.158071844993
6665 0.304831580552
6666 0.160751028807
22222 0.0000595374180765
32222 0.000297687090383
33222 0.000595374180765
33322 0.000595374180765
33332 0.000297687090383
33333 0.000476299344612
42222 0.000297687090383
43222 0.00119074836153
43322 0.0017861225423
43332 0.00119074836153
43333 0.00238149672306
44222 0.000595374180765
44322 0.0017861225423
44332 0.0017861225423
44333 0.00476299344612
44422 0.000595374180765
44432 0.00119074836153
44433 0.00476299344612
44442 0.000297687090383
44443 0.00238149672306
44444 0.00160751028807
52222 0.000297687090383
53222 0.00119074836153
53322 0.0017861225423
53332 0.00119074836153
53333 0.00238149672306
54222 0.00119074836153
54322 0.00357224508459
54332 0.00357224508459
54333 0.00952598689224
54422 0.0017861225423
54432 0.00357224508459
54433 0.0142889803384
54442 0.00119074836153
54443 0.00952598689224
54444 0.00803755144033
55222 0.000595374180765
55322 0.0017861225423
55332 0.0017861225423
55333 0.00476299344612
55422 0.0017861225423
55432 0.00357224508459
55433 0.0142889803384
55442 0.0017861225423
55443 0.0142889803384
55444 0.0160751028807
55522 0.000595374180765
55532 0.00119074836153
55533 0.00476299344612
55542 0.00119074836153
55543 0.00952598689224
55544 0.0160751028807
55552 0.000297687090383
55553 0.00238149672306
55554 0.00803755144033
55555 0.0038103947569
62222 0.000297687090383
63222 0.00119074836153
63322 0.0017861225423
63332 0.00119074836153
63333 0.00238149672306
64222 0.00119074836153
64322 0.00357224508459
64332 0.00357224508459
64333 0.00952598689224
64422 0.0017861225423
64432 0.00357224508459
64433 0.0142889803384
64442 0.00119074836153
64443 0.00952598689224
64444 0.00803755144033
65222 0.00119074836153
65322 0.00357224508459
65332 0.00357224508459
65333 0.00952598689224
65422 0.00357224508459
65432 0.00714449016918
65433 0.0285779606767
65442 0.00357224508459
65443 0.0285779606767
65444 0.0321502057613
65522 0.0017861225423
65532 0.00357224508459
65533 0.0142889803384
65542 0.00357224508459
65543 0.0285779606767
65544 0.048225308642
65552 0.00119074836153
65553 0.00952598689224
65554 0.0321502057613
65555 0.0190519737845
66222 0.000595374180765
66322 0.0017861225423
66332 0.0017861225423
66333 0.00476299344612
66422 0.0017861225423
66432 0.00357224508459
66433 0.0142889803384
66442 0.0017861225423
66443 0.0142889803384
66444 0.0160751028807
66522 0.0017861225423
66532 0.00357224508459
66533 0.0142889803384
66542 0.00357224508459
66543 0.0285779606767
66544 0.048225308642
66552 0.0017861225423
66553 0.0142889803384
66554 0.048225308642
66555 0.038103947569
66622 0.000595374180765
66632 0.00119074836153
66633 0.00476299344612
66642 0.00119074836153
66643 0.00952598689224
66644 0.0160751028807
66652 0.00119074836153
66653 0.00952598689224
66654 0.0321502057613
66655 0.038103947569
66662 0.000297687090383
66663 0.00238149672306
66664 0.00803755144033
66665 0.0190519737845
66666 0.00744217725956

Of course, if you're only interested in some specific property of the results (like the number of kept dice, or the value of the highest kept die), you don't have to go through this whole table adding up the probabilities. Instead, just modify the AnyDice code to return that property directly (e.g. with result: #KEEP or result: 1@KEEP) and re-run it.

\$\endgroup\$

You must log in to answer this question.

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