From 5b4c26ecf24083e767a0788ea3af300ca79bb113 Mon Sep 17 00:00:00 2001 From: evilchili Date: Fri, 29 Dec 2023 19:24:26 -0800 Subject: [PATCH] automatic formatting --- dnd_item/cli.py | 44 ++++++--------- dnd_item/five_e.py | 113 +++++++++++++++++++------------------- dnd_item/types.py | 130 +++++++++++++++++++------------------------- dnd_item/weapons.py | 106 +++++++++++++++++++----------------- 4 files changed, 186 insertions(+), 207 deletions(-) diff --git a/dnd_item/cli.py b/dnd_item/cli.py index e456a65..f797ae7 100644 --- a/dnd_item/cli.py +++ b/dnd_item/cli.py @@ -1,33 +1,31 @@ import logging import os - from enum import Enum from pathlib import Path import typer - from rich import print -from rich.logging import RichHandler from rich.console import Console +from rich.logging import RichHandler from rich.table import Table +from dnd_item import five_e from dnd_item.types import RollTable from dnd_item.weapons import WeaponGenerator -from dnd_item import five_e app = typer.Typer() app_state = {} class OUTPUT_FORMATS(Enum): - text = 'text' - yaml = 'yaml' - markdown = 'markdown' + text = "text" + yaml = "yaml" + markdown = "markdown" @app.callback() def main( - cr: int = typer.Option(default=None, help='The Challenge Rating to use when determining rarity.'), + cr: int = typer.Option(default=None, help="The Challenge Rating to use when determining rarity."), ): debug = os.getenv("FANITEM_DEBUG", None) logging.basicConfig( @@ -35,38 +33,32 @@ def main( level=logging.DEBUG if debug else logging.INFO, handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])], ) - logging.getLogger('markdown_it').setLevel(logging.ERROR) + logging.getLogger("markdown_it").setLevel(logging.ERROR) - app_state['cr'] = cr or 0 - app_state['data'] = Path(__file__).parent / Path("sources") + app_state["cr"] = cr or 0 + app_state["data"] = Path(__file__).parent / Path("sources") @app.command() def weapon(count: int = typer.Option(1, help="The number of weapons to generate.")): console = Console() - for weapon in WeaponGenerator().random(count=count, challenge_rating=app_state['cr']): + for weapon in WeaponGenerator().random(count=count, challenge_rating=app_state["cr"]): console.print(weapon.details) @app.command("roll-table") def table( - die: int = typer.Option( - 20, - help='The size of the die for which to create a table'), + die: int = typer.Option(20, help="The size of the die for which to create a table"), hide_rolls: bool = typer.Option( False, - help='If True, do not show the Roll column.', + help="If True, do not show the Roll column.", ), - collapsed: bool = typer.Option( - True, - help='If True, collapse multiple die values with the same option.'), - width: int = typer.Option( - 180, - help='Width of the table.'), + collapsed: bool = typer.Option(True, help="If True, collapse multiple die values with the same option."), + width: int = typer.Option(180, help="Width of the table."), output: OUTPUT_FORMATS = typer.Option( - 'text', - help='The output format to use.', - ) + "text", + help="The output format to use.", + ), ): """ CLI for creating roll tables of randomly-generated items. @@ -75,7 +67,7 @@ def table( sources=[WeaponGenerator], die=die, hide_rolls=hide_rolls, - challenge_rating=app_state['cr'], + challenge_rating=app_state["cr"], ) if output == OUTPUT_FORMATS.yaml: diff --git a/dnd_item/five_e.py b/dnd_item/five_e.py index cff09a1..7b02a8e 100644 --- a/dnd_item/five_e.py +++ b/dnd_item/five_e.py @@ -1,46 +1,31 @@ import json -import yaml - from collections import defaultdict - -from random_sets.datasources import DataSource - from pathlib import Path +import yaml +from random_sets.datasources import DataSource + sources = Path(__file__).parent / Path("sources") -RARITY = { - 'unknown': 'common', - 'none': 'common', - '': '' -} +RARITY = {"unknown": "common", "none": "common", "": ""} -TYPE = { - 'M': 'martial', - 'R': 'ranged', - '': '' -} +TYPE = {"M": "martial", "R": "ranged", "": ""} -DAMAGE = { - 'S': 'Slashing', - 'P': 'Piercing', - 'B': 'Bludgeoning', - '': '' -} +DAMAGE = {"S": "Slashing", "P": "Piercing", "B": "Bludgeoning", "": ""} PROPERTIES = { - 'F': 'finesse', - 'AF': 'firearm', - 'A': 'ammmunition', - 'T': 'thrown', - 'L': 'light', - '2H': 'two-handed', - 'V': 'versatile', - 'RLD': 'reload', - 'LD': 'loading', - 'S': 'special', - 'H': 'heavy', - 'R': 'reach', + "F": "finesse", + "AF": "firearm", + "A": "ammmunition", + "T": "thrown", + "L": "light", + "2H": "two-handed", + "V": "versatile", + "RLD": "reload", + "LD": "loading", + "S": "special", + "H": "heavy", + "R": "reach", } @@ -49,42 +34,56 @@ class Weapons(DataSource): A rolltables data source backed by a 5e.tools json data file. used to convert the 5e.tools data to the yaml format consumed by dnd-rolltables. """ + def read_source(self) -> None: - src = json.load(self.source)['baseitem'] + src = json.load(self.source)["baseitem"] self.data = defaultdict(list) - headers = ['Rarity', 'Name', 'Category', 'Type', 'Weight', 'Damage Type', - 'Damage Dice', 'Range', 'Reload', 'Value', 'Properties'] + headers = [ + "Rarity", + "Name", + "Category", + "Type", + "Weight", + "Damage Type", + "Damage Dice", + "Range", + "Reload", + "Value", + "Properties", + ] for item in src: - if not item.get('weapon', False): + if not item.get("weapon", False): continue - if item.get('age', False): + if item.get("age", False): continue - rarity = RARITY.get(item['rarity'], 'Common').capitalize() - itype = TYPE.get(item['type'], '_unknown').capitalize() - properties = ', '.join([PROPERTIES[p] for p in item.get('property', [])]) + rarity = RARITY.get(item["rarity"], "Common").capitalize() + itype = TYPE.get(item["type"], "_unknown").capitalize() + properties = ", ".join([PROPERTIES[p] for p in item.get("property", [])]) - self.data[rarity].append({ - item['name'].capitalize(): [ - item['weaponCategory'], - itype, - str(item.get('weight', 0)), - DAMAGE.get(item.get('dmgType', '')), - item.get('dmg1', None), - item.get('range', None), - str(item.get('reload', '')), - str(item.get('value', '')), - properties, - ] - }) - self.metadata = {'headers': headers} + self.data[rarity].append( + { + item["name"].capitalize(): [ + item["weaponCategory"], + itype, + str(item.get("weight", 0)), + DAMAGE.get(item.get("dmgType", "")), + item.get("dmg1", None), + item.get("range", None), + str(item.get("reload", "")), + str(item.get("value", "")), + properties, + ] + } + ) + self.metadata = {"headers": headers} @property def as_yaml(self) -> str: - return yaml.dump({'metadata': self.metadata}) + yaml.dump(dict(self.data)) + return yaml.dump({"metadata": self.metadata}) + yaml.dump(dict(self.data)) -def weapons(source_path: str = 'items-base.json') -> dict: +def weapons(source_path: str = "items-base.json") -> dict: with open(sources / Path(source_path)) as filehandle: ds = Weapons(source=filehandle) return ds diff --git a/dnd_item/types.py b/dnd_item/types.py index d03d638..4442294 100644 --- a/dnd_item/types.py +++ b/dnd_item/types.py @@ -1,29 +1,26 @@ -import re import logging - -from pathlib import Path +import re from collections.abc import Mapping from dataclasses import dataclass, field - -from random_sets.sets import WeightedSet, DataSourceSet +from pathlib import Path import rolltable.types - +from random_sets.sets import DataSourceSet, WeightedSet # Create DataSourceSets, which are WeightedSets populated with DataSource # objects generated from yaml data files. These are used to supply default # values to item generators; see below. sources = Path(__file__).parent / Path("sources") -ENCHANTMENT = DataSourceSet(sources / Path('magic_damage_types.yaml')) -WEAPON_TYPES = DataSourceSet(sources / Path('weapons.yaml')) -RARITY = DataSourceSet(sources / Path('rarity.yaml')) +ENCHANTMENT = DataSourceSet(sources / Path("magic_damage_types.yaml")) +WEAPON_TYPES = DataSourceSet(sources / Path("weapons.yaml")) +RARITY = DataSourceSet(sources / Path("rarity.yaml")) PROPERTIES_BY_RARITY = { - 'base': DataSourceSet(sources / Path('properties_base.yaml')), - 'common': DataSourceSet(sources / Path('properties_common.yaml')), - 'uncommon': DataSourceSet(sources / Path('properties_uncommon.yaml')), - 'rare': DataSourceSet(sources / Path('properties_rare.yaml')), - 'very rare': DataSourceSet(sources / Path('properties_very_rare.yaml')), - 'legendary': DataSourceSet(sources / Path('properties_legendary.yaml')), + "base": DataSourceSet(sources / Path("properties_base.yaml")), + "common": DataSourceSet(sources / Path("properties_common.yaml")), + "uncommon": DataSourceSet(sources / Path("properties_uncommon.yaml")), + "rare": DataSourceSet(sources / Path("properties_rare.yaml")), + "very rare": DataSourceSet(sources / Path("properties_very_rare.yaml")), + "legendary": DataSourceSet(sources / Path("properties_legendary.yaml")), } @@ -57,14 +54,14 @@ class AttributeDict(Mapping): """ attrs = {} for k, v in sorted(kwargs.items()): - attrs[k] = AttributeDict.from_dict(v)if type(v) is dict else v + attrs[k] = AttributeDict.from_dict(v) if type(v) is dict else v return cls(attributes=attrs) @dataclass class Item(AttributeDict): - """ - """ + """ """ + _name: str = None @property @@ -76,7 +73,7 @@ class Item(AttributeDict): @property def description(self): - desc = "\n".join([k.title() + ". " + v.description for k, v in self.get('properties', {}).items()]) + desc = "\n".join([k.title() + ". " + v.description for k, v in self.get("properties", {}).items()]) return desc.format(**self) @classmethod @@ -88,13 +85,12 @@ class Item(AttributeDict): # delay processing the 'properties' attribute until after the other # attributes, because they may contain references to those attributes. - properties = attrs.pop('properties', None) + properties = attrs.pop("properties", None) attributes = dict() # recursively locate and populate template strings def _format(obj, this=None): - # enables use of the 'this' keyword to refer to the current context # in a template. Refer to the enchantment sources for an example. if this: @@ -102,9 +98,7 @@ class Item(AttributeDict): # dicts and lists are descended into if type(obj) is dict: - return AttributeDict.from_dict(dict( - (key, _format(val, this=obj)) for key, val in obj.items() - )) + return AttributeDict.from_dict(dict((key, _format(val, this=obj)) for key, val in obj.items())) if type(obj) is list: return [_format(o, this=this) for o in obj] @@ -124,32 +118,29 @@ class Item(AttributeDict): else: attributes[k] = _format(v) if properties: - attributes['properties'] = AttributeDict.from_dict(_format(properties)) - for prop in attributes['properties'].values(): - overrides = [k for k in prop.attributes.keys() if k.startswith('override_')] + attributes["properties"] = AttributeDict.from_dict(_format(properties)) + for prop in attributes["properties"].values(): + overrides = [k for k in prop.attributes.keys() if k.startswith("override_")] for o in overrides: if prop.attributes[o]: - attributes[o.replace('override_', '')] = prop.attributes[o] + attributes[o.replace("override_", "")] = prop.attributes[o] # store the item name as the _name attribute; it is accessable directly, or # via the name property. This makes overriding the name convenient for subclassers, # which may require naming semantics that cannot be resolved at instantiation time. - _name = attributes['name'] - del attributes['name'] + _name = attributes["name"] + del attributes["name"] # At this point, attributes is a dictionary with members of multiple # types, but every dict member has been converted to an AttributeDict, # and all template strings in the object have been formatted. Return an # instance of the Item class using these formatted attributes. - return cls( - _name=_name, - attributes=attributes - ) + return cls(_name=_name, attributes=attributes) class ItemGenerator: - """ - """ + """ """ + item_class = Item def __init__(self, bases: WeightedSet, rarity: WeightedSet, properties_by_rarity: dict): @@ -159,19 +150,16 @@ class ItemGenerator: def _property_count_by_rarity(self, rarity: str) -> int: property_count_by_rarity = { - 'common': WeightedSet((1, 0.1), (0, 1.0)), - 'uncommon': WeightedSet((1, 1.0)), - 'rare': WeightedSet((1, 1.0), (2, 0.5)), - 'very rare': WeightedSet((1, 0.5), (2, 1.0)), - 'legendary': WeightedSet((2, 1.0), (3, 1.0)), + "common": WeightedSet((1, 0.1), (0, 1.0)), + "uncommon": WeightedSet((1, 1.0)), + "rare": WeightedSet((1, 1.0), (2, 0.5)), + "very rare": WeightedSet((1, 0.5), (2, 1.0)), + "legendary": WeightedSet((2, 1.0), (3, 1.0)), } - return min( - property_count_by_rarity[rarity].random(), - len(self.properties_by_rarity[rarity].members) - ) + return min(property_count_by_rarity[rarity].random(), len(self.properties_by_rarity[rarity].members)) def get_requirements(self, item) -> set: - pat = re.compile(r'{([^\.\}]+)') + pat = re.compile(r"{([^\.\}]+)") def getreqs(obj): if type(obj) is dict: @@ -188,28 +176,28 @@ class ItemGenerator: def random_properties(self) -> dict: item = self.bases.random() - item['rarity'] = self.rarity.random() + item["rarity"] = self.rarity.random() properties = {} - num_properties = self._property_count_by_rarity(item['rarity']['rarity']) + num_properties = self._property_count_by_rarity(item["rarity"]["rarity"]) while len(properties) != num_properties: - thisprop = self.properties_by_rarity[item['rarity']['rarity']].random() - properties[thisprop['name']] = thisprop + thisprop = self.properties_by_rarity[item["rarity"]["rarity"]].random() + properties[thisprop["name"]] = thisprop # add properties from the base item (versatile, thrown, artifact..) - for name in item.pop('properties', '').split(','): + for name in item.pop("properties", "").split(","): name = name.strip() if name: - properties[name] = self.properties_by_rarity['base'].source.as_dict()[name] + properties[name] = self.properties_by_rarity["base"].source.as_dict()[name] - item['properties'] = properties + item["properties"] = properties # look for template strings that reference item attributes which do not yet exist. # Add anything that is missing via a callback. - predefined = list(item.keys()) + ['this', '_name'] + predefined = list(item.keys()) + ["this", "_name"] for requirement in [r for r in self.get_requirements(item) if r not in predefined]: try: - item[requirement] = getattr(self, f'get_{requirement}')(**item) + item[requirement] = getattr(self, f"get_{requirement}")(**item) except AttributeError: logging.error("{item['name']} requires {self.__class__.__name__} to have a get_{requirement}() method.") raise @@ -225,15 +213,15 @@ class ItemGenerator: # select the appropriate frequency distributionnb ased on the specified # challenge rating. By default, all rarities are weighted equally. if challenge_rating in range(1, 5): - frequency = '1-4' + frequency = "1-4" elif challenge_rating in range(5, 11): - frequency = '5-10' + frequency = "5-10" elif challenge_rating in range(11, 17): - frequency = '11-16' + frequency = "11-16" elif challenge_rating >= 17: - frequency = '17' + frequency = "17" else: - frequency = 'default' + frequency = "default" self.rarity.set_frequency(frequency) items = [] @@ -250,14 +238,8 @@ class GeneratorSource: def random_values(self, count: int = 1) -> list: vals = sorted( ( - item.rarity['sort_order'], - [ - item.name, - item.rarity['rarity'], - item.summary, - ', '.join(item.get('properties', [])), - item.id - ] + item.rarity["sort_order"], + [item.name, item.rarity["rarity"], item.summary, ", ".join(item.get("properties", [])), item.id], ) for item in self.generator.random(count=count, challenge_rating=self.cr) ) @@ -275,7 +257,7 @@ class RollTable(rolltable.types.RollTable): self._cr = challenge_rating super().__init__( sources=sources, - frequency='default', + frequency="default", die=die, hide_rolls=hide_rolls, ) @@ -286,10 +268,10 @@ class RollTable(rolltable.types.RollTable): self._data.append(GeneratorSource(generator=src(), cr=self._cr)) self._headers = [ - 'Name', - 'Rarity', - 'Summary', - 'Properties', - 'ID', + "Name", + "Rarity", + "Summary", + "Properties", + "ID", ] self._header_excludes = [] diff --git a/dnd_item/weapons.py b/dnd_item/weapons.py index 3507310..09d4973 100644 --- a/dnd_item/weapons.py +++ b/dnd_item/weapons.py @@ -1,20 +1,19 @@ -import random import base64 import hashlib - +import random from functools import cached_property -from dnd_item import types from random_sets.sets import WeightedSet, equal_weights +from dnd_item import types + def random_from_csv(csv: str) -> str: - return random.choice(csv.split(',')).strip() + return random.choice(csv.split(",")).strip() class Weapon(types.Item): - """ - """ + """ """ def _descriptors(self) -> tuple: """ @@ -22,30 +21,32 @@ class Weapon(types.Item): """ nouns = dict() adjectives = dict() - if not hasattr(self, 'properties'): + if not hasattr(self, "properties"): return (nouns, adjectives) for prop_name, prop in self.properties.items(): - if hasattr(prop, 'nouns'): - nouns[prop_name] = equal_weights(prop.nouns.split(','), blank=False) - if hasattr(prop, 'adjectives'): - adjectives[prop_name] = equal_weights(prop.adjectives.split(','), blank=False) + if hasattr(prop, "nouns"): + nouns[prop_name] = equal_weights(prop.nouns.split(","), blank=False) + if hasattr(prop, "adjectives"): + adjectives[prop_name] = equal_weights(prop.adjectives.split(","), blank=False) return (nouns, adjectives) def _name_template(self, with_adjectives: bool, with_nouns: bool) -> str: num_properties = len(self.properties) options = [] if with_nouns and not with_adjectives: - options.append(('{name} of {nouns}', 0.5)) + options.append(("{name} of {nouns}", 0.5)) if with_adjectives and not with_nouns: - options.append(('{adjectives} {name}', 0.5)) + options.append(("{adjectives} {name}", 0.5)) if with_nouns and with_adjectives: if num_properties == 1: - options.append(('{adjectives} {name} of {nouns}', 1.0)) + options.append(("{adjectives} {name} of {nouns}", 1.0)) elif num_properties > 1: - options.extend([ - ('{adjectives} {name} of {nouns}', 1.0), - ('{name} of {adjectives} {nouns}', 0.5), - ]) + options.extend( + [ + ("{adjectives} {name} of {nouns}", 1.0), + ("{name} of {adjectives} {nouns}", 0.5), + ] + ) return WeightedSet(*options).random() def _random_descriptors(self): @@ -64,7 +65,6 @@ class Weapon(types.Item): seen_nouns = dict() for prop_name in set(list(nouns.keys()) + list(adjectives.keys())): - if prop_name in nouns and prop_name not in adjectives: if prop_name not in seen_nouns: add_word(prop_name, random_nouns, nouns) @@ -89,8 +89,8 @@ class Weapon(types.Item): add_word(prop_name, random_nouns, nouns) add_word(prop_name, random_adjectives, adjectives) - random_nouns = ' and '.join(random_nouns) - random_adjectives = ' '.join(random_adjectives) + random_nouns = " and ".join(random_nouns) + random_adjectives = " ".join(random_adjectives) return (random_nouns, random_adjectives) @cached_property @@ -109,11 +109,11 @@ class Weapon(types.Item): @property def to_hit(self): bonus_val = 0 - bonus_dice = '' - if not hasattr(self, 'properties'): - return '' + bonus_dice = "" + if not hasattr(self, "properties"): + return "" for prop in self.properties.values(): - mod = getattr(prop, 'to_hit', None) + mod = getattr(prop, "to_hit", None) if not mod: continue if type(mod) is int: @@ -124,24 +124,22 @@ class Weapon(types.Item): @property def damage_dice(self): - if not hasattr(self, 'properties'): - return '' - dmg = { - self.damage_type: str(self.damage) or '' - } + if not hasattr(self, "properties"): + return "" + dmg = {self.damage_type: str(self.damage) or ""} for prop in self.properties.values(): - mod = getattr(prop, 'damage', None) + mod = getattr(prop, "damage", None) if not mod: continue key = str(prop.damage_type) - this_damage = dmg.get(key, '') + this_damage = dmg.get(key, "") if this_damage: dmg[key] = f"{this_damage}+{mod}" else: dmg[key] = mod - return ' + '.join([f"{v} {k}" for k, v in dmg.items()]) + return " + ".join([f"{v} {k}" for k, v in dmg.items()]) @property def summary(self): @@ -152,20 +150,28 @@ class Weapon(types.Item): """ Format the item properties as nested bullet lists. """ - props = ', '.join(self.get('properties', dict()).keys()) - return "\n".join([ - f"{self.name}", - f" * {self.rarity.rarity} {self.category} weapon ({props})", - f" * {self.summary}", - f"\n{self.description}\n" - ]) + props = ", ".join(self.get("properties", dict()).keys()) + return "\n".join( + [ + f"{self.name}", + f" * {self.rarity.rarity} {self.category} weapon ({props})", + f" * {self.summary}", + f"\n{self.description}\n", + ] + ) @property def id(self): - sha1bytes = hashlib.sha1(''.join([ - self._name, self.to_hit, self.damage_dice, - ]).encode()) - return base64.urlsafe_b64encode(sha1bytes.digest()).decode('ascii')[:10] + sha1bytes = hashlib.sha1( + "".join( + [ + self._name, + self.to_hit, + self.damage_dice, + ] + ).encode() + ) + return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10] class WeaponGenerator(types.ItemGenerator): @@ -182,16 +188,16 @@ class WeaponGenerator(types.ItemGenerator): def random_properties(self) -> dict: # add missing base weapon defaults (TODO: update the sources) item = super().random_properties() - item['targets'] = 1 - if item['category'] == 'Martial': - if not item['range']: - item['range'] = '' + item["targets"] = 1 + if item["category"] == "Martial": + if not item["range"]: + item["range"] = "" return item # handlers for extra properties def get_enchantment(self, **attrs) -> dict: prop = types.ENCHANTMENT.random() - prop['adjectives'] = random_from_csv(prop['adjectives']) - prop['nouns'] = random_from_csv(prop['nouns']) + prop["adjectives"] = random_from_csv(prop["adjectives"]) + prop["nouns"] = random_from_csv(prop["nouns"]) return prop