5
\$\begingroup\$

I am looking for help with creating an AnyDice program that can calculate the possibilities of the following mechanic.

Players roll a number of d6, then proceed to sum all dice of a single set of matching dice.

Thus...

... a roll of 3, 3, 5, 5, 6 would generate a score of 10 (5 + 5).

... a roll of 1, 1, 2, 2, 4 would generate a score of 4 (2 + 2).

If no matching set is rolled, the highest die is regarded a set.

Thus...

... a roll of 1, 2, 3, 4, 5 would generate a score of 5 (5).

Preferably, the program should output [mechanic] >= [mechanic].

Thank you.

\$\endgroup\$
  • 2
    \$\begingroup\$ What if a single die is higher than any matched set? Like, if I rolled 1, 1, 1, 2, 3, 6, could I take the 6, or am I stuck with the (1 + 1 + 1) = 3? \$\endgroup\$ – DuckTapeAl Jun 6 '15 at 22:33
  • \$\begingroup\$ @DuckTapeAL: you are never forced to use a set if a single die would produce a better result. \$\endgroup\$ – user23378 Jun 11 '15 at 14:52
  • 1
    \$\begingroup\$ @RavenStag You seem to have made two accounts. You should merge them by following the instructions here. \$\endgroup\$ – SevenSidedDie Jun 11 '15 at 22:54
4
\$\begingroup\$

Edited using Ilmari Karonen advice.

You only need to make N be the number of rolled dies.

Use the link or copy the code yourself.

function: sum all I in ROLL:s{
 result: I*( ROLL=I )
}

function: myroll ROLL:s{

 A: [sum all 1 in ROLL]

 loop I over {2..6}{
  B: [sum all I in ROLL]
  A: [highest of A and B] 
 }

 result: A
}

output [myroll 5d6]
\$\endgroup\$
2
\$\begingroup\$

Here's a simpler AnyDice solution, originally based on Albert Masclans' code, but almost entirely rewritten:

function: highest sum of equal dice in ROLL:s {
  MAX: 0
  loop I over ROLL {
    SUM: I * (ROLL = I)
    MAX: [highest of SUM and MAX] 
  }
  result: MAX
}
output [highest sum of equal dice in 5d6]

