Skip to content

Instantly share code, notes, and snippets.

@modality
Last active October 15, 2021 18:38
Show Gist options
  • Save modality/1746abad86f74843a1e75f853a9fa992 to your computer and use it in GitHub Desktop.
Save modality/1746abad86f74843a1e75f853a9fa992 to your computer and use it in GitHub Desktop.
DICE SCHEME GENERATOR

The problem

I want to create random tables for roleplaying games. I have N items I want to go on the random table, and there are M optional items that could also go on that table. What are all the possible rolling schemes, using standard RPG dice, that could cover all the required items on the table, using the optional items to pad the table?

How to run this, and other notes

My solution is a breadth-first search which prints out possible dice schemes. You could modify it to return the schemes instead of printing them. There are constraints you can change at the top of the file.

Run this way:

$ python3 dicebag.py

I have chosen some constraints, you can turn "weird handfuls" and "weird double handfuls" on and off, though if weird double handfuls is enabled, weird handfuls should be too.

  1. Don't use more than two kinds of dice
  2. WEIRD_HANDFULS - when this is True, if you are using two kinds of dice, you can have more than one of one kind.
  3. WEIRD_DOUBLE_HANDFULS - when this is True, if you are using two kinds of dice, you can have more than one of both kinds.

Disabing both WEIRD_HANDFULS and WEIRD_DOUBLE_HANDFULS means that if two dice are used, only one of each is used, ex:

1d8+1d20

But never:

2d8+1d20 (weird handful)

4d8+2d20 (weird double handful)

Sample output

$ python3 dicebag.py
Solutions for 29 items with 6 optional items
1d6+9d4 (10-42), 33 possible items
4d8+1d6 (5-38), 34 possible items
6d6+1d4 (7-40), 34 possible items
1d20+5d4 (6-40), 35 possible items
1d20+2d6 (3-32), 30 possible items
3d10+1d6 (4-36), 33 possible items
3d10+1d8 (4-38), 35 possible items
1d8+9d4 (10-44), 35 possible items
1d20+2d8 (3-36), 34 possible items
1d6+8d4 (9-38), 30 possible items
10d4 (10-40), 31 possible items
1d20+4d4 (5-36), 32 possible items
4d8 (4-32), 29 possible items
1d12+7d4 (8-40), 33 possible items
1d12+4d6 (5-36), 32 possible items
1d10+7d4 (8-38), 31 possible items
1d12+2d10 (3-32), 30 possible items
1d10+3d8 (4-34), 31 possible items
6d6 (6-36), 31 possible items
1d8+8d4 (9-40), 32 possible items
1d8+5d6 (6-38), 33 possible items
3d12 (3-36), 34 possible items
1d10+8d4 (9-42), 34 possible items
1d10+5d6 (6-40), 35 possible items
1d20+1d12 (2-32), 31 possible items
1d20+1d10 (2-30), 29 possible items
2d12+1d8 (3-32), 30 possible items
1d20+3d4 (4-32), 29 possible items
2d12+1d10 (3-34), 32 possible items
1d12+6d4 (7-36), 30 possible items
1d20+3d6 (4-38), 35 possible items
1d8+7d4 (8-36), 29 possible items
4d8+1d4 (5-36), 32 possible items
11d4 (11-44), 34 possible items
3d10+1d4 (4-34), 31 possible items
1d12+3d8 (4-36), 33 possible items
1d10+4d6 (5-34), 30 possible items
5d6+1d4 (6-34), 29 possible items
WEIRD_HANDFULS = True
WEIRD_DOUBLE_HANDFULS = True
COMMON_DICE = [4, 6, 8, 10, 12, 20]
class DiceScheme:
def __init__(self, size1, count1, size2=None, count2=None):
self.size1 = size1
self.count1 = count1
self.size2 = size2
self.count2 = count2
@property
def roll_range(self):
# the inclusive range of outcomes with this scheme
if self.size2 is None:
return (self.count1, (self.size1 * self.count1))
return (
self.count1 + self.count2,
(self.size1 * self.count1) + (self.size2 * self.count2),
)
@property
def breadth(self):
# the number of possible outcomes with this scheme
return self.roll_range[1] - self.roll_range[0] + 1
@property
def as_tuple(self):
# sorted tuple with the biggest size die first
if self.size2 is None:
return (self.size1, self.count1, None, None)
if self.size1 > self.size2:
return (self.size1, self.count1, self.size2, self.count2)
return (self.size2, self.count2, self.size1, self.count1)
@property
def as_string(self):
# human readable string
if self.size2 is None:
return f"{self.count1}d{self.size1} ({self.roll_range[0]}-{self.roll_range[1]}), {self.breadth} possible items"
return f"{self.count1}d{self.size1}+{self.count2}d{self.size2} ({self.roll_range[0]}-{self.roll_range[1]}), {self.breadth} possible items"
def is_just_right(self, items, soft_items):
return self.breadth >= items and self.breadth <= (items + soft_items)
def is_too_big(self, items, soft_items):
return self.breadth > (items + soft_items)
def generate_descendants(self):
descendants = []
# simple clone, ex: 1d6 -> 2d6
if self.size2 is None or WEIRD_HANDFULS:
simple = DiceScheme(self.size1, self.count1 + 1)
descendants.append(simple)
# combo clones, ex: 1d6 -> 1d6+1d4
if self.size2 is None and self.count1 == 1:
for die in COMMON_DICE:
if die != self.size1:
combo = DiceScheme(self.size1, self.count1, die, 1)
descendants.append(combo)
# weird handful clones
# combo -> weird handful, ex: 1d6+1d4 -> 2d6+1d4
# weird handful -> weird double handful, ex: 3d6+1d4 -> 3d6+2d4
if self.size2 is not None:
if self.count1 > 1 and WEIRD_HANDFULS:
weird = DiceScheme(self.size1, self.count1 + 1, self.size2, self.count2)
descendants.append(weird)
if WEIRD_DOUBLE_HANDFULS:
weird = DiceScheme(self.size1, self.count1, self.size2, self.count2 + 1)
descendants.append(weird)
return descendants
def get_solutions(num_items, soft_items):
solutions = set()
visited = set()
possible_schemes = [DiceScheme(d, 1) for d in COMMON_DICE]
while len(possible_schemes) > 0:
scheme = possible_schemes.pop(0)
visited.add(scheme.as_tuple)
if scheme.is_too_big(num_items, soft_items):
continue
if scheme.is_just_right(num_items, soft_items):
solutions.add(scheme.as_tuple)
descendants = scheme.generate_descendants()
for new_scheme in descendants:
if new_scheme.as_tuple not in visited:
possible_schemes.append(new_scheme)
return [DiceScheme(*solution) for solution in solutions]
def print_solutions(num_items, soft_items):
print(f"Solutions for {num_items} items with {soft_items} optional items")
solutions = get_solutions(num_items, soft_items)
for solution in solutions:
print(solution.as_string)
if __name__ == "__main__":
import random
num_items = random.randint(3, 70)
soft_items = random.randint(3, 8)
print_solutions(num_items, soft_items)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment