mwchase icon

LC:Nobilis Update 2

mwchase | PRO | 05/07/17 10:33:16 PM UTC | 0 ⭐ | 1351 👁️ | Never ⏰ | []
Diff |

15.45 KB

|

None

|

0 👍

/

0 👎

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)
+
+
[email protected](Choice)
+class StateVisitor(Caching):
+
+    is_valid = True
+
+    selected = False
+
+    @property
+    def choice(self):
+        return self.node
+
+
[email protected](ChoiceDisplay)
+class StateVisitor(ChoiceButton):
+
+    enabled = False
+
+    selected = False
+
+    @property
+    def choice_display(self):
+        return self.node
+
+
[email protected](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)
+
+
[email protected](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
+
+
[email protected](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
+
+
[email protected](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)
+
+
[email protected](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'

Comments