You can change the 5d6 on the last line to anything you want. Large die pools, however, will likely time out, because the code works by naïvely iterating over every possible roll (that's what happens in AnyDice when you pass a die to a function expecting a sequence), and that quickly becomes way too slow.


For somewhat larger dice pools, here's a Python program that does the same calculation:

# ORE-like dice odds calculator for https://rpg.stackexchange.com/q/63120
import argparse
parser = argparse.ArgumentParser(description='Distribution of the maximum sum of identical dice in NdD.')
parser.add_argument('N', type=int, help='number of dice to roll')
parser.add_argument('D', type=int, default=6, nargs='?', help='number of sides per die')
args = parser.parse_args()

# generate all possible sorted NdD rolls and their probabilities
# see http://en.wikipedia.org/wiki/Multinomial_distribution for the math
factorial = [1.0]
def dice_pool(n, d):
    for i in xrange(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 xrange(0, n+1):
            pair = (d, i)
            for roll, den in _dice_pool(n-i, d-1):
                yield roll + (pair,), den * factorial[i]
    else:
        yield ((d, n),), factorial[n]

# the actual calculation and output code starts here
dist = {}
for roll, prob in dice_pool(args.N, args.D):
    total = max(num * count for num, count in roll)
    if total not in dist: dist[total] = 0.0
    dist[total] += prob

max_prob = max(dist.values())
print "Maximum sum of identical dice in %dd%d:" % (args.N, args.D)
for total, prob in dist.iteritems():
    print "%4d %7.4f%% %s" % (total, 100 * prob, "#" * int(60 * prob / max_prob + 0.5))

The dice_pool(n, d) function that makes up most of the code is actually a general-purpose tool for calculating probabilities involving unordered pools of dice. It's a generator that successively returns each possible roll (in the form ((1, n1), (2, n2), (3, n3), ...), where n1, n2, n3, etc. are the number of dice that rolled 1, 2, 3, etc. respectively) and the probability of obtaining that roll. (Yes, the yielded value is a tuple containing a tuple of tuples. It's perfectly natural to use.)

For speed, I've chosen to use floats for the probability calculations, despite the possibility of small roundoff errors. It would be possible to obtain exact results by using integer / rational math everywhere, but the gain would likely be minimal, since there should be no real opportunities for catastrophic cancellation here. There are also a few other optimizations, like precalculating the factorials of numbers from 0 to N. With the optimizations, calculating the probabilities for 20d6 takes about 0.8 seconds on my old laptop, while 50d6 takes about 47 seconds.

\$\endgroup\$
1
\$\begingroup\$

I can't do it in AnyDice, but it's not that hard to code in Python...

In Python

# OREishOdds.py 
# ©2015 William F. Hostman. CC by attribution
# res is output results array - a place to store our output.
res = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,00,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
# num is our running total of iterations
num = 0
# work is the array we do the math work upon.
work = [0,0,0,0]
# we now tell it what the die looks like...
diedef = [1,2,3,4,5,6]
# dice: for die in [array of faces]
for a in diedef:
    for b in diedef:
        for c in diedef:
            for d in diedef:
                 # work
                 work[0] = a
                 work[1] = b
                 work[2] = c
                 work[3] = d
                 # insert additional dice above
                 work.sort()
                 # counting
                 score = 0
                 for z in diedef:
                     tempscore = 0
                     for y in work:
                         if y == z:
                             tempscore += y
                     if tempscore > score:
                         score = tempscore
                 res[score] +=1
                 num += 1
                 # end of work
# output
temp = 0
for score in res:
    temp += 1
    print temp, score, (1000*score/num)

Extending this

res needs to have at least as many 0 values as 1+(sides * number).

Each die gets a separate value, but you could shortcut this by defining a die as a variable and replacing the arrays for individual die for statements with that defined array.

The work block needs to be indented together. Python is whitespace dependent.

To add a die: add another for line with another letter, add another 0 to the array work, add another work[ ] = line with the next sequential number, and the new die's variable.

You cannot shortcut and simply load work directly in the for statements, because the sort routine would screw up the accounting.

The output is in result value, number of occurrences, and permille result (percentage * 10). If you want more places, increase the multiplier in the print line.


Tweaking the dice values

If you want to play around with some funky results, for example, replacing rolls of 1 with 0 (so 1's count as nothing), just replace the 1's in the arrays in the for lines. So the arrays would be [0,2,3,4,5,6].

You switch to d8's by using [1,2,3,4,5,6,7,8].


It's a bit uglier to code for ORE's 2-d results (width and height), but since you weren't asking about that... aw, heck, I'll discuss that, too.

And that's because any true ORE roll has the question of whether one's prioritizing height (value of number counted) or width (number of replications in the roll).

If one prioritizes one or the other, it's easy to grid out. Otherwise, one has to tick the various array locations possible on a 2d grid. The sum of the entries would thus be more than the count of iterations.


This isn't particularly fast code. (Python's not particularly fast, and this isn't optimized code.) But its good enough provided you keep it under 8 dice or so, and those dice are all under d12's.


Above code ©2015 William F. Hostman. Permission to duplicate and use for any legal purpose granted, including inclusion in texts, provided attribution is given. Code took 10 minutes to write. Took as long or longer to comment it.

\$\endgroup\$
1
\$\begingroup\$

Not quite sure why mrlemon's answer got downvoted, as I had the same inclination. In my case, though, I wanted to use some of the newer items found in the Python3 standard library to make that code quite a bit simpler:

# OREishOdds.py
# ©2015 William F. Hostman. CC by attribution
# Python3 version (c)2017 Tim Keating

from itertools import product

d6 = list(range(1, 7))

def compute_scores(num, die):
    score_counts = {}

    all_possible_outcomes = product(*([die] * num))
    for roll in all_possible_outcomes:
        roll_score = score(roll)
        score_counts.update({roll_score: score_counts.get(roll_score, 0)+1})

    return score_counts

def score(roll):
    scores = {}
    for die in roll:
        scores[die] = scores.get(die, 0) + die

    return max(scores.values())

def all_odds(num, die):
    total_num_outcomes = len(die) ** num
    total_values = len(die) * num
    score_counts = compute_scores(num, die)
    for n in range(1, total_values + 1):
        score = score_counts.get(n, 0)
        print('{:<4}{:>6}{:>10.2%}'.format(n, score, score / total_num_outcomes))

if __name__ == '__main__':
    all_odds(5, d6)
\$\endgroup\$
0
\$\begingroup\$

After reading aramis' very long answer (no offense), I couldn't help but golf the python down into a neat function. This one is using statistics for simplicity, rather than actually calculate the probabilities, but it works quite well as an estimate.

def orestats(X,Y=6,N=1000):
    """
    X: Number of dice rolled
    Y: Number of Sides per Die
    N: Number of repetitions for statistics
    """

    def ore(X,Y):
        # Roll XdY and return the ORE result as a list
        from random import randint
        roll = [randint(1,Y) for _ in range(X)]
        return [i*roll.count(i) for i in range(1,Y+1)]

    # Calculate the results N times and deconvolve the list.
    result = [x for y in [ore(X,Y) for _ in range(N)] for x in y] 

    # Count all occurences of every possible result
    totals = [(i,result.count(i)/N*100) for i in range(1,X*Y+1)]

    # Print into a neat pseudo-AnyDice format (result, probability, bar graph)
    print('\n'.join('{:>3}: {:>4.1f} {}'.format(x[0],x[1],'█'*int(x[1])) for x in totals))

 # Example call for 10d12, executed 10000 times
 orestats(10,12,10000)

(Took me longer than 10 minutes, too.)

\$\endgroup\$

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.