Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- diff --git a/data.py b/data.py
- new file mode 100644
- --- /dev/null
- +++ b/data.py
- @@ -0,0 +1,379 @@
- +import collections
- +import functools
- +import string
- +
- +
- +ALLOWED_CHARS = set(string.letters + string.digits + '_')
- +SENTINEL = object()
- +PARENT = {}
- +
- +
- +def visitor_name(cls):
- + node_type = getattr(cls, 'node_type', None)
- + if node_type is None:
- + raise ValueError(f'No node_type defined for {cls}')
- + return f'visit_{node_type}'
- +
- +
- +class NodeMeta(type):
- +
- + def __new__(mcs, name, bases, namespace, **kwargs):
- + # Not sure this doesn't blow up.
- + cls = super().__new__(mcs, name, bases, namespace, **kwargs)
- + node_type = getattr(cls, 'node_type', SENTINEL)
- + if node_type is not SENTINEL:
- + if node_type is None:
- + return cls
- + if node_type in cls._types:
- + raise ValueError(f'Duplicate node type: {node_type!r}')
- + if not ALLOWED_CHARS.issuperset(node_type):
- + raise ValueError(f'Impolite node type: {node_type!r}')
- + cls._types.add(node_type)
- + _visitor_name = visitor_name(cls)
- +
- + def dispatch(self, visitor):
- + return getattr(visitor, _visitor_name)(self)
- + setattr(cls.Visitor, _visitor_name, cls.Visitor.default_visit)
- + else:
- + cls.node_type = None
- + cls._types = set()
- +
- + def dispatch(self, methods):
- + raise NotImplementedError(
- + 'Subclasses should auto-generate this.')
- +
- + class Visitor:
- +
- + def default_visit(self, node):
- + raise NotImplementedError(
- + f'No implementation for {node} on {self}.')
- +
- + @classmethod
- + def bind(cls_, node_class, function=SENTINEL):
- + if cls_ is cls.Visitor:
- + raise ValueError('Cannot bind base Visitor class.')
- + if function is SENTINEL:
- + return functools.partial(cls_.bind, node_class)
- + setattr(cls_, visitor_name(node_class), function)
- + return cls_
- +
- + @classmethod
- + def undefined(cls_):
- + if cls_ is cls.Visitor:
- + raise ValueError(
- + 'No point checking undefined on base Visitor '
- + 'class.')
- + default = cls.Visitor.default_visit
- + return {attr for attr in vars(cls.Visitor) if
- + attr.startswith('visit_') and
- + default == getattr(cls_, attr)}
- +
- + cls.Visitor = Visitor
- +
- + cls.dispatch = dispatch
- +
- + return cls
- +
- +
- +class BusinessNode(metaclass=NodeMeta):
- +
- + __slots__ = ()
- +
- + @property
- + def parent(self):
- + return PARENT.get((type(self), self))
- +
- +
- +class CachingMeta(type):
- +
- + def __get__(self, instance, owner):
- + if instance is None:
- + return functools.partial(self)
- + else:
- + return functools.partial(self, instance)
- +
- +
- +class Caching(metaclass=CachingMeta):
- +
- + set_up = False
- +
- + def __new__(cls, instance, node):
- + return instance.setdefault((type(node), node), super().__new__(cls))
- +
- + def __init_subclass__(cls, **kwargs):
- + super().__init_subclass__(**kwargs)
- + init = vars(cls).get('__init__')
- + if init is not None:
- + def __init__(self, *args, **kwargs):
- + if self.set_up:
- + return
- + init(self, *args, **kwargs)
- + self.set_up = True
- + cls.__init__ = __init__
- +
- +
- +class StateVisitor(BusinessNode.Visitor, dict):
- +
- + __slots__ = ()
- +
- +
- +class UIVisitor(BusinessNode.Visitor, dict):
- +
- + __slots__ = 'state',
- +
- + def __init__(self, state):
- + super().__init__()
- + self.state = state
- +
- +
- +class Choice(BusinessNode, collections.namedtuple('Choice', 'template')):
- +
- + node_type = 'choice'
- +
- +
- +class ChoiceDisplay(BusinessNode,
- + collections.namedtuple('ChoiceDisplay', 'choice')):
- +
- + node_type = 'choice_display'
- +
- +
- +class ChoiceTaken(BusinessNode,
- + collections.namedtuple('ChoiceTaken', 'choice')):
- +
- + node_type = 'choice_taken'
- +
- +
- +class ChoiceListed(BusinessNode,
- + collections.namedtuple('ChoiceListed', 'choice')):
- +
- + node_type = 'choice_listed'
- +
- +
- +class ChoiceList(BusinessNode,
- + collections.namedtuple('ChoiceList',
- + 'choices initial_min initial_max')):
- +
- + node_type = 'choice_list'
- +
- + def __init__(self, choices, initial_min, initial_max):
- + for choice in choices:
- + key = type(choice), choice
- + if PARENT.setdefault(key, self) != self:
- + for choice in choices:
- + if PARENT.get(key) is self:
- + del PARENT[key]
- + raise ValueError(f'Cannot double-parent a choice: {choice}')
- +
- +
- +class DisplayList(BusinessNode,
- + collections.namedtuple('DisplayList',
- + 'factory _choices _sections')):
- +
- + node_type = 'display_list'
- +
- + def __init__(self, factory, _choices, sections):
- + for choice in self.choices:
- + key = type(choice), choice
- + if PARENT.setdefault(key, self) != self:
- + for choice in self.choices:
- + if PARENT.get(key) is self:
- + del PARENT[key]
- + raise ValueError(f'Cannot double-parent a choice: {choice}')
- +
- + @property
- + def choices(self):
- + for choice in self._choices:
- + yield self.factory(choice)
- +
- + @property
- + def sections(self):
- + # This is O(n^2), but n is small
- + for section in self._sections:
- + yield Section(self, section[0])
- +
- +
- +class Section(BusinessNode,
- + collections.namedtuple('Section', 'display_list', 'name')):
- +
- + def __init__(self, display_list, name):
- + self.section
- +
- + @property
- + def predicate(self):
- + return self.section[1]
- +
- + @property
- + def section(self):
- + for section in self.display_list._sections:
- + if section[0] == self.name:
- + return section
- + raise ValueError(
- + f'No section in {self.display_list} matching {self.name}')
- +
- +
- +class StateBase(Caching):
- +
- + visible = False
- +
- + def __init__(self, instance, node):
- + self.visitor = instance
- + self.node = node
- +
- + @property
- + def parent(self):
- + if self.node.parent:
- + return self.node.parent.dispatch(self.visitor)
- +
- +
- +class BusinessButton(Caching):
- +
- + @property
- + def is_valid(self):
- + return self.enabled or not self.selected
- +
- + @property
- + def visible(self):
- + parent = self.parent
- + return parent is not None and parent.visible
- +
- +
- +class ChoiceButton(BusinessButton):
- +
- + @property
- + def choice(self):
- + return self.node.choice.dispatch(self.visitor)
- +
- +
- +@StateVisitor.bind(Choice)
- +class StateVisitor(Caching):
- +
- + is_valid = True
- +
- + selected = False
- +
- + @property
- + def choice(self):
- + return self.node
- +
- +
- +@StateVisitor.bind(ChoiceDisplay)
- +class StateVisitor(ChoiceButton):
- +
- + enabled = False
- +
- + selected = False
- +
- + @property
- + def choice_display(self):
- + return self.node
- +
- +
- +@StateVisitor.bind(ChoiceTaken)
- +class StateVisitor(ChoiceButton):
- +
- + @property
- + def choice_taken(self):
- + return self.node
- +
- + # Should this go on UI instead?
- + @property
- + def visible(self):
- + return (super().visible and
- + self.choice_taken.choice.dispatch(self.visitor).selected)
- +
- +
- +@StateVisitor.bind(ChoiceListed)
- +class StateVisitor(ChoiceButton):
- +
- + @property
- + def choice_listed(self):
- + return self.node
- +
- + @property
- + def selected(self):
- + return self.choice_listed.choice.dispatch(self.visitor).selected
- +
- + @property
- + def enabled(self):
- + parent = self.parent
- + if parent:
- + return self.selected or not parent.at_maximum
- + return True
- +
- +
- +@StateVisitor.bind(ChoiceList)
- +class StateVisitor(Caching):
- +
- + def __init__(self, instance, node):
- + super().__init__(instance, node)
- + self.minimum = node.initial_min
- + self.maximum = node.initial_max
- +
- + @property
- + def is_valid(self):
- + if self.maximum is not None and self.selected > self.maximum:
- + return False
- + return all(choice.is_valid for choice in self.choices)
- +
- + @property
- + def choices(self):
- + for choice in self.choice_list.choices:
- + yield choice.dispatch(self.visitor)
- +
- + @property
- + def selected(self):
- + return sum(choice.selected for choice in self.choices)
- +
- + @property
- + def choice_list(self):
- + return self.node
- +
- + @property
- + def at_minimum(self):
- + return self.selected >= self.minimum
- +
- + @property
- + def at_maximum(self):
- + return self.maximum is not None and self.selected >= self.maximum
- +
- +
- +@StateVisitor.bind(DisplayList)
- +class StateVisitor(Caching):
- +
- + @property
- + def display_list(self):
- + return self.node
- +
- + @property
- + def is_valid(self):
- + return all(choice.is_valid for choice in self.choices)
- +
- + @property
- + def choices(self):
- + for choice in self.display_list.choices:
- + yield choice.dispatch(self.visitor)
- +
- + @property
- + def sections(self):
- + for section in self.display_list.sections:
- + yield section.dispatch(self.visitor)
- +
- +
- +@StateVisitor.bind(Section)
- +class StateVisitor(Caching):
- +
- + @property
- + def section(self):
- + return self.node
- +
- + @property
- + def name(self):
- + return self.section.name
- +
- + @property
- + def choices(self):
- + predicate = self.section.predicate
- + for choice in self.section.display_list.choices:
- + visited = choice.dispatch(self.visitor)
- + if predicate(visited):
- + yield visited
- diff --git a/nobilis.py b/nobilis.py
- --- a/nobilis.py
- +++ b/nobilis.py
- @@ -146,6 +146,130 @@
- ()),
- +GeneralTemplate = collections.namedtuple('GeneralTemplate', 'name indices')
- +
- +
- +FOUNDATION_TEMPLATES = ()
- +FOUNDATION_TEMPLATES += GeneralTemplate(
- + 'Something Cool', (1, 2, 10, 15)),
- +FOUNDATION_TEMPLATES += GeneralTemplate(
- + 'In Love with Something', (4, 7, 9, 16)),
- +FOUNDATION_TEMPLATES += GeneralTemplate(
- + 'Epic, Inhuman, and Powerful', (5, 6, 12, 14)),
- +FOUNDATION_TEMPLATES += GeneralTemplate(
- + 'Just Plain Weird', (3, 8, 11, 13)),
- +
- +ESTATE_SOURCE_TEMPLATES = ()
- +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
- + 'Light Side of Human Experience', (2, 6, 10, 12)),
- +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
- + 'Dark Side of Human Experience', (3, 5, 11, 15)),
- +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
- + 'Beautiful Side of the World', (4, 7, 14, 16)),
- +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
- + 'Painful Side of the World', (1, 8, 9, 13)),
- +
- +ESTATE_NATURE_TEMPLATES = ()
- +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
- + 'Something You Can Point To', (8, 9, 13, 14, 15)),
- +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
- + 'Something You Live', (3, 5, 6, 4, 11, 12)),
- +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
- + 'Something You Can Describe', (1, 2, 7, 10, 16)),
- +
- +ORIGIN_TEMPLATES = ()
- +ORIGIN_TEMPLATES += GeneralTemplate('Troubled Life', (2, 5, 6, 11)),
- +ORIGIN_TEMPLATES += GeneralTemplate('Humble Life', (8, 9, 14, 15)),
- +ORIGIN_TEMPLATES += GeneralTemplate('Blessed Life', (1, 4, 12, 16)),
- +ORIGIN_TEMPLATES += GeneralTemplate('Extraordinary Life', (3, 7, 10, 13)),
- +
- +TROUBLED_TEMPLATES = ()
- +TROUBLED_TEMPLATES += GeneralTemplate(
- + "You're Still in Trouble!", (3, 5, 8, 13)),
- +TROUBLED_TEMPLATES += GeneralTemplate(
- + 'Some Scars Remain', (7, 11, 12, 14)),
- +TROUBLED_TEMPLATES += GeneralTemplate(
- + "It's All Happening Again!", (6, 10, 15, 16)),
- +TROUBLED_TEMPLATES += GeneralTemplate(
- + 'Trouble Inspired Me', (1, 2, 4, 9)),
- +
- +HUMBLE_TEMPLATES = ()
- +HUMBLE_TEMPLATES += GeneralTemplate(
- + 'Love for the Ordinary...', (6, 7, 15, 16)),
- +HUMBLE_TEMPLATES += GeneralTemplate(
- + 'Alienation...', (3, 5, 11, 14)),
- +HUMBLE_TEMPLATES += GeneralTemplate(
- + 'Transformation...', (4, 8, 12, 13)),
- +HUMBLE_TEMPLATES += GeneralTemplate(
- + 'Freedom!', (1, 2, 9, 10)),
- +
- +BLESSED_TEMPLATES = ()
- +BLESSED_TEMPLATES += GeneralTemplate('Reverence in Purpose', (2, 4, 10, 13)),
- +BLESSED_TEMPLATES += GeneralTemplate('Community', (1, 3, 8, 15)),
- +BLESSED_TEMPLATES += GeneralTemplate('Your Way of Life', (7, 9, 14, 16)),
- +BLESSED_TEMPLATES += GeneralTemplate('Anger', (5, 6, 9, 12)),
- +
- +NORMAL_CONTACT_TEMPLATES = ()
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Organization', (1,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Followers', (2,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Excrucian', (3,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Inspirational Friend or Lover', (4,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Nemesis', (5,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Corrupting Influence', (6,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Ghost', (7,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Cammora', (8,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Legacy', (9,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'True Love', (10,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Alien(s)', (11,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Manufactured Army', (12,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Faraway or Troubled Love', (13,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Ward', (14,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Moral Friends and Family', (15,)),
- +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
- + 'Disciples', (16,)),
- +
- +SPECIAL_CONTACT_TEMPLATES = ()
- +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Noble Friend or Enemy', ()),
- +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Cleave of the Botanists', ()),
- +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()),
- +
- +
- +AFFLIATION_TEMPLATES = ()
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + '(The Song of) Heaven', (1, 2, 3)),
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + '(The Song of) Hell', (4, 5, 6)),
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + '(The Song of) The Light', (7, 8)),
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + '(The Song of) The Dark', (9, 10)),
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + '(The Song of) The Wild', (11, 12, 13)),
- +AFFLIATION_TEMPLATES += GeneralTemplate(
- + 'An Independent Song', (14, 15, 16)),
- +
- +
- +BACKGROUND_TEMPLATES = ()
- +BACKGROUND_TEMPLATES += GeneralTemplate('Animal', (6, 8, 14, 15)),
- +BACKGROUND_TEMPLATES += GeneralTemplate('Strange', (4, 11, 12, 16)),
- +BACKGROUND_TEMPLATES += GeneralTemplate('"Sort of" Human', (1, 7, 10, 13)),
- +
- +
- MIN_SELECTED = 'min_selected'
- MAX_SELECTED = 'max_selected'
- SELECTED = 'selected'
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement