WIP of table generator

This commit is contained in:
evilchili 2022-07-30 00:45:49 -07:00
parent 044227bf2d
commit 32c9832daa
2 changed files with 317 additions and 0 deletions

View File

@ -0,0 +1,72 @@
metadata:
headers:
- Roll
- Frequency
- Description
- Effect
die: 20
frequencies:
default:
Common: 0.5
Uncommon: 0.3
Rare: 0.15
Weird: 0.05
deadly:
Common: 0.05
Uncommon: 0.15
Rare: 0.3
Weird: 0.5
Common:
- Clear Skies: No effect
Uncommon:
- Scorching Temperatures: 2x water consumption
- Clinging Sand: Disadvantage on STR checks and saves
- Searing Light: Disadvantage on DEX checks and saves
- Ghostly Wailing: Disadvantage on CON checks for concentration checks
- Low Oxygen: Disadvantage on INT checks and saves
- Glowing Dust Storm: Disadvantge on WIS checks and saves
- Psychic Fog: Disadvantage on CHA checks and saves
- Sand Storm:
Heavily obscured; visibility 5ft, disadvantage on WIS
(Perception) checks and INT (Investigation) checks
- Burning Sands:
Ground travelers must succeed on a DC 15 CON save or take 1d6
fire damage every half-day.
- Oppressive Quiet: Silence beyond 20ft; disadvantage on WIS checks involving sound
- Insect Swarms: 1d6 Piercing damage per half-day
- Air of Dread: Succeed on DC 15 WIS save or be frightened for half-day
Rare:
- Spontaneous Hail: No effect; counts as magical water if consumed
- Glowing Auras: Faerie Fire spell effect
- Heavy Gravity: Disadvantge on STR, DEX checks and saves
- Random Whirlwinds:
At the start of each round, whirlwinds 10ft in diameter and and
100ft tall randomly form. Any creature within 10ft of a
whirlwind at the start of their turn must succeed on a DC 15 STR
(Athletics) save or be pulled 10ft towards it. A creature
occupying the same space as a whirlwind at the start of their
turn takes 1d8 bludgeoning damage.
- Tentacles:
At the start of each round, Black Tentacles is cast on a random area. Creatures starting their turn there must succeed on a DC 15 DEX saving throw or take 1d6 bludgeoning damage and be restrained. A creature who starts their turn already restrained takes 1d6 bludgeoning damage.
- Broken Time:
At the start of each round, the DM rolls a d20. Until the start
of the next round, anyone with initiative score greater than or
equal to the DM's roll is slowed and anyone lower is hasted.
Weird:
- Whispered Insanity: >
"Clear skies" (but you are cursed; the DM rolls for each traveler)
d1 - None of this is really happening.
d2 - We're invincible!
d3 - They're all turning against you.
d4 - You're being followed.
d5 - An object in your inventory is the key to your survival.
d6 - Your soul has fled your body.
d7 - One of your companions is an imposter.
d8 - You should take off your clothes and experience true freedom.
- Inverted Bubble Rain: No effect
- Hot Metal Storm:
Heat Metal spell effect (gearforged take 1pt exhaustion per half-day)
- Morphing Dust: DC 15 WIS saving throw or become a frog each half-day
- Ethereal Wind: Travelers are transported to the Border Ethereal.
- Arcane Mirages: Mirage Arcane spell effect; disadvantage on skill checks to Forage and Survey
- Magonic Field Resonance: All spell damage / effect rolls are maxed.

245
deadsands/www/tables.py Normal file
View File

