# Modelling opposed dice pools with a swap

I have a dice pool system that awards for instance damage to the loser of an opposed roll. Players roll their pool of dice, with each player's target number being the highest number rolled by their opponent. Any roll above or equal to the target counts as one success. Players then compare their number of successes and the loser takes the difference as damage.

In the case of the players having an equal number of successes, then the player with the highest series of dice wins and deals 1 point of damage to the loser. (Starting with the highest die, the players discard matching dice until one of the players has a higher die.)

I have modelled this in AnyDice thus: Brawl Dice

function: brawl A:s vs B:s {
SA: A >= 1@B
SB: B >= 1@A
if SA-SB=0 {
result:(A > B) - (A < B)
}
result:SA-SB
}
output [brawl 3d6 vs 3d6] named "A vs B Damage"


All well and good, but my question is:

How would I model a similar situation but with Player A being able to swap his lowest dice for Player B's highest?

Also, if anyone knows how I can optimise the above code it would be appreciated. Currently I'm limited to fairly small pools.

Example #1:

Player A rolls: 4,3,3,2,1
Player B rolls: 4,4,2,2,2

Player A swaps dice, exchanges his 1 result for Player B's 4 result.
Player A's final pool: 4,4,3,3,2
Player B's final pool: 4,2,2,2,1

Both players' target number is 4 (taken as the highest value of the other player's pool).
Player A has two successes.
Player B has one success.
Player B takes one "damage".

Example #2:

Player A rolls: 4,3,3,2,1
Player B rolls: 6,4,2,2,2

Player A swaps dice, exchanges his 1 result for Player B's 6 result.
Player A's final pool: 6,4,3,3,2
player B's final pool: 4,2,2,2,1

Player A's target number is 4 (2 successes).
Player B's target number is 6 (0 successes).

Player B takes two "damage".

Example #3:

Player A rolls: 4,3,2,2,1
Player B rolls: 4,4,4,3,1

Player A swaps dice, exchanges his 1 result for Player B's 4 result.
Player A's final pool: 4,4,3,2,2
Player B's final pool: 4,4,3,1,1

Player A's target number is 4 (2 successes).
Player B's target number is 4 (2 successes).

Tiebreak!
Player A: 4,4,3,2,2
Player B: 4,4,3,1,1
Player A wins the tiebreak awarding 1 damage to player B.

If there is a Troll solution then that would also be acceptable. I have never used Troll before but I think I should be able to pick it up.

I have managed to get to the same place with Troll as with the AnyDice code above, but I'm stuck at implementing the swapping mechanic. :(

a:=5d6;
b:=5d6;

result := (count (max b) <= a)-(count (max a) <= b);

aa := sum(max(a -- b));
bb := sum(max(b -- a));
tiebreak := if aa > bb then 1
else if aa < bb then -1
else 0;

if result = 0  then tiebreak  else result


Here's a simple "brute force" AnyDice solution to your problem:

function: brawl A:s vs B:s {
SA: A >= 1@B
SB: B >= 1@A
if SA = SB { result: (A > B) - (A < B) }
else { result: SA - SB }
}
function: set element I:n in SEQ:s to N:n {
NEW: {}
loop J over {1 .. #SEQ} {
if I = J { NEW: {NEW, N} }
else { NEW: {NEW, J@SEQ} }
}
result: NEW
}
function: brawl A:s vs B:s with optional swap {
AX: [sort [set element #A in A to 1@B]]
BX: [sort [set element 1 in B to #A@A]]
NOSWAP: [brawl A vs B]
SWAP: [brawl AX vs BX]
result: [highest of NOSWAP and SWAP]
}
output [brawl 3d6 vs 3d6 with optional swap] named "A vs B Damage"


The [brawl A vs B] function does exactly the same thing as in your original code (even though I tweaked it slightly), while the [set element I in SEQ to N] helper function is from this answer. The new [brawl A vs B with optional swap] function just calls the first function twice, once with the lowest die of A swapped with the highest die of B and once without, and returns the better result of the two.

The nice thing about this approach is that we don't actually need to determine exactly when it's advantageous for A to swap the dice. All we need to assume is that, with the actual rolled dice on the table, player A is smart enough to do the math and figure out whether swapping will make their score better or worse.

However, it turns out that in this particular case the optimal strategy is pretty simple: A should swap their lowest die roll with B's highest if and only if it's less than B's highest. (If they're equal, then swapping them makes no difference anyway, of course.) So the following optimized function will in fact give the same results in this case:

function: brawl A:s vs B:s with optional swap {
if #A@A >= 1@B {
result: [brawl A vs B]
}
AX: [sort [set element #A in A to 1@B]]
BX: [sort [set element 1 in B to #A@A]]
result: [brawl AX vs BX]
}


Ps. As an alternative, here's a Python program that calculates the same thing, using (a slightly modified version of) the dice pool generator from this answer:

# generate all possible sorted NdD rolls and their probabilities
# see http://en.wikipedia.org/wiki/Multinomial_distribution for the math
# original: https://rpg.stackexchange.com/questions/63120/anydice-help-ore-like-resolution/65440#65440
# (this version modified to return rolls as simple n-tuples of integers, sorted in descending order)

factorial = [1.0]
def dice_pool(n, d):
for i in range(len(factorial), n+1):
factorial.append(factorial[i-1] * i)
nom = factorial[n] / float(d)**n
for roll, den in _dice_pool(n, d):
yield roll, nom / den

def _dice_pool(n, d):
if d > 1:
for i in range(0, n+1):
highest = (d,) * i
for roll, den in _dice_pool(n-i, d-1):
yield highest + roll, den * factorial[i]
else:
yield (d,) * n, factorial[n]

def brawl_with_swap(rollA, rollB):
# optionally swap A's lowest roll with B's highest:
minA = rollA[-1]
maxB = rollB[0]
if minA < maxB:
rollA = sorted(rollA[:-1] + (maxB,), reverse=True)
rollB = sorted(rollB[1:] + (minA,), reverse=True)
# scoring:
scoreA = sum(x >= rollB[0] for x in rollA)
scoreB = sum(x >= rollA[0] for x in rollB)
if scoreA != scoreB:
return scoreA - scoreB
else:
return (rollA > rollB) - (rollA < rollB)

stats = {}
for rollA, probA in dice_pool(3,6):
for rollB, probB in dice_pool(3,6):
result = brawl_with_swap(rollA, rollB)
if result not in stats: stats[result] = 0.0
stats[result] += probA * probB

for result, prob in sorted(stats.items()):
print("%+2d:%8.4f%% %s" % (result, 100*prob, "#" * int(60*prob + 0.5)))


Unlike A.B.'s stochastic simulation, this code actually calculates the exact probabilities (well, exact up to floating point accuracy, anyway) of the various outcomes directly by enumerating all the possible dice rolls and their probabilities, just like AnyDice does. It's quite a bit faster than AnyDice, however, with the 3d6 vs. 3d6 case taking only about 0.1 seconds, and 4d6 vs. 4d6 taking only 0.25 seconds on the TIO server.

Assuming that both players play optimally and if I understood your explanation, you can use this code to find the probability of certain outcomes depending on variables such as dice used, and number of dice rolled for each player.

The link provided above allows you to enter different input for these variables as well as number of iterations. It is defaulted to using a d6 with 5 dice per player pool, and 100,000 iterations.

from random import randint

sidesOfDie = int(input())
numberofRolls = int(input())
loops = int(input())

iterator = 0
winsA = 0
winsB = 0
ties = 0
tiesWinA = 0
tiesWinB = 0
sameRollBefore = 0
sameRollAfter = 0
damageToA = 0
damageToB = 0

while iterator != loops:
iterator += 1

valuesA = []
valuesB = []

while len(valuesA) < numberofRolls:
valuesA.append(randint(1, sidesOfDie))
valuesB.append(randint(1, sidesOfDie))

valuesA = sorted(valuesA)[::-1]
valuesB = sorted(valuesB)[::-1]

if valuesA == valuesB:
sameRollBefore += 1

temp = valuesB[0]
valuesB[0] = valuesA[4]
valuesA[4] = temp
valuesA = sorted(valuesA)[::-1]
valuesB = sorted(valuesB)[::-1]

targetA = int(valuesB[0])
targetB = int(valuesA[0])

countA = 0
countB = 0
for v in valuesA:
if int(v) == targetA:
countA += 1
for v in valuesB:
if int(v) == targetB:
countB += 1

if countA == countB:
ties += 1
if valuesA == valuesB:
sameRollAfter += 1
else:
count = 0
while valuesA[count] == valuesB[count] and count != numberofRolls:
count += 1
else:
if valuesA[count] > valuesB[count]:
tiesWinA += 1
damageToB += 1
else:
tiesWinB += 1
damageToA += 1
elif countA > countB:
winsA += 1
damageToB += (countA - countB)
else:
winsB += 1
damageToA += (countB - countA)

print('Total number of iterations:', iterator)
print('Dice used: d' + str(sidesOfDie))
print('Number of dice rolled by each player for each iteration:', numberofRolls)

print('\nPlayer A wins:', winsA)
print('Player A win percentage:', winsA/iterator)
print('Player B wins:', winsB)
print('Player B win percentage:', winsB/iterator)

print('\nTotal damage done:', damageToA + damageToB)
print('Damage done to Player A:', damageToA)
print('Average damage done to Player A per iteration:', damageToA/iterator)
print('Damage done to Player B:', damageToB)
print('Average damage done to Player B per iteration:', damageToB/iterator)

print('\nTiebreakers:', ties)
print('Tiebreaker percentage:', ties/iterator)
print('%d tiebreakers won by Player A with a percentage of' % tiesWinA, tiesWinA/ties)
print('%d tiebreakers won by Player B with a percentage of' % tiesWinB, tiesWinB/ties)
print('%d same rolls after swapping with a percentage of' % sameRollAfter, sameRollAfter/ties)

print('\nNumber of same rolls before swapping:', sameRollBefore)


It's written in Python since I'm not familiar with AnyDice.

• While the script itself might or might not work, the question asked for useage of anydice. Anwering with python is like... answering to a D&D 5E question with rules from StarWars D20 – Trish Jan 3 '18 at 18:41
• Thanks for the reply, however it doesn't help me as I'm looking for a way to calculate the probabilities of certain actions not necessary look at individual outcomes. For instance I need a way to compare adding X number of dice to a pool opposed to swapping dice between pools. – Doug Jan 4 '18 at 7:56
• @Doug the typical way to do that kind of simulation is to run it repeatedly and aggregate the results - a technique known as Monte Carlo simulation. If you know enough to be able to take the example code and wrap it in something that will run it a few million times and record the results, that will give you an accurate statistical analysis of the results. – Carcer Jan 4 '18 at 11:40
• @A.B. sorted(values, reverse=True) is cleaner. – okeefe Jan 4 '18 at 14:47
• @Doug I provided a link in the answer to an online interpreter where all you need to do is enter the values for different variables and press run. Can be used from anywhere with internet access. But I am glad you got the answer in your preferred language. – A.B. Jan 5 '18 at 13:21

Torben creator of Troll Dice has very kindly provided the answer:

a := 5d6;
b := 5d6;
if (min a) < (max b) then (
ab := (a -- (min a)) U (max b);
ba := (b -- (max b)) U (min a);

result := (count (max ba) <= ab)-(count (max ab) <= ba);

aa := sum(max(ab -- ba));
bb := sum(max(ba -- ab));
tiebreak := if aa > bb then 1
else if aa < bb then -1
else 0;

if result = 0  then tiebreak  else result
) else (
result := (count (max b) <= a)-(count (max a) <= b);

aa := sum(max(a -- b));
bb := sum(max(b -- a));
tiebreak := if aa > bb then 1
else if aa < bb then -1
else 0;

if result = 0  then tiebreak  else result
)

• It does, hit the calculate probability button on the web page. – Doug Jan 10 '18 at 11:54