def generate(table_name, num_rolls=1): """ Generate N items from the given table. Assumes DATA_DIR has been set. Return a list of item names (str). """ trace('generate') table = load_table(table_name) if table is None: raise ValueError('Invalid table: ' + table_name) values = list() if table['type'] == 'items': for i in range(num_rolls): item_name = lookup_items(table) values.append(item_name) elif table['type'] == 'table-list': for i in range(num_rolls): item_list = lookup_table_list(table) values.extend(item_list) elif table['type'] == 'tables': for i in range(num_rolls): item_name = lookup_tables(table) values.append(item_name) else: raise ValueError('Unknown table type: ' + table['type']) return values
def roll_quantity(quantity): """ Roll some dice "3d6+6x10" -> ((3 x 1-6) + 6) x 10 -> 160 """ trace("roll_quantity") result = MATCHER.fullmatch(quantity) if result is None: debug('no match', quantity) value = quantity else: groups = result.groups() number = int(groups[0]) sides = int(groups[1]) if groups[2] is not None: adjustment = int(groups[2]) else: adjustment = 0 if groups[3] is not None: multiplier = int(groups[3]) else: multiplier = 1 debug('number', number, 'sides', sides, 'adjustment', adjustment, 'multiplier', multiplier) n = 0 for i in range(number): n += randint(1, sides) value = (n + adjustment) * multiplier debug('value', value) return str(value)
def set_data_dir(data_dir): """ Set the data directory (DATA_DIR) Raises FileNotFoundError """ trace('set_data_dir') data_dir = os.path.realpath(data_dir) if os.path.isdir(data_dir): debug('Setting DATA_DIR to', data_dir) global DATA_DIR DATA_DIR = data_dir else: raise FileNotFoundError(data_dir)
def list_tables(*args): """ Return a list of tables in DATA_DIR. Any file in DATA_DIR with a json or yaml extension is considered to be a table. """ trace('list_tables') files = list() for file_name in os.listdir(DATA_DIR): parts = file_name.split('.') if parts[-1] == 'json' or parts[-1] == 'yaml': files.append('.'.join(parts[:-1])) return files
def lookup_tables(table): """ Select a table from a tables table. Returns an item name. """ trace('lookup_tables') debug('lookup_tables', table['name']) #import pdb; pdb.set_trace() row = random_row(table) table_name = row['table'] subtable = load_table(table_name) if subtable['type'] == 'items': return lookup_items(subtable) elif subtable['type'] == 'tables': return lookup_tables(subtable)
def get_chance(column): """ Get the optional chance value from a table-list column. Default is 100, if not present. :param column: dict :return: number in [1, 100] """ trace('get_chance') value = 100 if 'chance' in column: chance = column['chance'] if chance > 0 or chance <= 100: value = chance else: ValueError('column chance out of range: {}'.format(chance)) return value
def random_row(table): """ Return a random row dict from a table. """ trace('random_row') total_weight = table['total-weight'] item_index = randint(1, total_weight) weight_total = 0 for row in table['rows']: row_weight = row['weight'] if 'weight' in row else 1 weight_total += row_weight if item_index <= weight_total: return row raise RuntimeError( 'row index out of bounds: index {}, total_weight {}'.format( item_index, total_weight))
def lookup_table_list(table): """ The top-level table is a list of tables. Roll once on each table in the list. Returns a list of item names. Each table item has: name: the name of the item or table chance: roll less than or equal to this value for item to be generated table: if None, use "quantity + name", else roll on table quantity: number of items (bunch) or rolls on table """ trace('lookup_table_list') debug('table name', table['name']) values = list() columns = table['columns'] for column in columns: chance = get_chance(column) roll = randint(1, 100) if roll > chance: debug('skip', column['name'], roll, 'vs', column['chance']) continue # failed chance roll if column['table'] is None: quantity = roll_quantity(column['quantity']) name = column['name'] values.append('{} {}'.format(quantity, name)) else: quantity = roll_quantity(column['quantity']) table_name = column['table'] for i in range(int(quantity)): debug('rolling on table', table_name) subtable = load_table(table_name) if subtable['type'] == 'items': item_name = lookup_items(subtable) values.append(item_name) elif subtable['type'] == 'tables': item_name = lookup_tables(subtable) values.append(item_name) elif subtable['type'] == 'table-list': item_list = lookup_table_list(subtable) values.extend(item_list) else: raise ValueError('invalid table type: ' + subtable['type']) return values
def check_weights(table): """ Check that row weights add up to total-weight. Raise ValueError if not equal """ trace('check_weights') if 'total-weight' in table: total_weight = table['total-weight'] weight_total = 0 for row in table['rows']: row_weight = row['weight'] if 'weight' in row else 1 weight_total += row_weight debug('table', table['name'], 'weight', weight_total, 'of', total_weight) if total_weight != weight_total: raise ValueError("{}: row weights don't add up: {} of {}".format( table['name'], weight_total, total_weight)) else: debug('not a weighted table', table['name'])
def lookup_items(table): """ Do a lookup in an 'items' table, return the result. Returns a string. Raises ValueError. """ trace('lookup_item') debug('lookup_item', table['name'], table['total-weight'], len(table['rows'])) item = random_row(table) item_name = item['name'] subitem = None quantity = None if 'subtable' in item: debug('roll on subtable', item['subtable']) subtable = load_table(item['subtable']) if subtable['type'] == 'items': subitem = lookup_items(subtable) elif subtable['type'] == 'tables': subitem = lookup_tables(subtable) else: raise ValueError('invalid table type: ' + subtable['type']) # if len(subitem) == 0: # subitem = None if 'quantity' in item: debug('roll quantity', item['quantity']) quantity = roll_quantity(item['quantity']) if quantity == "1": quantity = None elif 'value' in item: # quantity and value are mutually exclusive assert (quantity is None) quantity = roll_quantity(item['value']) + " gp" if subitem is not None and quantity is not None: item_name = '{} ({}, {})'.format(item['name'], subitem, quantity) elif subitem is not None: item_name = '{} ({})'.format(item['name'], subitem) elif quantity is not None: item_name = '{} ({})'.format(item['name'], quantity) else: item_name = '{}'.format(item['name']) return item_name
def is_table(table_name=None): """ Search DATA_DIR for a file called 'table_name.json' or 'table_name.yaml'. Returns True if found, False is not """ trace('is_table') if table_name is None: raise ValueError('table_name is None') for file_name in os.listdir(DATA_DIR): debug('file_name', file_name) if file_name == table_name + '.yaml': debug('found: ', DATA_DIR, '/', file_name) return True if file_name == table_name + '.json': debug('found: ', DATA_DIR + '/' + file_name) return True debug('not found: ', table_name, 'in', DATA_DIR) return False
def load_table(table_name): """ Load a table, cache the result. Returns a table (dict) Raises ValueError """ global _tables trace('load_table') table = None if table_name in _tables: table = _tables[table_name] debug('cache hit', table_name) else: table = tablator.data.load(table_name) check_weights(table) _tables[table_name] = table debug('loaded', table_name) return table
def load(table_name=None): """ Load a table from DATA_DIR, return Python. """ trace('load') if table_name is None: raise ValueError('table_name is None') table_file = os.path.join(DATA_DIR, table_name + '.yaml') if os.path.exists(table_file): import yaml with open(table_file, 'r') as f: return yaml.load(f) table_file = os.path.join(DATA_DIR, table_name + '.json') if os.path.exists(table_file): import json with open(table_file, 'r') as f: return json.load(f) raise ValueError("Table not found: " + table_name)
def print_plain(table_name): """ Print a table as plain text to the standard output. """ trace('print_plain') table = load_table(table_name) index = 1 if table['type'] == 'items': # A table of items debug('print "items" table') dice = str(table['total-weight']) if dice == '100': dice = '%' print(' d{}'.format(dice), table['name'], sep='\t') print('-----', '-' * len(table['name']), sep='\t') # TODO Include quantity, if present for row in table['rows']: wt = row['weight'] if 'weight' in row else 1 s = None if wt > 1: s = "{:02d}-{:02d}\t{}".format(index, index + wt - 1, row['name']) else: s = "{:02d}\t{}".format(index, row['name']) index += wt print(s) print() elif table['type'] == 'tables': # A table of tables debug('print "tables" table') dice = str(table['total-weight']) if dice == '100': dice = '%' print(' d{}'.format(dice), table['name'], sep='\t') print('-----', '-' * len(table['name']), sep='\t') for row in table['rows']: table_name = get_table_name(row['table']) wt = row['weight'] if 'weight' in row else 1 s = None if wt > 1: s = "{:02d}-{:02d}\t{}".format(index, index + wt - 1, table_name) else: s = "{:02d}\t{}".format(index, table_name) index += wt print(s) print() elif table['type'] == 'table-list': # A list of tables, roll on each table debug('print "table-list" table') print(table['name']) print('-' * len(table['name'])) for column in table['columns']: chance = '{0:3d}%'.format(get_chance(column)) quantity = column['quantity'] name = column['name'] if column['table'] is None: print(chance, quantity, name) else: table_name = get_table_name(column['table']) print(chance, quantity, table_name) print() else: debug('unknown table type', table['type'])