mwchase

LC:Nobilis Update 2

May 7th, 2017
1,344
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. +class StateVisitor(Caching):
  253. +
  254. +    is_valid = True
  255. +
  256. +    selected = False
  257. +
  258. +    @property
  259. +    def choice(self):
  260. +        return self.node
  261. +
  262. +
  263. [email protected](ChoiceDisplay)
  264. +class StateVisitor(ChoiceButton):
  265. +
  266. +    enabled = False
  267. +
  268. +    selected = False
  269. +
  270. +    @property
  271. +    def choice_display(self):
  272. +        return self.node
  273. +
  274. +
  275. [email protected](ChoiceTaken)
  276. +class StateVisitor(ChoiceButton):
  277. +
  278. +    @property
  279. +    def choice_taken(self):
  280. +        return self.node
  281. +
  282. +    # Should this go on UI instead?
  283. +    @property
  284. +    def visible(self):
  285. +        return (super().visible and
  286. +                self.choice_taken.choice.dispatch(self.visitor).selected)
  287. +
  288. +
  289. [email protected](ChoiceListed)
  290. +class StateVisitor(ChoiceButton):
  291. +
  292. +    @property
  293. +    def choice_listed(self):
  294. +        return self.node
  295. +
  296. +    @property
  297. +    def selected(self):
  298. +        return self.choice_listed.choice.dispatch(self.visitor).selected
  299. +
  300. +    @property
  301. +    def enabled(self):
  302. +        parent = self.parent
  303. +        if parent:
  304. +            return self.selected or not parent.at_maximum
  305. +        return True
  306. +
  307. +
  308. [email protected](ChoiceList)
  309. +class StateVisitor(Caching):
  310. +
  311. +    def __init__(self, instance, node):
  312. +        super().__init__(instance, node)
  313. +        self.minimum = node.initial_min
  314. +        self.maximum = node.initial_max
  315. +
  316. +    @property
  317. +    def is_valid(self):
  318. +        if self.maximum is not None and self.selected > self.maximum:
  319. +            return False
  320. +        return all(choice.is_valid for choice in self.choices)
  321. +
  322. +    @property
  323. +    def choices(self):
  324. +        for choice in self.choice_list.choices:
  325. +            yield choice.dispatch(self.visitor)
  326. +
  327. +    @property
  328. +    def selected(self):
  329. +        return sum(choice.selected for choice in self.choices)
  330. +
  331. +    @property
  332. +    def choice_list(self):
  333. +        return self.node
  334. +
  335. +    @property
  336. +    def at_minimum(self):
  337. +        return self.selected >= self.minimum
  338. +
  339. +    @property
  340. +    def at_maximum(self):
  341. +        return self.maximum is not None and self.selected >= self.maximum
  342. +
  343. +
  344. [email protected](DisplayList)
  345. +class StateVisitor(Caching):
  346. +
  347. +    @property
  348. +    def display_list(self):
  349. +        return self.node
  350. +
  351. +    @property
  352. +    def is_valid(self):
  353. +        return all(choice.is_valid for choice in self.choices)
  354. +
  355. +    @property
  356. +    def choices(self):
  357. +        for choice in self.display_list.choices:
  358. +            yield choice.dispatch(self.visitor)
  359. +
  360. +    @property
  361. +    def sections(self):
  362. +        for section in self.display_list.sections:
  363. +            yield section.dispatch(self.visitor)
  364. +
  365. +
  366. +class StateVisitor(Caching):
  367. +
  368. +    @property
  369. +    def section(self):
  370. +        return self.node
  371. +
  372. +    @property
  373. +    def name(self):
  374. +        return self.section.name
  375. +
  376. +    @property
  377. +    def choices(self):
  378. +        predicate = self.section.predicate
  379. +        for choice in self.section.display_list.choices:
  380. +            visited = choice.dispatch(self.visitor)
  381. +            if predicate(visited):
  382. +                yield visited
  383. diff --git a/nobilis.py b/nobilis.py
  384. --- a/nobilis.py
  385. +++ b/nobilis.py
  386. @@ -146,6 +146,130 @@
  387.      ()),
  388.  
  389.  
  390. +GeneralTemplate = collections.namedtuple('GeneralTemplate', 'name indices')
  391. +
  392. +
  393. +FOUNDATION_TEMPLATES = ()
  394. +FOUNDATION_TEMPLATES += GeneralTemplate(
  395. +    'Something Cool', (1, 2, 10, 15)),
  396. +FOUNDATION_TEMPLATES += GeneralTemplate(
  397. +    'In Love with Something', (4, 7, 9, 16)),
  398. +FOUNDATION_TEMPLATES += GeneralTemplate(
  399. +    'Epic, Inhuman, and Powerful', (5, 6, 12, 14)),
  400. +FOUNDATION_TEMPLATES += GeneralTemplate(
  401. +    'Just Plain Weird', (3, 8, 11, 13)),
  402. +
  403. +ESTATE_SOURCE_TEMPLATES = ()
  404. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  405. +    'Light Side of Human Experience', (2, 6, 10, 12)),
  406. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  407. +    'Dark Side of Human Experience', (3, 5, 11, 15)),
  408. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  409. +    'Beautiful Side of the World', (4, 7, 14, 16)),
  410. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  411. +    'Painful Side of the World', (1, 8, 9, 13)),
  412. +
  413. +ESTATE_NATURE_TEMPLATES = ()
  414. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  415. +    'Something You Can Point To', (8, 9, 13, 14, 15)),
  416. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  417. +    'Something You Live', (3, 5, 6, 4, 11, 12)),
  418. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  419. +    'Something You Can Describe', (1, 2, 7, 10, 16)),
  420. +
  421. +ORIGIN_TEMPLATES = ()
  422. +ORIGIN_TEMPLATES += GeneralTemplate('Troubled Life', (2, 5, 6, 11)),
  423. +ORIGIN_TEMPLATES += GeneralTemplate('Humble Life', (8, 9, 14, 15)),
  424. +ORIGIN_TEMPLATES += GeneralTemplate('Blessed Life', (1, 4, 12, 16)),
  425. +ORIGIN_TEMPLATES += GeneralTemplate('Extraordinary Life', (3, 7, 10, 13)),
  426. +
  427. +TROUBLED_TEMPLATES = ()
  428. +TROUBLED_TEMPLATES += GeneralTemplate(
  429. +    "You're Still in Trouble!", (3, 5, 8, 13)),
  430. +TROUBLED_TEMPLATES += GeneralTemplate(
  431. +    'Some Scars Remain', (7, 11, 12, 14)),
  432. +TROUBLED_TEMPLATES += GeneralTemplate(
  433. +    "It's All Happening Again!", (6, 10, 15, 16)),
  434. +TROUBLED_TEMPLATES += GeneralTemplate(
  435. +    'Trouble Inspired Me', (1, 2, 4, 9)),
  436. +
  437. +HUMBLE_TEMPLATES = ()
  438. +HUMBLE_TEMPLATES += GeneralTemplate(
  439. +    'Love for the Ordinary...', (6, 7, 15, 16)),
  440. +HUMBLE_TEMPLATES += GeneralTemplate(
  441. +    'Alienation...', (3, 5, 11, 14)),
  442. +HUMBLE_TEMPLATES += GeneralTemplate(
  443. +    'Transformation...', (4, 8, 12, 13)),
  444. +HUMBLE_TEMPLATES += GeneralTemplate(
  445. +    'Freedom!', (1, 2, 9, 10)),
  446. +
  447. +BLESSED_TEMPLATES = ()
  448. +BLESSED_TEMPLATES += GeneralTemplate('Reverence in Purpose', (2, 4, 10, 13)),
  449. +BLESSED_TEMPLATES += GeneralTemplate('Community', (1, 3, 8, 15)),
  450. +BLESSED_TEMPLATES += GeneralTemplate('Your Way of Life', (7, 9, 14, 16)),
  451. +BLESSED_TEMPLATES += GeneralTemplate('Anger', (5, 6, 9, 12)),
  452. +
  453. +NORMAL_CONTACT_TEMPLATES = ()
  454. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  455. +    'Organization', (1,)),
  456. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  457. +    'Followers', (2,)),
  458. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  459. +    'Excrucian', (3,)),
  460. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  461. +    'Inspirational Friend or Lover', (4,)),
  462. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  463. +    'Nemesis', (5,)),
  464. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  465. +    'Corrupting Influence', (6,)),
  466. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  467. +    'Ghost', (7,)),
  468. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  469. +    'Cammora', (8,)),
  470. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  471. +    'Legacy', (9,)),
  472. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  473. +    'True Love', (10,)),
  474. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  475. +    'Alien(s)', (11,)),
  476. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  477. +    'Manufactured Army', (12,)),
  478. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  479. +    'Faraway or Troubled Love', (13,)),
  480. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  481. +    'Ward', (14,)),
  482. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  483. +    'Moral Friends and Family', (15,)),
  484. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  485. +    'Disciples', (16,)),
  486. +
  487. +SPECIAL_CONTACT_TEMPLATES = ()
  488. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Noble Friend or Enemy', ()),
  489. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Cleave of the Botanists', ()),
  490. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()),
  491. +
  492. +
  493. +AFFLIATION_TEMPLATES = ()
  494. +AFFLIATION_TEMPLATES += GeneralTemplate(
  495. +    '(The Song of) Heaven', (1, 2, 3)),
  496. +AFFLIATION_TEMPLATES += GeneralTemplate(
  497. +    '(The Song of) Hell', (4, 5, 6)),
  498. +AFFLIATION_TEMPLATES += GeneralTemplate(
  499. +    '(The Song of) The Light', (7, 8)),
  500. +AFFLIATION_TEMPLATES += GeneralTemplate(
  501. +    '(The Song of) The Dark', (9, 10)),
  502. +AFFLIATION_TEMPLATES += GeneralTemplate(
  503. +    '(The Song of) The Wild', (11, 12, 13)),
  504. +AFFLIATION_TEMPLATES += GeneralTemplate(
  505. +    'An Independent Song', (14, 15, 16)),
  506. +
  507. +
  508. +BACKGROUND_TEMPLATES = ()
  509. +BACKGROUND_TEMPLATES += GeneralTemplate('Animal', (6, 8, 14, 15)),
  510. +BACKGROUND_TEMPLATES += GeneralTemplate('Strange', (4, 11, 12, 16)),
  511. +BACKGROUND_TEMPLATES += GeneralTemplate('"Sort of" Human', (1, 7, 10, 13)),
  512. +
  513. +
  514.  MIN_SELECTED = 'min_selected'
  515.  MAX_SELECTED = 'max_selected'
  516.  SELECTED = 'selected'
Advertisement
Add Comment
Please, Sign In to add comment