# How to calculate probabilities for a three pool elimination mechanic?

I've been struggling to get my head around this dice pool mechanic either in anydice or even a bespoke program.

There are 3 pools of d6, call them A, B & C:

A: ad6
B: bd6
C: cd6


Any dice in pools A and B that match any dice in pool C are removed from their respective pool.

The score is the highest non-eliminated dice in pool A or B (+0.1 if more than 1) and we need to know the total number of dice (not their values) of non-eliminated dice in pool B.

For example:

A: 3d6
B: 2d6
C: 4d6

Example 1
A: {6,2,6}  -> {6,2,6}
B: {2,1}    -> {2,1}
C: {3,3,5,4}
Result: 6.1,2

Example 2
A: {5,1,6}  -> {5,x,x}
B: {1,4}    -> {x,x}
C: {1,6,6,4}
Result: 5,0

Example 3
A: {3,2,6}  -> {3,x,6}
B: {5,4}    -> {5,x}
C: {4,1,4,2}
Result: 6,1

Example 4
A: {6,5,5}  -> {x,5,5}
B: {1,4}    -> {x,4}
C: {6,3,1,3}
Result: 5.1,1


The mechanic is from Technoir.

How do I model this?

It's not (yet) exactly what you're asking for, but here's an AnyDice program that returns the highest roll in a dice pool, excluding any dice rolled in another pool:

