diff --git a/src/nobilis/game_data.py b/src/nobilis/game_data.py --- a/src/nobilis/game_data.py +++ b/src/nobilis/game_data.py @@ -244,18 +244,18 @@ SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()), -AFFLIATION_TEMPLATES = () -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES = () +AFFILIATION_TEMPLATES += GeneralTemplate( '(The Song of) Heaven', (1, 2, 3)), -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES += GeneralTemplate( '(The Song of) Hell', (4, 5, 6)), -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES += GeneralTemplate( '(The Song of) The Light', (7, 8)), -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES += GeneralTemplate( '(The Song of) The Dark', (9, 10)), -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES += GeneralTemplate( '(The Song of) The Wild', (11, 12, 13)), -AFFLIATION_TEMPLATES += GeneralTemplate( +AFFILIATION_TEMPLATES += GeneralTemplate( 'An Independent Song', (14, 15, 16)), diff --git a/src/nobilis/lifepath.py b/src/nobilis/lifepath.py --- a/src/nobilis/lifepath.py +++ b/src/nobilis/lifepath.py @@ -12,6 +12,7 @@ def key(item): + """Return a type-based key for the given item.""" return type(item), item @@ -19,6 +20,7 @@ # now. Basically, it needs to be used as a mixin AHEAD OF a namedtuple class # with a first field of "hash". class QuickHash: + """Base class for namedtuples that store their hash in the first field.""" __slots__ = () @@ -28,6 +30,7 @@ @classmethod def _pre_hash(cls, *args, **kwargs): + """Return the hash the instance will use.""" args = tuple(args) fields = getattr(cls, '_fields', ('hash',))[1:] keywords = fields[len(args):] @@ -42,10 +45,12 @@ class Child(Business): + """Purely structural child visitor.""" __slots__ = () def default_visit(self, node): + """Yield zero times.""" yield from () @@ -53,6 +58,7 @@ class Tree(QH, nt('Tree', 'hash root reverse_mapping')): + """Purely structural tree structure, for determining parents.""" __slots__ = () @@ -71,77 +77,64 @@ return super().__new__(cls, root, mapping.Map(reverse_mapping)) def parent(self, node): + """Return the parent of the given node.""" return self.reverse_mapping.get(key(node)) @Business.mark class Choice(QH, nt('Choice', 'hash template')): - - __slots__ = () - - -@Business.mark -class ChoiceDisplay(QH, nt('ChoiceDisplay', 'hash choice')): + """An underlying choice for tracking state.""" __slots__ = () +# First to test in rewrite. @Business.mark -class ChoiceTaken(QH, nt('ChoiceTaken', 'hash choice')): +class Tab(QH, nt('Tab', 'hash name node')): __slots__ = () -@Business.mark -class ChoiceListed(QH, nt('ChoiceListed', 'hash choice')): - - __slots__ = () - - +# First to test in rewrite. @Business.mark -class ChoiceList(QH, nt('ChoiceList', 'hash choices initial_min initial_max')): - - __slots__ = () - - -@Business.mark -class DisplayList(QH, nt('DisplayList', 'hash factory choices_ sections_')): +class TabList(QH, nt('TabList', 'hash tabs')): __slots__ = () @property - def choices(self): - return map_tuple.MapTuple(self.factory, self.choices_) - - @property - def sections(self): - return map_tuple.MapTuple( - functools.partial(Section, self), range(len(self.sections_))) + def enumerated_names(self): + return tuple(enumerate(tab.name for tab in self.tabs)) +# Second to test in rewrite, may need rename. @Business.mark -class Section(QH, nt('Section', 'hash display_list index')): +class Table(QH, nt('Table', 'hash heading node')): + + __slots__ = () + + +# Second to test in rewrite, may need rename. +@Business.mark +class TableList(QH, nt('TableList', 'hash items')): __slots__ = () - @property - def _data(self): - return self.display_list.sections_[self.index] - @property - def name(self): - return self._data[0] - - @property - def predicate(self): - return self._data[1] +@Child.bind(Tab) +def Child(self, node, next_visitor): + yield node.node -@Child.bind(ChoiceList) +@Child.bind(TabList) def Child(self, node, next_visitor): - yield from node.choices + yield from node.tabs -@Child.bind(DisplayList) +@Child.bind(Table) def Child(self, node, next_visitor): - yield from node.choices + yield node.node + + +@Child.bind(TableList) +def Child(self, node, next_visitor): + yield from node.items diff --git a/tests/nobilis/test_lifepath.py b/tests/nobilis/test_lifepath.py --- a/tests/nobilis/test_lifepath.py +++ b/tests/nobilis/test_lifepath.py @@ -7,7 +7,7 @@ def test_key(lifepath): assert lifepath.key(lifepath.Choice(None)) != lifepath.key( - lifepath.ChoiceDisplay(None)) + lifepath.TabList(None)) def test_hash_amortization(lifepath): @@ -61,55 +61,133 @@ def test_children(lifepath): child = lifepath.Child() - leaf = lifepath.Choice(None) + tab = lifepath.Tab('name', None) - assert tuple(child.visit(leaf)) == () + tab_list = lifepath.TabList((tab,)) + + assert tuple(child.visit(tab_list)) == (tab,) + assert tuple(child.visit(tab)) == (None,) - for wrapper in (lifepath.ChoiceDisplay, lifepath.ChoiceTaken, - lifepath.ChoiceListed): - assert tuple(child.visit(wrapper(leaf))) == () + item = lifepath.Table('heading', None) - assert tuple(child.visit(lifepath.ChoiceList((leaf,), 0, None))) == (leaf,) + item_list = lifepath.TableList((item,)) - display_list = lifepath.DisplayList( - lifepath.ChoiceDisplay, (leaf,), (('name', None),)) - - assert tuple(child.visit(display_list)) == (lifepath.ChoiceDisplay(leaf),) + assert tuple(child.visit(item_list)) == (item,) + assert tuple(child.visit(item)) == (None,) def test_tree(lifepath): - leaf = lifepath.Choice(None) - root = lifepath.ChoiceList((leaf,), 0, None) + leaf = lifepath.Tab('test1', None) + root = lifepath.TabList((leaf,)) tree = lifepath.Tree(root) assert tree.parent(leaf) == root - rooter = lifepath.ChoiceList((root,), 0, None) + rooter = lifepath.TabList((root,)) treeer = lifepath.Tree(rooter) assert treeer.parent(root) == rooter assert treeer.parent(leaf) == root - dag = lifepath.ChoiceList((leaf, leaf), 0, None) + dag = lifepath.TabList((leaf, leaf)) with pytest.raises(ValueError): print(lifepath.Tree(dag)) -def test_section(lifepath): - leaf = lifepath.Choice(None) - sentinel = object() - - display_list = lifepath.DisplayList( - lifepath.ChoiceDisplay, (leaf,), (('name', sentinel),)) - - section, = display_list.sections - - assert section.name == 'name' - assert section.predicate is sentinel - - def test_state(lifepath): # Okay, so, let's think how this has to go. # Ideally, I wouldn't be relying on mutable state, so... # state = lifepath.State() pass + + +# Okay, let's start over. At the current way of thinking of things, a character +# sheet is going to have multiple tabs. + +def test_power_tabs(lifepath): + placeholder = object() + + # Must be selected before the others. + key_tab = lifepath.Tab('Keys', placeholder) + # No additional dependencies. + foundation_tab = lifepath.Tab('Foundation', placeholder) + # No additional dependencies. + estate_tab = lifepath.Tab('Estate', placeholder) + # Internal dependencies. + origins_tab = lifepath.Tab('Origins', placeholder) + # Some interaction with Origins. + contacts_tab = lifepath.Tab('Contacts', placeholder) + # No additional dependencies. + affiliation_tab = lifepath.Tab('Affiliation', placeholder) + # No additional dependencies. + background_tab = lifepath.Tab('Background', placeholder) + + tab_list = lifepath.TabList(( + key_tab, + foundation_tab, + estate_tab, + origins_tab, + contacts_tab, + affiliation_tab, + background_tab)) + + # Now that we have the tab_list, we need to reason about it... + # Would be good to get an enumerated list of names. + + assert tab_list.enumerated_names == ( + (0, 'Keys'), + (1, 'Foundation'), + (2, 'Estate'), + (3, 'Origins'), + (4, 'Contacts'), + (5, 'Affiliation'), + (6, 'Background'),) + + +# Actually, what if I do both kinds of character sheet? + +def test_deceiver_tabs(lifepath): + placeholder = object() + + # Must be selected before the others. + key_tab = lifepath.Tab('Keys', placeholder) + # The Foundation tab seems the most mix-and-matchable. + # No additional dependencies. + foundation_tab = lifepath.Tab('Foundation', placeholder) + # No additional dependencies. + p_state_tab = lifepath.Tab('pState', placeholder) + # No internal dependencies. + breakthrough_tab = lifepath.Tab('Breakthrough', placeholder) + # May end up with 2 plan tabs + # No internal dependencies. + plans_tab = lifepath.Tab('Plans', placeholder) + # Contacts divided into typical and rare, both sorted, which implies some + # way of marking them outside of a section label. + # Of note: no dependency on other sections. Just pick "up to three" + # Not sure if that REQUIRES at least 1, it's probably a good idea though. + # No internal dependencies. + contacts_tab = lifepath.Tab('Contacts', placeholder) + # No internal dependencies. + justification_tab = lifepath.Tab('Justification', placeholder) + # No internal dependencies. + territory_tab = lifepath.Tab('Territory', placeholder) + + tab_list = lifepath.TabList(( + key_tab, + foundation_tab, + p_state_tab, + breakthrough_tab, + plans_tab, + contacts_tab, + justification_tab, + territory_tab)) + + assert tab_list.enumerated_names == ( + (0, 'Keys'), + (1, 'Foundation'), + (2, 'pState'), + (3, 'Breakthrough'), + (4, 'Plans'), + (5, 'Contacts'), + (6, 'Justification'), + (7, 'Territory'),)