mwchase icon

LC:Nobilis Update 2 - data.py only

mwchase | PRO | 05/07/17 10:39:37 PM UTC | 0 ⭐ | 866 👁️ | Never ⏰ | []
Python |

9.96 KB

|

None

|

0 👍

/

0 👎

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

Comments