@ -0,0 +1,245 @@
import yaml
import random
from collections.abc import Generator
from typing import Optional, Mapping, List
class RollTable:
"""
Generate a roll table using weighted distributions of random options.
Usage:
Given source.yaml containing options such as:
option1:
- key1: description
- key2: description
...
...
Generate a random table:
>>> print(RollTable(path='source.yaml'))
d1 option6 key3 description
d2 option2 key2 description
d3 option3 key4 description
...
You can customize the frequency distribution, headers, and table size by
defining metadata in your source file.
Using Metadata:
By default options are given uniform distribution and random keys will be
selected from each option with equal probability. This behaviour can be
changed by adding an optional metadata section to the source file:
metadata:
frequenceis:
default:
option1: 0.5
option2: 0.1
option3: 0.3
option4: 0.1
This will guarantee that random keys from option1 are selected 50% of the
time, from option 2 10% of the time, and so forth. Frequencies should add
up to 1.0.
If the metadata section includes 'frequencies', The 'default' distribution
must be defined. Additional optional distributions may also be defined, if
you want to provide alternatives for specific use cases.
metadata:
frequenceis:
default:
option1: 0.5
option2: 0.1
option3: 0.3
option4: 0.1
inverted:
option1: 0.1
option2: 0.3
option3: 0.1
option4: 0.5
A specific frequency distribution can be specifed by passing the 'frequency'
parameter at instantiation:
>>> t = RollTable('source.yaml', frequency='inverted')
The metadata section can also override the default size of die to use for
the table (a d20). For example, this creates a 100-row table:
metadata:
die: 100
This too can be overridden at instantiation:
>>> t = RollTable('source.yaml', die=64)
Finally, headers for your table columns can also be defined in metadata:
metadata:
headers:
- Roll
- Category
- Description
- Effect
This will yield output similar to:
>>> print(RollTable(path='source.yaml'))
Roll Category Name Effect
d1 option6 key3 description
d2 option2 key2 description
d3 option3 key4 description
...
"""
def __init__(self, path: str, frequency: str = 'default',
die: Optional[int] = None, collapsed: bool = True):
"""
Initialize a RollTable instance.
Args:
path - the path to the source file
frequency - the name of the frequency distribution to use; must
be defined in the source file's metadata.
die - specify a die size
collapsed - If True, collapse multiple die values with the same
options into a single line.
"""
self._path = path
self._frequency = frequency
self._die = die
self._collapsed = collapsed
self._metadata = None
self._source = None
self._values = None
def _load_source(self) -> None:
"""
Cache the yaml source and parsed or generated the metadata.
"""
if self._source:
return
with open(self._path, 'r') as source:
self._source = yaml.safe_load(source)
def _defaults():
num_keys = len(self._source.keys())
default_freq = num_keys/100
return {
'headers': [''] * num_keys,
'die': self._die,
'frequencies': {
'default': [(k, default_freq) for k in self._source.keys()]
}
}
self._metadata = self._source.pop('metadata', _defaults())
def _collapsed_lines(self) -> Generator[list]:
"""
Generate an array of column values for each row of the table but
sort the values and squash multiple rows with the same values into one,
with a range for the die roll instead of a single die. That is,
d1 foo bar baz
d2 foo bar baz
becomes
d1-d2 foo bar baz
"""
def collapsed(last_val, offset, val, i):
(cat, option) = last_val
(k, v) = list(*option.items())
if offset + 1 == i:
return [f'd{i}', cat, k, v]
else:
return [f'd{offset+1}-d{i}', cat, k, v]
last_val = None
offset = 0
for (i, val) in enumerate(self.values):
if not last_val:
last_val = val
offset = i
continue
if val != last_val:
yield collapsed(last_val, offset, val, i)
last_val = val
offset = i
yield collapsed(last_val, offset, val, i+1)
@property
def freqtable(self):
return self.metadata['frequencies'][self._frequency]
@property
def source(self) -> Mapping:
"""
The parsed source data
"""
if not self._source:
self._load_source()
return self._source
@property
def metadata(self) -> Mapping:
"""
The parsed or generated metadata
"""
if not self._metadata:
self._load_source()
return self._metadata
@property
def values(self) -> List:
"""
Randomly pick values from the source data following the frequency
distrubtion of the options.
"""
if not self._values:
weights = []
options = []
for (option, weight) in self.freqtable.items():
weights.append(weight)
options.append(option)
freqs = random.choices(options, weights=weights,
k=self._die or self.metadata['die'])
self._values = []
for option in freqs:
self._values += [(option, random.choice(self.source[option]))]
return sorted(self._values, key=lambda val: list(val[1].values())[0])
@property
def lines(self) -> Generator[List]:
"""
Yield a list of table rows suitable for formatting as output.
"""
yield self.metadata['headers']
if self._collapsed:
for line in self._collapsed_lines():
yield line
else:
for (i, item) in enumerate(self.values):
(cat, option) = item
(k, v) = list(option.items())[0]
yield [f'd{i+1}', cat, k, v]
def __str__(self) -> str:
"""
Return the lines as a single string.
"""
return "\n".join([
'{:10s}\t{:8s}\t{:20s}\t{:s}'.format(*line) for line in self.lines
])
if __name__ == '__main__':
import sys
print(RollTable(path=sys.argv[1], die=int(sys.argv[2])))