WIP of table generator
This commit is contained in:
parent
044227bf2d
commit
32c9832daa
72
deadsands/www/sources/weather.yaml
Normal file
72
deadsands/www/sources/weather.yaml
Normal 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
245
deadsands/www/tables.py
Normal 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])))
|
Loading…
Reference in New Issue
Block a user