initial commit
This commit is contained in:
parent
2810da2179
commit
c8c3c0724f
0
rolltable/__init__.py
Normal file
0
rolltable/__init__.py
Normal file
244
rolltable/tables.py
Normal file
244
rolltable/tables.py
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
- Frequency
|
||||||
|
- 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]))]
|
||||||
|
self._values = sorted(self._values,
|
||||||
|
key=lambda val: list(val[1].keys())[0])
|
||||||
|
return self._values
|
||||||
|
|
||||||
|
@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