1.0 release
This commit is contained in:
parent
50379e9a2a
commit
1cc245af07
147
README.md
147
README.md
|
@ -1,88 +1,71 @@
|
||||||
Generate a roll table using weighted distributions of random options.
|
# RollTables
|
||||||
Given source.yaml containing options such as:
|
|
||||||
|
|
||||||
option1:
|
RollTables is a python library for generating tables suitable for selecting random options using dice rolls.
|
||||||
- key1: description
|
|
||||||
- key2: description
|
|
||||||
...
|
|
||||||
...
|
|
||||||
|
|
||||||
Generate a random table:
|
## Quick Start
|
||||||
|
|
||||||
>>> print(RollTable(path='source.yaml'))
|
```
|
||||||
d1 option6 key3 description
|
# example.yaml
|
||||||
d2 option2 key2 description
|
|
||||||
d3 option3 key4 description
|
|
||||||
...
|
|
||||||
|
|
||||||
You can customize the frequency distribution, headers, and table size by
|
# metadata is optional
|
||||||
defining metadata in your source file.
|
metadata:
|
||||||
|
# headers are optional
|
||||||
|
headers:
|
||||||
|
# The first column header always applies to the frequency label;
|
||||||
|
# you can hide this (or any other column) by setting the header to null
|
||||||
|
- Rarity
|
||||||
|
- Color
|
||||||
|
- Notes
|
||||||
|
# frequencies are optional; by default distribution will be uniform
|
||||||
|
frequencies:
|
||||||
|
# multiple distributions may be specified besides 'default'
|
||||||
|
default:
|
||||||
|
- common: 0.5
|
||||||
|
- uncommon: 0.3
|
||||||
|
- rare: 0.15
|
||||||
|
- wondrous: 0.05
|
||||||
|
# 'common' is the text label for the frequency distribution
|
||||||
|
common:
|
||||||
|
# each time a 'common' value is selected for the table, it will be
|
||||||
|
# chosen at random from the following values
|
||||||
|
- red
|
||||||
|
- orange
|
||||||
|
- yellow
|
||||||
|
uncommon:
|
||||||
|
- green
|
||||||
|
- blue
|
||||||
|
rare:
|
||||||
|
- indigo
|
||||||
|
- violet
|
||||||
|
wondrous:
|
||||||
|
# choices can be definitions; both key and the value will be added as columns
|
||||||
|
- octarine: the color of magic
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
% poetry run roll-table example.yaml
|
||||||
|
┏━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Roll ┃ Rarity ┃ Color ┃ Notes ┃
|
||||||
|
┡━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ d1-d5 │ common │ red │ │
|
||||||
|
│ d6-d10 │ common │ yellow │ │
|
||||||
|
│ d11-d12 │ rare │ indigo │ │
|
||||||
|
│ d13 │ rare │ violet │ │
|
||||||
|
│ d14-d15 │ uncommon │ blue │ │
|
||||||
|
│ d16-d19 │ uncommon │ green │ │
|
||||||
|
│ d20 │ wondrous │ octarine │ the color of magic │
|
||||||
|
└─────────┴──────────┴──────────┴────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
Using Metadata:
|
### Library Use
|
||||||
|
|
||||||
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
|
|
||||||
...
|
|
||||||
|
|
||||||
|
```
|
||||||
|
from rolltable import tables
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
Path('spells.yaml').read_text(),
|
||||||
|
Path('weapons.yaml').read_text(),
|
||||||
|
Path('items.yaml').read_text()
|
||||||
|
]
|
||||||
|
rt = tables.RollTable(sources, die=100)
|
||||||
|
```
|
||||||
|
|
37
example.yaml
Normal file
37
example.yaml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# example.yaml
|
||||||
|
#
|
||||||
|
# This is an annotated example source file for generating random roll tables.
|
||||||
|
#
|
||||||
|
# metadata is optional
|
||||||
|
metadata:
|
||||||
|
# headers are optional
|
||||||
|
headers:
|
||||||
|
# The first column header always applies to the frequency label;
|
||||||
|
# you can hide this (or any other column) by setting the header to null
|
||||||
|
- Rarity
|
||||||
|
- Color
|
||||||
|
- Notes
|
||||||
|
# frequencies are optional; by default distribution will be uniform
|
||||||
|
frequencies:
|
||||||
|
# multiple distributions may be specified besides 'default'
|
||||||
|
default:
|
||||||
|
common: 0.5
|
||||||
|
uncommon: 0.3
|
||||||
|
rare: 0.15
|
||||||
|
wondrous: 0.05
|
||||||
|
# 'common' is the text label for the frequency distribution
|
||||||
|
common:
|
||||||
|
# each time a 'common' value is selected for the table, it will be
|
||||||
|
# chosen at random from the following values
|
||||||
|
- red
|
||||||
|
- orange
|
||||||
|
- yellow
|
||||||
|
uncommon:
|
||||||
|
- green
|
||||||
|
- blue
|
||||||
|
rare:
|
||||||
|
- indigo
|
||||||
|
- violet
|
||||||
|
wondrous:
|
||||||
|
# choices can be definitions; both key and the value will be added as columns
|
||||||
|
- octarine: the color of magic
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = 'dnd-rolltable'
|
name = 'dnd-rolltable'
|
||||||
version = '0.9'
|
version = '1.0'
|
||||||
license = 'The Unlicense'
|
license = 'The Unlicense'
|
||||||
authors = ['Greg Boyington <evilchili@gmail.com>']
|
authors = ['Greg Boyington <evilchili@gmail.com>']
|
||||||
description = 'Generate roll tables using weighted random distributions'
|
description = 'Generate roll tables using weighted random distributions'
|
||||||
|
|
|
@ -2,6 +2,8 @@ from rolltable import tables
|
||||||
import typer
|
import typer
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
@ -9,28 +11,28 @@ app = typer.Typer()
|
||||||
|
|
||||||
@app.command("roll-table")
|
@app.command("roll-table")
|
||||||
def create(
|
def create(
|
||||||
source: str = typer.Argument(
|
sources: List[Path] = typer.Argument(
|
||||||
...,
|
...,
|
||||||
help="Path to the yaml-formatted source file."),
|
help="Path to one or more yaml-formatted source file."),
|
||||||
frequency: str = typer.Option(
|
frequency: str = typer.Option(
|
||||||
'default',
|
'default',
|
||||||
help='use the specified frequency from the source file'),
|
help='use the specified frequency from the source file'),
|
||||||
die: int = typer.Option(
|
die: int = typer.Option(
|
||||||
20,
|
20,
|
||||||
help='The size of the die for which to create a table'),
|
help='The size of the die for which to create a table'),
|
||||||
collapse: bool = typer.Option(
|
collapsed: bool = typer.Option(
|
||||||
True,
|
True,
|
||||||
help='If True, collapse multiple die values with the same option.')
|
help='If True, collapse multiple die values with the same option.')
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
CLI for creating roll tables.
|
CLI for creating roll tables.
|
||||||
"""
|
"""
|
||||||
with open(source, 'r') as src:
|
|
||||||
rt = tables.RollTable(source=src, frequency=frequency, die=die,
|
rt = tables.RollTable([Path(s).read_text() for s in sources], frequency=frequency, die=die)
|
||||||
collapsed=collapse)
|
|
||||||
rt.load_source()
|
rows = rt.rows if collapsed else rt.expanded_rows
|
||||||
table = Table(*rt.rows[0])
|
table = Table(*rows[0])
|
||||||
for row in rt.rows[1:]:
|
for row in rows[1:]:
|
||||||
table.add_row(*row)
|
table.add_row(*row)
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,62 @@
|
||||||
import yaml
|
import yaml
|
||||||
import random
|
import random
|
||||||
from collections.abc import Generator
|
from typing import Optional, List, IO
|
||||||
from typing import Optional, Mapping, List, IO
|
|
||||||
|
|
||||||
|
class DataSource:
|
||||||
|
"""
|
||||||
|
Represents a yaml data source used to generate roll tables.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
source - the IO source to parse
|
||||||
|
frequency - the frequency distribution to apply
|
||||||
|
headers - an array of header strings
|
||||||
|
data - The parsed YAML data
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
|
||||||
|
load_source - Read and parse the source, populating the attributes
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, source: IO, frequency: str = 'default') -> None:
|
||||||
|
"""
|
||||||
|
Initialize a DataSource instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source - an IO object to read source from
|
||||||
|
frequency - the name of the frequency distribution to use; must
|
||||||
|
be defined in the source file's metadata.
|
||||||
|
"""
|
||||||
|
self.source = source
|
||||||
|
self.frequency = frequency
|
||||||
|
self.headers = []
|
||||||
|
self.frequencies = None
|
||||||
|
self.data = None
|
||||||
|
self.load_source()
|
||||||
|
|
||||||
|
def load_source(self) -> None:
|
||||||
|
"""
|
||||||
|
Cache the yaml source and the parsed or generated metadata.
|
||||||
|
"""
|
||||||
|
if self.data:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.data = yaml.safe_load(self.source)
|
||||||
|
metadata = self.data.pop('metadata', {})
|
||||||
|
|
||||||
|
num_keys = len(self.data.keys())
|
||||||
|
default_freq = num_keys / 100
|
||||||
|
|
||||||
|
if 'headers' in metadata:
|
||||||
|
self.headers = metadata['headers']
|
||||||
|
|
||||||
|
frequencies = {
|
||||||
|
'default': dict([(k, default_freq) for k in self.data.keys()])
|
||||||
|
}
|
||||||
|
if 'frequencies' in metadata:
|
||||||
|
frequencies.update(**metadata['frequencies'])
|
||||||
|
self.frequencies = frequencies[self.frequency]
|
||||||
|
|
||||||
|
|
||||||
class RollTable:
|
class RollTable:
|
||||||
|
@ -10,53 +65,33 @@ class RollTable:
|
||||||
|
|
||||||
Instance Attributes:
|
Instance Attributes:
|
||||||
|
|
||||||
data - The parsed source data, minus any metadata
|
sources - One or more yaml strings to parse as data sources
|
||||||
die - the size of the die for which to create a table (default: 20)
|
frequency - The frequency distribution to apply when populating the table
|
||||||
frequencies - frequency distribution applied when selecting random values
|
die - The size of the die for which to create a table (default: 20)
|
||||||
headers - array of column headers (default: do not print headers)
|
headers - An array of header strings
|
||||||
(default: uniform across all options)
|
rows - An array of table headers and rows
|
||||||
rows - An array of table rows derived from the values
|
expanded_rows - An array of table headers and rows, one per die roll value
|
||||||
values - An array of randomly-selected values for each die roll
|
|
||||||
|
|
||||||
Instance Methods:
|
Usage:
|
||||||
|
|
||||||
load_source - Read and parse the source. Will be called automatically when necessary.
|
table = RollTable(['source.yaml'], die=4)
|
||||||
|
print(table)
|
||||||
|
>>> Roll Item
|
||||||
|
d1 Foo
|
||||||
|
d2-d4 Bar
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source: IO, frequency: str = 'default',
|
def __init__(self, sources: List[str], frequency: str = 'default',
|
||||||
die: Optional[int] = 20, collapsed: bool = True):
|
die: Optional[int] = 20) -> None:
|
||||||
"""
|
self._sources = sources
|
||||||
Initialize a RollTable instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source - an IO object to read source from
|
|
||||||
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._frequency = frequency
|
self._frequency = frequency
|
||||||
self._die = die
|
self._die = die
|
||||||
self._collapsed = collapsed
|
|
||||||
self._headers = None
|
|
||||||
self._frequencies = None
|
|
||||||
self._source = source
|
|
||||||
self._data = None
|
self._data = None
|
||||||
self._values = None
|
|
||||||
self._rows = None
|
self._rows = None
|
||||||
|
self._headers = None
|
||||||
@property
|
self._header_excludes = None
|
||||||
def frequencies(self):
|
self._generated_values = None
|
||||||
if not self._data:
|
self._config()
|
||||||
self.load_source()
|
|
||||||
return self._frequencies
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self) -> Mapping:
|
|
||||||
if not self._data:
|
|
||||||
self.load_source()
|
|
||||||
return self._data
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def die(self) -> int:
|
def die(self) -> int:
|
||||||
|
@ -64,163 +99,98 @@ class RollTable:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self) -> List:
|
def headers(self) -> List:
|
||||||
if not self._data:
|
|
||||||
self.load_source()
|
|
||||||
return self._headers
|
return self._headers
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> List:
|
def _values(self) -> List:
|
||||||
if not self._values:
|
if not self._generated_values:
|
||||||
weights = []
|
def values_from_datasource(ds):
|
||||||
options = []
|
weights = []
|
||||||
for (option, weight) in self.frequencies.items():
|
options = []
|
||||||
weights.append(weight)
|
for (option, weight) in ds.frequencies.items():
|
||||||
options.append(option)
|
weights.append(weight)
|
||||||
freqs = random.choices(options, weights=weights, k=self.die)
|
options.append(option)
|
||||||
self._values = []
|
freqs = random.choices(options, weights=weights, k=self.die)
|
||||||
for option in freqs:
|
values = []
|
||||||
self._values += [(option, random.choice(self.data[option]))]
|
for option in freqs:
|
||||||
if hasattr(self._values[0][1], 'keys'):
|
choice = random.choice(ds.data[option])
|
||||||
self._values = sorted(self._values, key=lambda val: list(val[1].keys())[0])
|
if hasattr(choice, 'keys'):
|
||||||
else:
|
c = [option]
|
||||||
self._values = sorted(self._values)
|
for (k, v) in choice.items():
|
||||||
return self._values
|
c.extend([k, v])
|
||||||
|
values.append(c)
|
||||||
|
else:
|
||||||
|
values.append([option, choice])
|
||||||
|
return sorted(values)
|
||||||
|
|
||||||
|
ds_values = [values_from_datasource(t) for t in self._data]
|
||||||
|
|
||||||
|
self._generated_values = []
|
||||||
|
for face in range(self._die):
|
||||||
|
value = []
|
||||||
|
for index, ds in enumerate(ds_values):
|
||||||
|
value += ds_values[index][face]
|
||||||
|
self._generated_values.append(value)
|
||||||
|
return self._generated_values
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rows(self) -> List:
|
def rows(self) -> List:
|
||||||
if not self._rows:
|
def formatted(lastrow, offset, row, i):
|
||||||
rows = []
|
fmt = f'd{i}' if offset + 1 == i else f'd{offset+1}-d{i}'
|
||||||
if self.headers:
|
return self._column_filter([fmt] + lastrow)
|
||||||
rows.append(['Roll'] + self.headers)
|
|
||||||
if self._collapsed:
|
lastrow = None
|
||||||
for line in self._collapsed_rows():
|
offset = 0
|
||||||
rows.append(line)
|
self._rows = [self._column_filter(['Roll'] + self.headers)]
|
||||||
else:
|
for face in range(self._die):
|
||||||
for (i, item) in enumerate(self.values):
|
row = self._values[face]
|
||||||
(cat, option) = item
|
if not lastrow:
|
||||||
if hasattr(option, 'items'):
|
lastrow = row
|
||||||
(k, v) = list(option.items())[0]
|
offset = face
|
||||||
rows.append([f'd{i+1}', cat, k, v])
|
continue
|
||||||
else:
|
if row != lastrow:
|
||||||
rows.append([f'd{i+1}', cat, option])
|
self._rows.append(formatted(lastrow, offset, row, face))
|
||||||
self._rows = rows
|
lastrow = row
|
||||||
|
offset = face
|
||||||
|
self._rows.append(formatted(lastrow, offset, row, face+1))
|
||||||
return self._rows
|
return self._rows
|
||||||
|
|
||||||
def load_source(self) -> None:
|
@property
|
||||||
|
def expanded_rows(self) -> List:
|
||||||
|
self._rows = [self._column_filter(['Roll'] + self.headers)]
|
||||||
|
for face in range(self._die):
|
||||||
|
row = self._values[face]
|
||||||
|
self._rows.append(self._column_filter([f'd{face+1}'] + row))
|
||||||
|
return self._rows
|
||||||
|
|
||||||
|
def _config(self):
|
||||||
"""
|
"""
|
||||||
Cache the yaml source and the parsed or generated metadata.
|
Parse data sources, generate headers, and create the column filters
|
||||||
"""
|
"""
|
||||||
if self._data:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._data = yaml.safe_load(self._source)
|
# create the datasource objects
|
||||||
metadata = self._data.pop('metadata', {})
|
self._data = []
|
||||||
|
for src in self._sources:
|
||||||
|
ds = DataSource(src, frequency=self._frequency)
|
||||||
|
ds.load_source()
|
||||||
|
self._data.append(ds)
|
||||||
|
|
||||||
num_keys = len(self._data.keys())
|
# merge the headers
|
||||||
default_freq = num_keys / 100
|
self._headers = []
|
||||||
|
for ds in self._data:
|
||||||
|
self._headers += ds.headers
|
||||||
|
|
||||||
if 'headers' in metadata:
|
# identify which columsn to hide in the output by recording where a
|
||||||
self._headers = metadata['headers']
|
# None header appears
|
||||||
|
self._header_excludes = []
|
||||||
|
for i in range(len(self._headers)):
|
||||||
|
if self.headers[i] is None:
|
||||||
|
self._header_excludes.append(i+1) # +1 to account for the 'Roll' column
|
||||||
|
|
||||||
frequencies = {
|
def _column_filter(self, row):
|
||||||
'default': dict([(k, default_freq) for k in self._data.keys()])
|
return [col for (pos, col) in enumerate(row) if pos not in self._header_excludes]
|
||||||
}
|
|
||||||
if 'frequencies' in metadata:
|
|
||||||
frequencies.update(**metadata['frequencies'])
|
|
||||||
self._frequencies = frequencies[self._frequency]
|
|
||||||
|
|
||||||
def _collapsed_rows(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
|
|
||||||
if hasattr(option, 'items'):
|
|
||||||
(k, v) = list(*option.items())
|
|
||||||
else:
|
|
||||||
k = option
|
|
||||||
v = ''
|
|
||||||
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)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
|
||||||
Return the rows as a single string.
|
|
||||||
"""
|
|
||||||
rows = list(self.rows)
|
rows = list(self.rows)
|
||||||
str_format = '\t'.join(['{:10s}'] * len(rows[0]))
|
str_format = '\t'.join(['{:10s}'] * len(rows[0]))
|
||||||
return "\n".join([str_format.format(*row) for row in rows])
|
return "\n".join([str_format.format(*row) for row in rows])
|
||||||
|
|
||||||
|
|
||||||
class CombinedTable(RollTable):
|
|
||||||
"""
|
|
||||||
Create a table that is a union of other tables.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tables: List[str], die: Optional[int] = 20):
|
|
||||||
self._die = die
|
|
||||||
self._tables = tables
|
|
||||||
self._rows = None
|
|
||||||
self._headers = None
|
|
||||||
|
|
||||||
# reset any cached values
|
|
||||||
for t in self._tables:
|
|
||||||
t._rows = None
|
|
||||||
t._values = None
|
|
||||||
t._collapsed = False
|
|
||||||
t._die = self._die
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tables(self) -> List:
|
|
||||||
return self._tables
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rows(self) -> List:
|
|
||||||
"""
|
|
||||||
Compute the rows of the table by concatenating the rows of the individual tables.
|
|
||||||
"""
|
|
||||||
if not self._rows:
|
|
||||||
|
|
||||||
# if one table has headers, they must all have them, so fill with empty strings.
|
|
||||||
if sum([1 for t in self.tables if t.headers]) < len(self.tables):
|
|
||||||
for t in self.tables:
|
|
||||||
if not t.headers:
|
|
||||||
t._headers = ['.'] * len(t.values[0])
|
|
||||||
|
|
||||||
self._rows = []
|
|
||||||
for i in range(self._die):
|
|
||||||
row = [self.tables[0].rows[i][0]]
|
|
||||||
for x in range(len(self.tables)):
|
|
||||||
for col in self.tables[x].rows[i][1:]:
|
|
||||||
row.append(col)
|
|
||||||
self._rows.append(row)
|
|
||||||
return self._rows
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
print(RollTable(path=sys.argv[1], die=int(sys.argv[2])))
|
|
||||||
|
|
|
@ -46,6 +46,10 @@ option 1:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fixture_no_descriptions = """
|
fixture_no_descriptions = """
|
||||||
|
metadata:
|
||||||
|
headers:
|
||||||
|
- option
|
||||||
|
- choice
|
||||||
option 1:
|
option 1:
|
||||||
- choice 1
|
- choice 1
|
||||||
"""
|
"""
|
||||||
|
@ -80,45 +84,41 @@ B3:
|
||||||
|
|
||||||
|
|
||||||
def test_combined_tables():
|
def test_combined_tables():
|
||||||
tA = tables.RollTable(fixture_combined_A)
|
combined = tables.RollTable([fixture_combined_A, fixture_combined_B], die=6)
|
||||||
tB = tables.RollTable(fixture_combined_B)
|
assert str(combined)
|
||||||
|
|
||||||
combined = tables.CombinedTable(tables=[tA, tB], die=6)
|
|
||||||
assert 'A1' in str(combined)
|
|
||||||
assert 'B1' in str(combined)
|
|
||||||
|
|
||||||
|
|
||||||
def test_table_end_to_end():
|
def test_table_end_to_end():
|
||||||
assert str(tables.RollTable(fixture_source))
|
assert str(tables.RollTable([fixture_source]))
|
||||||
|
|
||||||
|
|
||||||
def test_table_end_to_end_with_metadata():
|
def test_table_end_to_end_with_metadata():
|
||||||
assert str(tables.RollTable(fixture_metadata + fixture_source))
|
assert str(tables.RollTable([fixture_metadata + fixture_source]))
|
||||||
|
|
||||||
|
|
||||||
def test_table_frequency():
|
def test_table_frequency():
|
||||||
t = tables.RollTable(fixture_metadata + fixture_source, frequency='nondefault')
|
t = tables.RollTable([fixture_metadata + fixture_source], frequency='nondefault')
|
||||||
assert t.frequencies['Option 1'] == 0.0
|
assert t._data[0].frequencies['Option 1'] == 0.0
|
||||||
assert t.frequencies['Option 2'] == 0.1
|
assert t._data[0].frequencies['Option 2'] == 0.1
|
||||||
assert t.frequencies['Option 3'] == 0.9
|
assert t._data[0].frequencies['Option 3'] == 0.9
|
||||||
|
|
||||||
|
|
||||||
def test_one_option():
|
def test_one_option():
|
||||||
t = tables.RollTable(fixture_one_choice, die=1)
|
t = tables.RollTable([fixture_one_choice], die=1)
|
||||||
assert t.values == [('option 1', {'choice 1': 'description 1'})]
|
assert t._values == [['option 1', 'choice 1', 'description 1']]
|
||||||
|
|
||||||
|
|
||||||
def test_collapsed():
|
def test_collapsed():
|
||||||
t = tables.RollTable(fixture_repeated_choices, die=6, collapsed=True)
|
t = tables.RollTable([fixture_repeated_choices], die=6)
|
||||||
assert len(list(t.rows)) == 1
|
assert len(list(t.rows)) == 2 # (+1 for headers)
|
||||||
|
|
||||||
|
|
||||||
def test_not_collapsed():
|
def test_not_collapsed():
|
||||||
t = tables.RollTable(fixture_repeated_choices, die=6, collapsed=False)
|
t = tables.RollTable([fixture_repeated_choices], die=6)
|
||||||
assert len(list(t.rows)) == 6
|
assert len(list(t.expanded_rows)) == 7 # (+1 for headers)
|
||||||
|
|
||||||
|
|
||||||
def test_no_descriptions():
|
def test_no_descriptions():
|
||||||
t = tables.RollTable(fixture_no_descriptions, die=1)
|
t = tables.RollTable([fixture_no_descriptions], die=1)
|
||||||
assert 'd1' in str(t)
|
assert 'd1' in str(t)
|
||||||
assert 'option 1' in str(t)
|
assert 'option 1' in str(t)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user