Advertisement
mwchase

LC:Nobilis Update 2

May 7th, 2017
590
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Diff 15.45 KB | None | 0 0
  1. diff --git a/data.py b/data.py
  2. new file mode 100644
  3. --- /dev/null
  4. +++ b/data.py
  5. @@ -0,0 +1,379 @@
  6. +import collections
  7. +import functools
  8. +import string
  9. +
  10. +
  11. +ALLOWED_CHARS = set(string.letters + string.digits + '_')
  12. +SENTINEL = object()
  13. +PARENT = {}
  14. +
  15. +
  16. +def visitor_name(cls):
  17. +    node_type = getattr(cls, 'node_type', None)
  18. +    if node_type is None:
  19. +        raise ValueError(f'No node_type defined for {cls}')
  20. +    return f'visit_{node_type}'
  21. +
  22. +
  23. +class NodeMeta(type):
  24. +
  25. +    def __new__(mcs, name, bases, namespace, **kwargs):
  26. +        # Not sure this doesn't blow up.
  27. +        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
  28. +        node_type = getattr(cls, 'node_type', SENTINEL)
  29. +        if node_type is not SENTINEL:
  30. +            if node_type is None:
  31. +                return cls
  32. +            if node_type in cls._types:
  33. +                raise ValueError(f'Duplicate node type: {node_type!r}')
  34. +            if not ALLOWED_CHARS.issuperset(node_type):
  35. +                raise ValueError(f'Impolite node type: {node_type!r}')
  36. +            cls._types.add(node_type)
  37. +            _visitor_name = visitor_name(cls)
  38. +
  39. +            def dispatch(self, visitor):
  40. +                return getattr(visitor, _visitor_name)(self)
  41. +            setattr(cls.Visitor, _visitor_name, cls.Visitor.default_visit)
  42. +        else:
  43. +            cls.node_type = None
  44. +            cls._types = set()
  45. +
  46. +            def dispatch(self, methods):
  47. +                raise NotImplementedError(
  48. +                    'Subclasses should auto-generate this.')
  49. +
  50. +            class Visitor:
  51. +
  52. +                def default_visit(self, node):
  53. +                    raise NotImplementedError(
  54. +                        f'No implementation for {node} on {self}.')
  55. +
  56. +                @classmethod
  57. +                def bind(cls_, node_class, function=SENTINEL):
  58. +                    if cls_ is cls.Visitor:
  59. +                        raise ValueError('Cannot bind base Visitor class.')
  60. +                    if function is SENTINEL:
  61. +                        return functools.partial(cls_.bind, node_class)
  62. +                    setattr(cls_, visitor_name(node_class), function)
  63. +                    return cls_
  64. +
  65. +                @classmethod
  66. +                def undefined(cls_):
  67. +                    if cls_ is cls.Visitor:
  68. +                        raise ValueError(
  69. +                            'No point checking undefined on base Visitor '
  70. +                            'class.')
  71. +                    default = cls.Visitor.default_visit
  72. +                    return {attr for attr in vars(cls.Visitor) if
  73. +                            attr.startswith('visit_') and
  74. +                            default == getattr(cls_, attr)}
  75. +
  76. +            cls.Visitor = Visitor
  77. +
  78. +        cls.dispatch = dispatch
  79. +
  80. +        return cls
  81. +
  82. +
  83. +class BusinessNode(metaclass=NodeMeta):
  84. +
  85. +    __slots__ = ()
  86. +
  87. +    @property
  88. +    def parent(self):
  89. +        return PARENT.get((type(self), self))
  90. +
  91. +
  92. +class CachingMeta(type):
  93. +
  94. +    def __get__(self, instance, owner):
  95. +        if instance is None:
  96. +            return functools.partial(self)
  97. +        else:
  98. +            return functools.partial(self, instance)
  99. +
  100. +
  101. +class Caching(metaclass=CachingMeta):
  102. +
  103. +    set_up = False
  104. +
  105. +    def __new__(cls, instance, node):
  106. +        return instance.setdefault((type(node), node), super().__new__(cls))
  107. +
  108. +    def __init_subclass__(cls, **kwargs):
  109. +        super().__init_subclass__(**kwargs)
  110. +        init = vars(cls).get('__init__')
  111. +        if init is not None:
  112. +            def __init__(self, *args, **kwargs):
  113. +                if self.set_up:
  114. +                    return
  115. +                init(self, *args, **kwargs)
  116. +                self.set_up = True
  117. +            cls.__init__ = __init__
  118. +
  119. +
  120. +class StateVisitor(BusinessNode.Visitor, dict):
  121. +
  122. +    __slots__ = ()
  123. +
  124. +
  125. +class UIVisitor(BusinessNode.Visitor, dict):
  126. +
  127. +    __slots__ = 'state',
  128. +
  129. +    def __init__(self, state):
  130. +        super().__init__()
  131. +        self.state = state
  132. +
  133. +
  134. +class Choice(BusinessNode, collections.namedtuple('Choice', 'template')):
  135. +
  136. +    node_type = 'choice'
  137. +
  138. +
  139. +class ChoiceDisplay(BusinessNode,
  140. +                    collections.namedtuple('ChoiceDisplay', 'choice')):
  141. +
  142. +    node_type = 'choice_display'
  143. +
  144. +
  145. +class ChoiceTaken(BusinessNode,
  146. +                  collections.namedtuple('ChoiceTaken', 'choice')):
  147. +
  148. +    node_type = 'choice_taken'
  149. +
  150. +
  151. +class ChoiceListed(BusinessNode,
  152. +                   collections.namedtuple('ChoiceListed', 'choice')):
  153. +
  154. +    node_type = 'choice_listed'
  155. +
  156. +
  157. +class ChoiceList(BusinessNode,
  158. +                 collections.namedtuple('ChoiceList',
  159. +                                        'choices initial_min initial_max')):
  160. +
  161. +    node_type = 'choice_list'
  162. +
  163. +    def __init__(self, choices, initial_min, initial_max):
  164. +        for choice in choices:
  165. +            key = type(choice), choice
  166. +            if PARENT.setdefault(key, self) != self:
  167. +                for choice in choices:
  168. +                    if PARENT.get(key) is self:
  169. +                        del PARENT[key]
  170. +                raise ValueError(f'Cannot double-parent a choice: {choice}')
  171. +
  172. +
  173. +class DisplayList(BusinessNode,
  174. +                  collections.namedtuple('DisplayList',
  175. +                                         'factory _choices _sections')):
  176. +
  177. +    node_type = 'display_list'
  178. +
  179. +    def __init__(self, factory, _choices, sections):
  180. +        for choice in self.choices:
  181. +            key = type(choice), choice
  182. +            if PARENT.setdefault(key, self) != self:
  183. +                for choice in self.choices:
  184. +                    if PARENT.get(key) is self:
  185. +                        del PARENT[key]
  186. +                raise ValueError(f'Cannot double-parent a choice: {choice}')
  187. +
  188. +    @property
  189. +    def choices(self):
  190. +        for choice in self._choices:
  191. +            yield self.factory(choice)
  192. +
  193. +    @property
  194. +    def sections(self):
  195. +        # This is O(n^2), but n is small
  196. +        for section in self._sections:
  197. +            yield Section(self, section[0])
  198. +
  199. +
  200. +class Section(BusinessNode,
  201. +              collections.namedtuple('Section', 'display_list', 'name')):
  202. +
  203. +    def __init__(self, display_list, name):
  204. +        self.section
  205. +
  206. +    @property
  207. +    def predicate(self):
  208. +        return self.section[1]
  209. +
  210. +    @property
  211. +    def section(self):
  212. +        for section in self.display_list._sections:
  213. +            if section[0] == self.name:
  214. +                return section
  215. +        raise ValueError(
  216. +            f'No section in {self.display_list} matching {self.name}')
  217. +
  218. +
  219. +class StateBase(Caching):
  220. +
  221. +    visible = False
  222. +
  223. +    def __init__(self, instance, node):
  224. +        self.visitor = instance
  225. +        self.node = node
  226. +
  227. +    @property
  228. +    def parent(self):
  229. +        if self.node.parent:
  230. +            return self.node.parent.dispatch(self.visitor)
  231. +
  232. +
  233. +class BusinessButton(Caching):
  234. +
  235. +    @property
  236. +    def is_valid(self):
  237. +        return self.enabled or not self.selected
  238. +
  239. +    @property
  240. +    def visible(self):
  241. +        parent = self.parent
  242. +        return parent is not None and parent.visible
  243. +
  244. +
  245. +class ChoiceButton(BusinessButton):
  246. +
  247. +    @property
  248. +    def choice(self):
  249. +        return self.node.choice.dispatch(self.visitor)
  250. +
  251. +
  252. +@StateVisitor.bind(Choice)
  253. +class StateVisitor(Caching):
  254. +
  255. +    is_valid = True
  256. +
  257. +    selected = False
  258. +
  259. +    @property
  260. +    def choice(self):
  261. +        return self.node
  262. +
  263. +
  264. +@StateVisitor.bind(ChoiceDisplay)
  265. +class StateVisitor(ChoiceButton):
  266. +
  267. +    enabled = False
  268. +
  269. +    selected = False
  270. +
  271. +    @property
  272. +    def choice_display(self):
  273. +        return self.node
  274. +
  275. +
  276. +@StateVisitor.bind(ChoiceTaken)
  277. +class StateVisitor(ChoiceButton):
  278. +
  279. +    @property
  280. +    def choice_taken(self):
  281. +        return self.node
  282. +
  283. +    # Should this go on UI instead?
  284. +    @property
  285. +    def visible(self):
  286. +        return (super().visible and
  287. +                self.choice_taken.choice.dispatch(self.visitor).selected)
  288. +
  289. +
  290. +@StateVisitor.bind(ChoiceListed)
  291. +class StateVisitor(ChoiceButton):
  292. +
  293. +    @property
  294. +    def choice_listed(self):
  295. +        return self.node
  296. +
  297. +    @property
  298. +    def selected(self):
  299. +        return self.choice_listed.choice.dispatch(self.visitor).selected
  300. +
  301. +    @property
  302. +    def enabled(self):
  303. +        parent = self.parent
  304. +        if parent:
  305. +            return self.selected or not parent.at_maximum
  306. +        return True
  307. +
  308. +
  309. +@StateVisitor.bind(ChoiceList)
  310. +class StateVisitor(Caching):
  311. +
  312. +    def __init__(self, instance, node):
  313. +        super().__init__(instance, node)
  314. +        self.minimum = node.initial_min
  315. +        self.maximum = node.initial_max
  316. +
  317. +    @property
  318. +    def is_valid(self):
  319. +        if self.maximum is not None and self.selected > self.maximum:
  320. +            return False
  321. +        return all(choice.is_valid for choice in self.choices)
  322. +
  323. +    @property
  324. +    def choices(self):
  325. +        for choice in self.choice_list.choices:
  326. +            yield choice.dispatch(self.visitor)
  327. +
  328. +    @property
  329. +    def selected(self):
  330. +        return sum(choice.selected for choice in self.choices)
  331. +
  332. +    @property
  333. +    def choice_list(self):
  334. +        return self.node
  335. +
  336. +    @property
  337. +    def at_minimum(self):
  338. +        return self.selected >= self.minimum
  339. +
  340. +    @property
  341. +    def at_maximum(self):
  342. +        return self.maximum is not None and self.selected >= self.maximum
  343. +
  344. +
  345. +@StateVisitor.bind(DisplayList)
  346. +class StateVisitor(Caching):
  347. +
  348. +    @property
  349. +    def display_list(self):
  350. +        return self.node
  351. +
  352. +    @property
  353. +    def is_valid(self):
  354. +        return all(choice.is_valid for choice in self.choices)
  355. +
  356. +    @property
  357. +    def choices(self):
  358. +        for choice in self.display_list.choices:
  359. +            yield choice.dispatch(self.visitor)
  360. +
  361. +    @property
  362. +    def sections(self):
  363. +        for section in self.display_list.sections:
  364. +            yield section.dispatch(self.visitor)
  365. +
  366. +
  367. +@StateVisitor.bind(Section)
  368. +class StateVisitor(Caching):
  369. +
  370. +    @property
  371. +    def section(self):
  372. +        return self.node
  373. +
  374. +    @property
  375. +    def name(self):
  376. +        return self.section.name
  377. +
  378. +    @property
  379. +    def choices(self):
  380. +        predicate = self.section.predicate
  381. +        for choice in self.section.display_list.choices:
  382. +            visited = choice.dispatch(self.visitor)
  383. +            if predicate(visited):
  384. +                yield visited
  385. diff --git a/nobilis.py b/nobilis.py
  386. --- a/nobilis.py
  387. +++ b/nobilis.py
  388. @@ -146,6 +146,130 @@
  389.      ()),
  390.  
  391.  
  392. +GeneralTemplate = collections.namedtuple('GeneralTemplate', 'name indices')
  393. +
  394. +
  395. +FOUNDATION_TEMPLATES = ()
  396. +FOUNDATION_TEMPLATES += GeneralTemplate(
  397. +    'Something Cool', (1, 2, 10, 15)),
  398. +FOUNDATION_TEMPLATES += GeneralTemplate(
  399. +    'In Love with Something', (4, 7, 9, 16)),
  400. +FOUNDATION_TEMPLATES += GeneralTemplate(
  401. +    'Epic, Inhuman, and Powerful', (5, 6, 12, 14)),
  402. +FOUNDATION_TEMPLATES += GeneralTemplate(
  403. +    'Just Plain Weird', (3, 8, 11, 13)),
  404. +
  405. +ESTATE_SOURCE_TEMPLATES = ()
  406. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  407. +    'Light Side of Human Experience', (2, 6, 10, 12)),
  408. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  409. +    'Dark Side of Human Experience', (3, 5, 11, 15)),
  410. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  411. +    'Beautiful Side of the World', (4, 7, 14, 16)),
  412. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  413. +    'Painful Side of the World', (1, 8, 9, 13)),
  414. +
  415. +ESTATE_NATURE_TEMPLATES = ()
  416. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  417. +    'Something You Can Point To', (8, 9, 13, 14, 15)),
  418. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  419. +    'Something You Live', (3, 5, 6, 4, 11, 12)),
  420. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  421. +    'Something You Can Describe', (1, 2, 7, 10, 16)),
  422. +
  423. +ORIGIN_TEMPLATES = ()
  424. +ORIGIN_TEMPLATES += GeneralTemplate('Troubled Life', (2, 5, 6, 11)),
  425. +ORIGIN_TEMPLATES += GeneralTemplate('Humble Life', (8, 9, 14, 15)),
  426. +ORIGIN_TEMPLATES += GeneralTemplate('Blessed Life', (1, 4, 12, 16)),
  427. +ORIGIN_TEMPLATES += GeneralTemplate('Extraordinary Life', (3, 7, 10, 13)),
  428. +
  429. +TROUBLED_TEMPLATES = ()
  430. +TROUBLED_TEMPLATES += GeneralTemplate(
  431. +    "You're Still in Trouble!", (3, 5, 8, 13)),
  432. +TROUBLED_TEMPLATES += GeneralTemplate(
  433. +    'Some Scars Remain', (7, 11, 12, 14)),
  434. +TROUBLED_TEMPLATES += GeneralTemplate(
  435. +    "It's All Happening Again!", (6, 10, 15, 16)),
  436. +TROUBLED_TEMPLATES += GeneralTemplate(
  437. +    'Trouble Inspired Me', (1, 2, 4, 9)),
  438. +
  439. +HUMBLE_TEMPLATES = ()
  440. +HUMBLE_TEMPLATES += GeneralTemplate(
  441. +    'Love for the Ordinary...', (6, 7, 15, 16)),
  442. +HUMBLE_TEMPLATES += GeneralTemplate(
  443. +    'Alienation...', (3, 5, 11, 14)),
  444. +HUMBLE_TEMPLATES += GeneralTemplate(
  445. +    'Transformation...', (4, 8, 12, 13)),
  446. +HUMBLE_TEMPLATES += GeneralTemplate(
  447. +    'Freedom!', (1, 2, 9, 10)),
  448. +
  449. +BLESSED_TEMPLATES = ()
  450. +BLESSED_TEMPLATES += GeneralTemplate('Reverence in Purpose', (2, 4, 10, 13)),
  451. +BLESSED_TEMPLATES += GeneralTemplate('Community', (1, 3, 8, 15)),
  452. +BLESSED_TEMPLATES += GeneralTemplate('Your Way of Life', (7, 9, 14, 16)),
  453. +BLESSED_TEMPLATES += GeneralTemplate('Anger', (5, 6, 9, 12)),
  454. +
  455. +NORMAL_CONTACT_TEMPLATES = ()
  456. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  457. +    'Organization', (1,)),
  458. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  459. +    'Followers', (2,)),
  460. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  461. +    'Excrucian', (3,)),
  462. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  463. +    'Inspirational Friend or Lover', (4,)),
  464. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  465. +    'Nemesis', (5,)),
  466. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  467. +    'Corrupting Influence', (6,)),
  468. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  469. +    'Ghost', (7,)),
  470. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  471. +    'Cammora', (8,)),
  472. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  473. +    'Legacy', (9,)),
  474. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  475. +    'True Love', (10,)),
  476. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  477. +    'Alien(s)', (11,)),
  478. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  479. +    'Manufactured Army', (12,)),
  480. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  481. +    'Faraway or Troubled Love', (13,)),
  482. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  483. +    'Ward', (14,)),
  484. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  485. +    'Moral Friends and Family', (15,)),
  486. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  487. +    'Disciples', (16,)),
  488. +
  489. +SPECIAL_CONTACT_TEMPLATES = ()
  490. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Noble Friend or Enemy', ()),
  491. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Cleave of the Botanists', ()),
  492. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()),
  493. +
  494. +
  495. +AFFLIATION_TEMPLATES = ()
  496. +AFFLIATION_TEMPLATES += GeneralTemplate(
  497. +    '(The Song of) Heaven', (1, 2, 3)),
  498. +AFFLIATION_TEMPLATES += GeneralTemplate(
  499. +    '(The Song of) Hell', (4, 5, 6)),
  500. +AFFLIATION_TEMPLATES += GeneralTemplate(
  501. +    '(The Song of) The Light', (7, 8)),
  502. +AFFLIATION_TEMPLATES += GeneralTemplate(
  503. +    '(The Song of) The Dark', (9, 10)),
  504. +AFFLIATION_TEMPLATES += GeneralTemplate(
  505. +    '(The Song of) The Wild', (11, 12, 13)),
  506. +AFFLIATION_TEMPLATES += GeneralTemplate(
  507. +    'An Independent Song', (14, 15, 16)),
  508. +
  509. +
  510. +BACKGROUND_TEMPLATES = ()
  511. +BACKGROUND_TEMPLATES += GeneralTemplate('Animal', (6, 8, 14, 15)),
  512. +BACKGROUND_TEMPLATES += GeneralTemplate('Strange', (4, 11, 12, 16)),
  513. +BACKGROUND_TEMPLATES += GeneralTemplate('"Sort of" Human', (1, 7, 10, 13)),
  514. +
  515. +
  516.  MIN_SELECTED = 'min_selected'
  517.  MAX_SELECTED = 'max_selected'
  518.  SELECTED = 'selected'
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement