Skip to content

Instantly share code, notes, and snippets.

@jsocol
Created June 22, 2020 01:57
Show Gist options
  • Save jsocol/9d24ea6bd5089f2b5f0dd7ca52c1af8e to your computer and use it in GitHub Desktop.
Save jsocol/9d24ea6bd5089f2b5f0dd7ca52c1af8e to your computer and use it in GitHub Desktop.

Revisions

  1. jsocol created this gist Jun 22, 2020.
    240 changes: 240 additions & 0 deletions rand_gems.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,240 @@
    """Generate random gemstones for D&D 5e
    It always bothered me that the tables in the DMG won't let you randomly
    generate a diamond worth 50gp, or 300gp, or 500gp, when these are all common
    spell components.
    This script generates random gemstones and values. It considers that you might
    have a small diamond worth 50gp, but probably not a giant azurite worth 5000gp.
    First, we get a base and a multiplier. A base could be e.g. 10, 100, 1000. A
    multiplier is any integer between 1 and 15. These choices are weighted, and you
    can adjust the weights if you wish. The total value of the gem is base *
    multiplier, so the most common gem should be 10 * 1 or 10gp, and the rarest
    would be the maximum at 15,000gp. You can also add additional bases, like 50gp,
    or change the range of multipliers, etc.
    Then we use this value to generate a type of gemstone. Each collection in the
    DMG is listed, with a couple of additions based on costly spell components
    (e.g. it must be possible to have an agate worth 1000gp since Awaken requires
    it). Each group is also given a maximum of 10x its typical value, e.g. you are
    not likely to find a carnelian worth more than 500gp. Gemstones do not have a
    minimum value, so it is possible to generate a 10gp diamond. After all, gems
    can always be cut down.
    Certain types of gemstone are more common than others, which picking the type
    of gem, there is a COMMON_CHANCE * 100% chance that you'll get one of the
    common types, which are, themselves, weighted.
    All types of gemstones are equally weighted within their groups. These weights
    can be changed.
    Run this script on the command line with the -h flag to see arguments.
    """

    import argparse
    import random


    COMMON_CHANCE = 0.4

    common_gems = [
    (5, 'diamond'),
    (5, 'ruby'),
    (2, 'jade'),
    (2, 'blue sapphire'),
    (1, 'emerald'),
    (1, 'opal'),
    ]

    gp10 = [
    (1, 'azurite'),
    (1, 'banded agate'),
    (1, 'blue quartz'),
    (1, 'eye agate'),
    (1, 'hematite'),
    (1, 'lapis lazuli'),
    (1, 'malachite'),
    (1, 'moss agate'),
    (1, 'obsidian'),
    (1, 'rhodochrosite'),
    (1, 'tiger eye'),
    (1, 'turquoise'),
    ]

    gp50 = [
    (1, 'bloodstone'),
    (1, 'carnelian'),
    (1, 'chalcedony'),
    (1, 'chrysoprase'),
    (1, 'citrine'),
    (1, 'jasper'),
    (1, 'moonstone'),
    (1, 'onyx'),
    (1, 'quartz'),
    (1, 'sardonyx'),
    (1, 'star rose quartz'),
    (1, 'zircon'),
    ]

    gp100 = [
    (1, 'amber'),
    (1, 'amethyst'),
    (1, 'black onyx'),
    (1, 'chrysoberyl'),
    (1, 'coral'),
    (1, 'garnet'),
    (1, 'jade'),
    (1, 'jet'),
    (1, 'pearl'),
    (1, 'spinel'),
    (1, 'tourmaline'),
    ]

    gp500 = [
    (1, 'alexandrite'),
    (1, 'aquamarine'),
    (1, 'black pearl'),
    (1, 'blue spinel'),
    (1, 'peridot'),
    (1, 'topaz'),
    ]

    gp1000 = [
    (1, 'agate'),
    (1, 'black opal'),
    (1, 'blue sapphire'),
    (1, 'emerald'),
    (1, 'fire opal'),
    (1, 'opal'),
    (1, 'star ruby'),
    (1, 'star sapphire'),
    (1, 'yellow sapphire'),
    ]

    gp5000 = [
    (1, 'black sapphire'),
    (1, 'diamond'),
    (1, 'jacinth'),
    (1, 'ruby'),
    ]


    max_values = {
    100: gp10,
    500: gp50,
    1000: gp100,
    5000: gp500,
    10000: gp1000,
    50000: gp5000,
    }


    bases_relative = (
    (60, 10),
    (35, 100),
    (5, 1000),
    )


    multipliers_relative = (
    (15, 1),
    (6, 2),
    (5, 3),
    (4, 4),
    (12, 5),
    (4, 6),
    (3, 7),
    (3, 8),
    (2, 9),
    (10, 10),
    (1, 11),
    (1, 12),
    (1, 13),
    (1, 14),
    (1, 15),
    )


    def absolute_weights(weights, max_value=None):
    result = {}
    total = 0
    for w, v in weights:
    if max_value is not None and v > max_value:
    continue
    total += w
    result[total] = v
    return result


    def get_weighted_value(relative, max_value=None):
    weighted = absolute_weights(relative, max_value)
    k = random.randint(1, max(k for k in weighted.keys()))

    for m, v in weighted.items():
    if k < m:
    return v
    return v


    def get_base(max_base=None):
    return get_weighted_value(bases_relative, max_value=max_base)


    def get_multiplier(max_m=None):
    return get_weighted_value(multipliers_relative, max_value=max_m)


    def get_possible_gems(value):
    gems = []
    for v, g in max_values.items():
    if value < v:
    gems += g
    return gems


    def pick_gem(gems):
    if random.random() < COMMON_CHANCE:
    return get_weighted_value(common_gems)
    return get_weighted_value(gems)


    def get_random_gem(max_base=None, max_multiplier=None):
    base = get_base(max_base)
    multiplier = get_multiplier(max_multiplier)

    value = base * multiplier
    gems = get_possible_gems(value)
    type_ = pick_gem(gems)

    return {
    'type': type_,
    'value': value,
    }


    def gem_phrase(gem):
    article = 'an' if gem['type'][0] in 'aeiou' else 'a'
    return '{} {} worth {}gp'.format(article, gem['type'], gem['value'])


    def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-b', '--max-base', type=int, default=None)
    parser.add_argument('-m', '--max-multiplier', type=int, default=None)
    parser.add_argument('-n', '--count', type=int, default=1)
    options = parser.parse_args()

    max_base = options.max_base
    max_multiplier = options.max_multiplier
    for _ in range(options.count):
    gem = get_random_gem(max_base, max_multiplier)
    print(gem_phrase(gem))


    __main__ = main


    if __name__ == '__main__':
    main()