3
\$\begingroup\$

I am working on a dice pool system for skills and actions to be used in a new system.

In this system, any rolls of 1 will negatively affect the outcome of resolving an action using a dice pool.

In this instance, I am trying to model the probability of each outcome when rolling 2d4 with 1d4 disadvantage/adversity by looking at the total of the 2 highest dice rolled (which in this case is the entire 2d4) after disadvantage is calculated.

The way disadvantage is intended to work is:

  • Any disadvantage dice that roll a 1 are added to the pool
  • Any disadvantage dice that roll above 1 will remove the highest die in the pool of equal or lesser value from the pool if that die in question is greater than 1 (the disadvantage die itself is not added)

Edit: Some sample sequences here, main dice sequence listed first then disadvantage die

  • 23, 1 -> 23 with extra 1 added, total of first 2 dice is 5
  • 23, 4 -> 4 knocks out 3, leaving only 2
  • 12, 2 -> 2 knocks out 2, leaving only 1
  • 44, 4 -> 4 knocks out one 4, leaving 4
  • 24, 1 -> 24 with extra 1 added, total of first 2 dice is 6

Since it is not possible to remove elements from sequences, I am trying to build up the sequence DISADVANTAGEPROCESSED while excluding dice that would be removed from the pool so the function can then return the total of the requested number of dice specified in the parameter POSTOOUTPUT calculated from this sequence.

I have worked on code similar to the below for several days and while I have advantage (and passing the dice sequence straight through with no advantage or disadvantage) working, I am still not getting what I believe are the correct results that I have done by manually mapping out the 64 possible outcomes.

The mechanics of adding disadvantage dice that roll 1 to the pool seem to be working, as I have determined by running the code with POSSTOOUTPUT set to {3}.

I have a feeling that I am missing something obvious and will still keep working on it, but any insight or guidance that can be offered would be much appreciated.

LOWMAX: 1
DICESEQ: 2d4
ADVANTAGESEQ: {}
DISADVANTAGESEQ: 1d4
POSTOOUTPUT: {1..2}