function: highest of DICE:d excluding EXCL:s {
NEW: {}
loop I over {1@DICE} {
if I = EXCL { I: 0 }
NEW: {NEW, I}
}
POOL: (#DICE) d NEW
result: 1@POOL
}
output [highest of 3d6 excluding 4d6]


It uses some pretty tricky AnyDice syntax, so let me explain a few of the details:

• When you pass a die to an AnyDice function expecting a sequence (like EXCL:s above), AnyDice will run the function for every possible roll of the dice, and sum the results (weighted by the probability of each roll). This is basically a brute force approach, and it can easily time out for large dice pools, but it's very versatile when it works. This is how I'm handling the exclusion mechanic here.

• #DICE gives the number of dice in the pool DICE, and {1@DICE} gives the possible values of a single die in the pool. That is, if DICE = 3d6, then #DICE = 3 and {1@DICE} = {1, 2, 3, 4, 5, 6}.

• When you compare a number to a sequence (as in I = EXCL), the result will be true (non-zero) if the comparison is true for any element of the sequence. That is, if I = EXCL basically checks whether I belongs to the sequence EXCL.

So what the loop inside the function does is construct a custom die NEW which looks just like one of the dice in DICE, but with any sides matching EXCL replaced by 0. Then POOL: (#DICE) d NEW creates a new custom dice pool consisting of as many of those custom dice as there were in the original pool DICE, and the final line returns the highest result in that pool.

Getting a bit closer, here's a version that adds the bonus for multiple equal highest dice:

function: highest of ROLL:s with bonus {
MAX: 1@ROLL
BONUS: MAX > 0 & (MAX = ROLL) > 1
result: 10 * MAX + BONUS
}
function: highest of DICE:d excluding EXCL:s {
NEW: {}
loop I over {1@DICE} {
if I = EXCL { I: 0 }
NEW: {NEW, I}
}
POOL: (#DICE) d NEW
result: [highest of POOL with bonus]
}
output [highest of 3d6 excluding 4d6]


Since AnyDice doesn't support fractional numbers, I'm multiplying the result of the roll by 10, so that, say, a result of 5.1 (= 5 + 0.1 bonus) is represented as 51.

The code above is exactly the same as the first program above, except that I've replace 1@POOL with a call to a helper function that adds the bonus if the highest roll occurs multiple times (and is not zero). The helper function uses the same trick of taking the roll in as a sequence with :s, so that it gets automatically iterated over every possible outcome of the roll.

Finally, here's a program that calculates the number of non-excluded dice in pool B:

function: number of DICE:d excluding EXCL:s {
NEW: {}
loop I over {1@DICE} {
NEW: {NEW, !(I = EXCL)}
}
result: (#DICE) d NEW
}
output [number of 2d6 excluding 4d6]


It's the same as the first program, except that now we're labeling the sides of our custom dice with 0 if they're in the exclusion list, and 1 otherwise, and then taking the sum of the roll as the result.

The remaining problem is to combine these programs, to account for the fact that both pools A and B depend on pool C simultaneously. The problem is that AnyDice doesn't really handle two-dimensional output well, but we can use the same trick as we used to handle the 0.1 bonus above, multiplying the result of pool A further by 10 before adding the result of pool B, like this:

function: highest of ROLL:s with bonus {
MAX: 1@ROLL
BONUS: MAX > 0 & (MAX = ROLL) > 1
result: 10 * MAX + BONUS
}
function: highest of A:d and count of B:d excluding EXCL:s {
ANEW: {}
loop I over {1@A} {
if I = EXCL { I: 0 }
ANEW: {ANEW, I}
}
BNEW: {}
loop I over {1@B} {
BNEW: {BNEW, !(I = EXCL)}
}
result: 10 * [highest of (#A) d ANEW with bonus] + (#B) d BNEW
}
output [highest of 3d6 and count of 2d6 excluding 4d6] named "A.A, B"


Here, the output consists of a three-digit number PQR, where P is the highest non-excluded roll in pool A, Q is 1 if the highest non-excluded roll in pool A is multiple, and R is the number of non-excluded rolls in pool B.

Oh, wait! You said you wanted the highest roll in pools A and B together, not just the highest roll in A? Well, I can do that, too, although it does require a little bit of reorganization, since the highest roll is no longer independent of the number of non-excluded rolls in pool B:

function: highest of A:s and B:s with bonus and count {
MAX: [highest of 1@A and 1@B]
BONUS: MAX > 0 & (MAX = A) + (MAX = B) > 1
COUNT: B > 0
result: 100 * MAX + 10 * BONUS + COUNT
}
function: SEQ:s excluding EXCL:s {
NEW: {}
loop I over SEQ {
if I = EXCL { I: 0 }
NEW: {NEW, I}
}
result: NEW
}
function: highest of A:d and B:d with bonus and count excluding EXCL:s {
AX: (#A) d [{1@A} excluding EXCL]
BX: (#B) d [{1@B} excluding EXCL]
result: [highest of AX and BX with bonus and count]
}
output [highest of 3d6 and 2d6 with bonus and count excluding 4d6]


Anydice doesn't actually roll any dice, so you can't create the pools (unless I am wrong) but once you have the pools this may be what you need.

You will need to enter manually A,B and C (on the top) and then calculate, select "Roller" and roll the two outputs.

PS : there is not 0.1 in Anydice so if the number is present twice it add 100 instead.

//the pools for the results of your dices
A:{6,5,5}
B:{1,4}
C:{6,3,1,3}

//a simple function returning the greater of 2 numbers
function: maxi A and B {
if A>B {
result:A
}
else {
result:B
}
}

//creation of the two maximums initialized at -1 to be sure it doesn't overlap
//(0 is fine too)
MAXA:-1
MAXB:-1

//counters, number_of_time_maximum_seen -1
TWICEA:0
TWICEB:0

//#A stand for the size of A
//the first loop, go from 1 to the size of A (first pool)
loop P over {1..#A}
{
//P@A stand for the P-th value of A
if !(P@A = C) {
//if the P-th value of A is inside C (last pool), check if this maximum has been seen or not and take the appropriate action
if MAXA = P@A {
TWICEA : 1 + TWICEA
}
else {
MAXA : [maxi MAXA and P@A]
TWICEA: 0
}
}
}

//the number of dice in B not in C
COUNT:#B
loop P over {1..#B}
{
if (P@B = C){
//if the P-th value of B is in C the count decrease by 1
COUNT : COUNT - 1
}
//if the P-th value of B isn't in C the same thing as A (see A loop)
else {
if MAXB = P@B {
TWICEB: 1 + TWICEB
}
else {
MAXB : [maxi MAXB and P@B]
TWICEB: 0
}
}

//if the two maximum are equals the highest is the one seen the more
if (MAXA = MAXB) {
output MAXA + 100 * ([maxi TWICEA and TWICEB]!=0)
}
//if the maximums are not equals the greater win
else if (MAXA >MAXB) {
output MAXA+100*(TWICEA!=0) named "highest non-eliminated dice"
}
else {
output MAXB+100*(TWICEB!=0) named "highest non-eliminated dice"
}
}
output COUNT named "non-eliminated dice count in pool B".


## Faster solution than AnyDice

Unfortunately, even with pre-filtering the outcomes before computing the Action (A) and Push (B) pools, AnyDice times out if an additional die is added to either of these pools. Here's a JupyterLite notebook based on my Icepool Python package that can compute the solution more quickly. You can edit the code and run it right in your browser.

import icepool
from icepool import d6

# An MultisetEvaluator incrementally computes the result
# given how many dice in each pool rolled each outcome.
class EvalTechnoir(icepool.MultisetEvaluator):
def next_state(self, state, outcome, action, push, hurt):
if state is None:
# Initial state.
highest, multiple_highest, surviving_push = 0, False, 0
else:
highest, multiple_highest, surviving_push = state
# If this number is not cancelled...
if hurt == 0:
total = action + push
if total > 0:
# Outcomes are seen in ascending order by default,
# so a new outcome is always the highest.
highest = outcome
multiple_highest = total > 1
surviving_push += push
return highest, multiple_highest, surviving_push

eval_technoir = EvalTechnoir()

# 3d6 Action, 2d6 Push, 4d6 Hurt.
# The outcomes are the highest surviving die,
# whether more than one die rolled the highest number,
# and the number of surviving Push dice (of any outcome).
print(eval_technoir(d6.pool(3), d6.pool(2), d6.pool(4)))


### Algorithm

Here's a paper of the algorithm to speed up pool computations. This avoids the exhaustive enumeration used by AnyDice's pool-to-sequence conversion, though in this case the unusual number of pools does decrease the advantage.