function: POSITION:s DICEPARAM:s with ADVANTAGEPARAM:s advantage and DISADVANTAGEPARAM:s disadvantage
{
 ADVANTAGE: 0
 ADVANTAGEPROCESSED: {}
 DISADVANTAGEPROCESSED: {}

\ Other code omitted due to irrelevance \

 if DISADVANTAGEPARAM != {} & ADVANTAGEPARAM = {} \If there is only disadvantage \
 {
  
  loop Y over {1..#DISADVANTAGEPARAM}  \ Have also tried loop Y over DISADVANTAGEPARAM and not used the indexing \
  {
    REMOVED: 0 \ This flag is to be set when a die has been removed from the pool for the current disadvantage die in the sequence \
   loop X over {1..#DICEPARAM} \ Have also tried loop X over DICEPARAM, no different \
   {
  \ Have also tried without the whole loops and just gone with code like if DICEPARAM > DISADVANTAGEPARAM hoping that the mechanics of passing dice collections to the function as parameters that expect sequences will do the job, still no luck \
    if X@DICEPARAM>Y@DISADVANTAGEPARAM | X@DICEPARAM <=LOWMAX
    {
     DISADVANTAGEPROCESSED: {DISADVANTAGEPROCESSED, X@DICEPARAM}
    }
    
    if X@DICEPARAM <= Y@DISADVANTAGEPARAM
    {
     if !REMOVED
     {
      REMOVED: 1
     }
     else
     {
      DISADVANTAGEPROCESSED: {DISADVANTAGEPROCESSED, X@DICEPARAM}
     }
    }

    if Y@DISADVANTAGEPARAM <= LOWMAX
    {
     DISADVANTAGEPROCESSED: {DISADVANTAGEPROCESSED, Y@DISADVANTAGEPARAM}
    }
 
   }
  }

  COMBINED: [sort {DISADVANTAGEPROCESSED}]
  result: POSITION@COMBINED

  }

}

output [POSTOOUTPUT DICESEQ with ADVANTAGESEQ advantage and DISADVANTAGESEQ disadvantage] named "[DICESEQ] with [ADVANTAGESEQ] advantage and [DISADVANTAGESEQ] disadvantage"
```
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Walking through some example scenarios would help make clearer what exactly you are trying to model. My understanding, with the disadvantage die always first, is: 1,1,1 --> 2. 1,3,4 --> 7. 2,2,3 --> 3. 2,3,3 --> 6. 3,2,3 --> 2. 4, 4, 1 --> 1... \$\endgroup\$ – Medix2 Jan 12 at 5:46
  • 1
    \$\begingroup\$ Do I understand correctly that (disadvantage die listed first) 2,1,1 --> 2 (because disadvantage dice cannot remove rolls of 1). And 2,1,2 --> 1 (because the disadvantage die removed the 2)? \$\endgroup\$ – Medix2 Jan 12 at 6:15
  • 2
    \$\begingroup\$ Thanks for responding. Your understanding as stated in both your replies is exactly correct. \$\endgroup\$ – Loopyob Jan 12 at 9:10
4
\$\begingroup\$

I agree, AnyDice's built-in sequence manipulation kind of sucks. You'd think that something like {1,3,5}@ROLL would return a three-element sequence containing the first, third and fifth values in ROLL, but no — it actually returns the sum of those values.

However, we can still write a function to do what we want:

function: select INDICES:s from SEQUENCE:s {
  RESULT: {}
  loop INDEX over INDICES {
    RESULT: {RESULT, INDEX@SEQUENCE}
  }
  result: RESULT
}

Now, [select {1,3,5} from ROLL] will in fact do what we wanted {1,3,5}@ROLL to do, at least as long as ROLL is a sequence.

(If it's a pool of dice, the function will still technically work, but the results will get automatically summed back into a number before being collected into a die, so it just ends up doing the same thing as {1,3,5}@ROLL but slower.)

Using this handy helper function, we can then write a function to implement your knock-out mechanic:

function: ROLL:s drop highest up to LIMIT:n unless EXCLUDE:s {
  \ assumes ROLL is sorted in descending order! \
  OFFSET: 1 + (ROLL > LIMIT) 
  if OFFSET > #ROLL | OFFSET@ROLL = EXCLUDE { result: ROLL }
  result: [select {1 .. OFFSET-1, OFFSET+1 .. #ROLL} from ROLL]
}

Note the trick here: by assuming the sequence of rolled values to be sorted in descending order (as AnyDice sorts dice pools by default), we can find the position of the highest value below or equal to the limit in the sequence simply by counting the number of values in the sequence that are higher than the limit and adding one.

Now we're ready to implement your full mechanic as a function:

function: roll ROLL:s disadvantage DIS:s {
  POOL: [sort ROLL]
  loop D over DIS {
    if D > 1 { POOL: [POOL drop highest up to D unless 1] }
  }
  result: {1,2}@POOL
}

Here's the whole program put together, along with some test cases.

Note that I'm redundantly sorting the input in the function above, just in case. That way, the code won't behave weirdly if you accidentally call the function with incorrectly sorted manual test input. In particular, I ran into that issue myself when testing the code with your sample cases, and decided to add the sort to make it less error-prone:

output [roll {2,3} disadvantage 1] named "2,3 with extra 1 added, total of first 2 dice is 5"
output [roll {2,3} disadvantage 4] named "4 knocks out 3, leaving only 2"
output [roll {1,2} disadvantage 2] named "2 knocks out 2, leaving only 1"
output [roll {4,4} disadvantage 4] named "4 knocks out one 4, leaving 4"
output [roll {2,4} disadvantage 1] named "2,4 with extra 1 added, total of first 2 dice is 6"

Screenshot of AnyDice test output

And here's the actual distribution of results with 2–4 normal dice and one disadvantage die:

Screenshot of AnyDice output

(The code linked above also calculates the results for 0 and 2 disadvantage dice, but if you graph them all at once, the result is just a mess of intersecting lines.)


Ps. From your question, I'm not 100% sure if you want to count ones as part of the sum if they occur among the top two dice left after the knock-out mechanic has been applied. If not, you can change the last line of the main function to e.g.:

POOL: [select {1,2} from POOL]
result: POOL - (POOL = 1)

This works by first truncating the sequence POOL to its first two elements, and then summing it and subtracting the number of ones. Alternatively, we could write another helper function to explicitly remove the ones from the sequence before summing it, e.g. like this:

function: remove EXCLUDE:s from SEQUENCE:s {
  RESULT: {}
  loop VALUE over SEQUENCE {
    if VALUE = EXCLUDE { RESULT: {RESULT, VALUE} }
  }
  result: RESULT
}

and use it like this in the main function:

result: {1,2}@[remove 1 from POOL]

You might also be interested in the distribution of the number of ones rolled for different dice pool sizes, to estimate the risk of the "negative effects" they'll trigger. The tricky part here is that, of course, the number of ones rolled will be strongly linked with the sum of the top two rolls, and in a rather complicated way.

One way to visualize this combined distribution would be to plot the conditional distribution of the number of ones, given that the sum of the top two rolls is \$N\$, for each possible sum \$N\$, e.g. like this:

function: count ones in ROLL:s disadvantage DIS:s if sum is TARGET:n {
  POOL: [sort ROLL]
  loop D over DIS {
    if D > 1 { POOL: [POOL drop highest up to D unless 1] }
  }
  SUM: {1,2}@POOL
  if SUM != TARGET { result: d{} } \ wrong sum, skip this result! \
  result: (ROLL = 1) + (DIS = 1)   \ else return total number of ones \
}

Here's a plot of the average number of ones for each possible result with two normal dice and one disadvantage die, illustrating how complicated the distribution can get:

Screenshot of AnyDice summary output

Alternatively, you could try encoding both the sum and the count of ones into a single result number, e.g. like this:

function: roll ROLL:s disadvantage DIS:s {
  POOL: [sort ROLL]
  loop D over DIS {
    if D > 1 { POOL: [POOL drop highest up to D unless 1] }
  }
  SUM: {1,2}@POOL
  ONES: (ROLL = 1) + (DIS = 1)
  result: 10*SUM + ONES
}

The line result: 10*SUM + ONES effectively appends the count of ones as an extra digit after the sum, so that e.g. a result of 42 means "top two dice sum to 4 after knock-out, with 2 ones rolled in total".

Alas, the resulting output, while highly information-rich, looks like a mess when plotted (since it's basically squashing a two-dimensional plot into one dimension). You could always export the output e.g. into a spreadsheet and plot it better there, though.

\$\endgroup\$
4
  • \$\begingroup\$ @Medix2: I assumed the OP wanted to know that, for the unspecified "negative effects" they're supposed to have. The mechanic doesn't IMO really make much sense otherwise. (Of course, if you only want to see the sum of the top two dice, that's easy enough to do by changing the last line of the main function to just result: SUM.) \$\endgroup\$ – Ilmari Karonen Jan 12 at 15:33
  • 1
    \$\begingroup\$ @Medix2: With that change, plus adding the "never knock out ones" rule and using d4 instead of d6 (my bad for not reading the question carefully!), my results match your manual calculation. \$\endgroup\$ – Ilmari Karonen Jan 12 at 17:33
  • \$\begingroup\$ My main concern was to get the main logic working, which thanks to you, is now doing so. Absolutely fantastic work. The 1s can indeed remain in the sum total. They are involved in a crit fail / crit success mechanic for which I have so far been able to develop a satisfactory model. \$\endgroup\$ – Loopyob Jan 13 at 5:50
  • \$\begingroup\$ The only thing I have added to this function is the else condition to add 1s rolled to the pool. I think this is working as intended. function: roll ROLL:s disadvantage DIS:s { POOL: [sort ROLL] loop D over DIS { if D > 1 { POOL: [POOL drop highest up to D unless 1] } else {POOL: {POOL, D}} } result: {1,2}@POOL } \$\endgroup\$ – Loopyob Jan 13 at 6:24

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.