Initial import of MARYSUE sources.
Chris Pressey
9 years ago
0 | MARYSUE | |
1 | ======= | |
2 | ||
3 | Original Generator! Do Not Steal!!1! | |
4 | ||
5 | Built for NaNoGenMo 2015. Self-contained; requires only Python 2.7. | |
6 | ||
7 | Usage: | |
8 | ||
9 | bin/MARYSUE | |
10 | ||
11 | will generate a novel and dump it in the Markdown format. To create an | |
12 | HTML file (requires pandoc) and display it in Firefox, | |
13 | ||
14 | bin/MARYSUE --publish | |
15 | ||
16 | More details TBW. |
0 | #!/usr/bin/env python | |
1 | ||
2 | """Usage: MARYSUE {options} | |
3 | ||
4 | MARYSUE - Original Generator! Do Not Steal!!1! | |
5 | ||
6 | """ | |
7 | ||
8 | # -------- | |
9 | from os.path import realpath, dirname, join | |
10 | import sys | |
11 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src')) | |
12 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..')) | |
13 | # -------- | |
14 | ||
15 | from optparse import OptionParser | |
16 | ||
17 | import marysue.util as random | |
18 | from marysue.plot import * | |
19 | from marysue.plotter import Plotter | |
20 | from marysue.publisher import Novel | |
21 | ||
22 | from stories import serenity | |
23 | ||
24 | ||
25 | ### default chapter configuration ### | |
26 | ||
27 | CHAPTER_COUNT = 40 | |
28 | ||
29 | ||
30 | #print random.randint(0, 100) | |
31 | ||
32 | chapters = [ | |
33 | { | |
34 | 'position': 'beginning', | |
35 | 'plot_min': [ | |
36 | (LoseItem, 1), | |
37 | ], | |
38 | 'plot_max': [ | |
39 | (LoseItem, 1), | |
40 | (Kidnapping, 0), | |
41 | ], | |
42 | }, | |
43 | ] + [ | |
44 | { | |
45 | 'position': 'beginning', | |
46 | } for _ in xrange(0, 9) | |
47 | ] + [ | |
48 | { | |
49 | 'position': 'middle', | |
50 | } for _ in xrange(0, CHAPTER_COUNT - (10 + 10)) | |
51 | ] + [ | |
52 | { | |
53 | 'position': 'end', | |
54 | } for _ in xrange(0, 9) | |
55 | ] + [ | |
56 | { | |
57 | 'position': 'final', | |
58 | 'plot_min': [ | |
59 | (RomanticResolution, 1), | |
60 | ], | |
61 | 'plot_max': [ | |
62 | (RomanticResolution, 1), | |
63 | (AwkwardTension, 0), | |
64 | (RomanticTension, 0), | |
65 | (AwkwardCombat, 0), | |
66 | ], | |
67 | } | |
68 | ] | |
69 | ||
70 | for n, chapter in enumerate(chapters): | |
71 | chapter.setdefault('plot_depth', 5) | |
72 | chapter.setdefault('plot_constraints', []) | |
73 | chapter.setdefault('plot_max', []) | |
74 | chapter.setdefault('plot_min', []) | |
75 | if n < (CHAPTER_COUNT / 4): | |
76 | chapter['plot_max'].append((AwkwardTension, 1)) | |
77 | if n < (CHAPTER_COUNT / 2): | |
78 | chapter['plot_max'].append((RomanticTension, 0)) | |
79 | if chapter['position'] != 'final': | |
80 | chapter['plot_max'].append((RomanticResolution, 0)) | |
81 | ||
82 | ||
83 | ### MAIN ### | |
84 | ||
85 | optparser = OptionParser(__doc__.strip()) | |
86 | optparser.add_option('--debug', action="store_true", default=False, | |
87 | help="trace some things inside the compiler") | |
88 | optparser.add_option('--dump', action="store_true", default=False, | |
89 | help="show story tree in schematic format") | |
90 | optparser.add_option('--plot-depth', default='5', | |
91 | help="depth of plot to generate") | |
92 | optparser.add_option('--plot-max', default=None, | |
93 | help="comma-seperated list of classname:count " | |
94 | "and the generated story will contain at most " | |
95 | "count occurrences of each plot class") | |
96 | optparser.add_option('--plot-min', default=None, | |
97 | help="comma-seperated list of classname:count " | |
98 | "and the generated story will contain at least " | |
99 | "count occurrences of each plot class") | |
100 | optparser.add_option('--synopsis', action="store_true", default=False, | |
101 | help="just dump a synopsis of the plot") | |
102 | optparser.add_option('--disable-shuffle-demon', action="store_true", default=False, | |
103 | help="disable the Shuffle Demon") | |
104 | optparser.add_option('--publish', action="store_true", default=False, | |
105 | help="generate an HTML5 file and open in browser " | |
106 | "(requires pandoc and firefox)") | |
107 | ||
108 | (options, args) = optparser.parse_args(sys.argv[1:]) | |
109 | ||
110 | ||
111 | ### configure things ### | |
112 | ||
113 | if options.disable_shuffle_demon: | |
114 | random.shuffle_demon.enabled = False | |
115 | ||
116 | ||
117 | def parse_plot_constraint(s): | |
118 | from marysue.plot import get_plot_class | |
119 | name, count = s.split(':') | |
120 | return (get_plot_class(name), count) | |
121 | ||
122 | ||
123 | plot_min = [parse_plot_constraint(s) for s in options.plot_min.split(',')] if options.plot_min else () | |
124 | plot_max = [parse_plot_constraint(s) for s in options.plot_max.split(',')] if options.plot_max else () | |
125 | ||
126 | ||
127 | generate_front_matter = True | |
128 | if plot_min or plot_max: | |
129 | generate_front_matter = False | |
130 | chapters = ( | |
131 | { | |
132 | 'plot_min': plot_min, | |
133 | 'plot_max': plot_max, | |
134 | 'plot_depth': int(options.plot_depth), | |
135 | }, | |
136 | ) | |
137 | ||
138 | ### do the generation! ### | |
139 | ||
140 | novel = Novel( | |
141 | chapters, | |
142 | generate_front_matter=generate_front_matter, | |
143 | synopsis=options.synopsis, | |
144 | dump=options.dump | |
145 | ) | |
146 | ||
147 | for n, chapter in enumerate(chapters): | |
148 | plotter = Plotter( | |
149 | serenity.protagonists, | |
150 | serenity.antagonists, | |
151 | serenity.goons, | |
152 | serenity.macguffins, | |
153 | serenity.settings, | |
154 | ) | |
155 | novel.generate_chapter(n, plotter, **chapter) | |
156 | if n % 6 == 5: | |
157 | serenity.serenity.promote() | |
158 | ||
159 | novel.trim() | |
160 | ||
161 | ### publish | |
162 | ||
163 | if options.publish: | |
164 | novel.publish() | |
165 | else: | |
166 | print novel.text |
0 | h1 { | |
1 | font-size: 300%; | |
2 | text-align: center; | |
3 | margin-left: 16%; | |
4 | margin-right: 16%; | |
5 | text-decoration: underline; | |
6 | margin-bottom: 1em; | |
7 | margin-top: 4em; | |
8 | } | |
9 | ||
10 | h2 { | |
11 | font-size: 200%; | |
12 | text-align: center; | |
13 | margin-top: 8em; | |
14 | margin-left: 16%; | |
15 | margin-right: 16%; | |
16 | } | |
17 | ||
18 | h3 { | |
19 | font-size: 200%; | |
20 | text-align: center; | |
21 | margin-bottom: 8em; | |
22 | margin-left: 16%; | |
23 | margin-right: 16%; | |
24 | } | |
25 | ||
26 | p { | |
27 | font-size: 125%; | |
28 | margin-left: 16%; | |
29 | margin-right: 16%; | |
30 | } | |
31 | ||
32 | hr { | |
33 | width: 33%; | |
34 | margin-top: 3em; | |
35 | margin-bottom: 3em; | |
36 | } | |
37 | ||
38 | ol { | |
39 | font-size: 110%; | |
40 | text-align: center; | |
41 | }⏎ |
0 | # encoding: UTF-8 | |
1 | ||
2 | """Abstract Story Trees. | |
3 | ||
4 | """ | |
5 | ||
6 | import re | |
7 | ||
8 | import marysue.util as random | |
9 | ||
10 | ||
11 | ALIASES = { | |
12 | 'indef': 'indefinite', | |
13 | 'def': 'definite', | |
14 | 'obj': 'object', | |
15 | 'sub': 'subject', | |
16 | 'subj': 'subject', | |
17 | 'his': 'possessive_pronoun', | |
18 | 'her': 'possessive_pronoun', | |
19 | 'he': 'pronoun', | |
20 | 'she': 'pronoun', | |
21 | } | |
22 | ||
23 | ||
24 | class AST(object): | |
25 | slots = None | |
26 | templates = None | |
27 | ||
28 | def __init__(self, *args, **kwargs): | |
29 | self._children = args | |
30 | if self.slots is not None: | |
31 | for key, value in kwargs.iteritems(): | |
32 | if key not in self.slots: | |
33 | raise AttributeError( | |
34 | "{0} has no attribute '{1}'".format( | |
35 | self.__class__.__name__, key | |
36 | ) | |
37 | ) | |
38 | for key in self.slots: | |
39 | kwargs.setdefault(key, None) | |
40 | self._attrs = kwargs | |
41 | ||
42 | def __repr__(self): | |
43 | children = ', '.join([repr(c) for c in self._children]) | |
44 | attrs = ', '.join(['%s=%r' % (key, value) for key, value in self._attrs.iteritems() if value is not None]) | |
45 | j = ', ' if children and attrs else '' | |
46 | return "%s(%s%s%s)" % (self.__class__.__name__, children, j, attrs) | |
47 | ||
48 | def repr_abbrev(self): | |
49 | attrs = ', '.join(['%s=%r' % (key, value) for key, value in self._attrs.iteritems() if value is not None]) | |
50 | return "%s(%s)" % (self.__class__.__name__, attrs) | |
51 | ||
52 | def dump(self, f, indent=0): | |
53 | f.write(' ' * indent) | |
54 | f.write(self.repr_abbrev()) | |
55 | f.write('\n') | |
56 | for child in self: | |
57 | child.dump(f, indent + 2) | |
58 | ||
59 | def __getitem__(self, index): | |
60 | return self._children[index] | |
61 | ||
62 | def __iter__(self): | |
63 | return self._children.__iter__() | |
64 | ||
65 | def __getattr__(self, name): | |
66 | if name in self._attrs: | |
67 | return self._attrs[name] | |
68 | raise AttributeError(name) | |
69 | ||
70 | def iteritems(self): | |
71 | return self._attrs.iteritems() | |
72 | ||
73 | def flatten(self): | |
74 | """Individual nodes which you want to flatten must define how they are to be flattened""" | |
75 | return self.__class__(*[c.flatten() for c in self], **dict(self.iteritems())) | |
76 | ||
77 | def insert(self, position, *args, **kwargs): | |
78 | """Returns a new AST node""" | |
79 | children = [c for c in self] | |
80 | for a in args: | |
81 | children.insert(position, a) | |
82 | position += 1 | |
83 | attrs = dict((k, v) for k, v in self.iteritems()) | |
84 | attrs.update(kwargs) | |
85 | return self.__class__(*children, **attrs) | |
86 | ||
87 | def render_t_impl(self, template): | |
88 | ||
89 | def pick_one(match): | |
90 | return random.choice(tuple(match.group(1).split('|'))) | |
91 | template = re.sub(r'\<(.*?)\>', pick_one, template) | |
92 | ||
93 | def repl(match): | |
94 | parts = [ALIASES.get(part, part) for part in match.group(1).split('.') if part] | |
95 | try: | |
96 | obj = self._attrs[parts.pop(0)] | |
97 | except KeyError: | |
98 | print self | |
99 | raise | |
100 | while parts: | |
101 | obj = getattr(obj, parts.pop(0)) | |
102 | return obj | |
103 | return re.sub(r'\{([a-zA-Z0-9_.]+)\}', repl, template) | |
104 | ||
105 | def render_t(self, template): | |
106 | try: | |
107 | return self.render_t_impl(template) | |
108 | except Exception: | |
109 | ||
110 | print "!!! error in template '%s' on ast %r" % (template, self) | |
111 | ||
112 | raise | |
113 | ||
114 | def render(self): | |
115 | if self.templates: | |
116 | # TODO why is random.choice() not sufficient here? | |
117 | template = random.shuffle_demon.choice(self.templates) | |
118 | return self.render_t(template) | |
119 | raise NotImplementedError(repr(self)) | |
120 | ||
121 | def __unicode__(self): | |
122 | return self.render() | |
123 | ||
124 | ||
125 | # TODO AST should inherit from this | |
126 | ||
127 | class Properties(object): | |
128 | """Please treat as immutable""" | |
129 | ||
130 | slots = None | |
131 | ||
132 | def __init__(self, **kwargs): | |
133 | if self.slots is not None: | |
134 | for key, value in kwargs.iteritems(): | |
135 | if key not in self.slots: | |
136 | raise AttributeError( | |
137 | "{0} has no attribute '{1}'".format( | |
138 | self.__class__.__name__, key | |
139 | ) | |
140 | ) | |
141 | for key in self.slots: | |
142 | kwargs.setdefault(key, None) | |
143 | self._attrs = kwargs | |
144 | ||
145 | def __repr__(self): | |
146 | attrs = ', '.join(['%s=%r' % (key, value) for key, value in self._attrs.iteritems() if value is not None]) | |
147 | return "%s(%s)" % (self.__class__.__name__, attrs) | |
148 | ||
149 | def __getattr__(self, name): | |
150 | if name in self._attrs: | |
151 | return self._attrs[name] | |
152 | raise AttributeError(name) | |
153 | ||
154 | def __setattr__(self, name, value): | |
155 | if name not in ('_attrs',): | |
156 | raise AttributeError(name) | |
157 | return super(Properties, self).__setattr__(name, value) | |
158 | ||
159 | def iteritems(self): | |
160 | return self._attrs.iteritems() | |
161 | ||
162 | def clone(self, **kwargs): | |
163 | attrs = self._attrs.copy() | |
164 | attrs.update(kwargs) | |
165 | return self.__class__(**attrs) |
0 | import marysue.util as random | |
1 | from marysue.objects import Proper, MasculineMixin, FeminineMixin, Group | |
2 | ||
3 | ||
4 | # - - - - ranks - - - - | |
5 | ||
6 | RANKS = ( | |
7 | 'Ensign', | |
8 | 'Lieutenant', | |
9 | 'Lieutenant Commander', | |
10 | 'Commander', | |
11 | 'Captain', | |
12 | 'Commodore', | |
13 | 'Admiral', | |
14 | 'Super Admiral', | |
15 | ) | |
16 | ||
17 | ||
18 | class Character(Proper): | |
19 | def __init__(self, names, **kwargs): | |
20 | super(Character, self).__init__(names, **kwargs) | |
21 | self.stature = random.choice(( | |
22 | 'somewhat short', | |
23 | 'of average height', | |
24 | 'rather tall', | |
25 | )) | |
26 | self.hair_length = random.choice(( | |
27 | 'long', | |
28 | 'shoulder length', | |
29 | 'short', | |
30 | 'close cropped', | |
31 | )) | |
32 | self.hair_colour = random.choice(( | |
33 | 'blonde', 'brown', 'red', 'auburn', 'black', | |
34 | )) | |
35 | self.eye_colour = random.choice(( | |
36 | 'brown', 'blue', 'grey', 'green', 'hazel', | |
37 | )) | |
38 | self.war_cry = "BY THE " + random.choice(( | |
39 | 'MOONS', | |
40 | 'RINGS', | |
41 | 'MOUNTAINS', | |
42 | 'METEORS', | |
43 | )) + " OF " + random.choice(( | |
44 | 'VENUS', | |
45 | 'MARS', | |
46 | 'JUPITER', | |
47 | 'NEPTUNE', | |
48 | )) | |
49 | ||
50 | @classmethod | |
51 | def characters_to_set(cls, *args): | |
52 | """Not the most intuitive place for this method? Oh well.""" | |
53 | s = set() | |
54 | for arg in args: | |
55 | if arg is None: | |
56 | continue | |
57 | if isinstance(arg, Group): | |
58 | for c in arg: | |
59 | s.add(c) | |
60 | else: | |
61 | s.add(arg) | |
62 | return set([p for p in s if isinstance(p, cls)]) | |
63 | ||
64 | def promote(self): | |
65 | """Mutates this character. One of the few methods that'll do that.""" | |
66 | self.rank = self.next_rank | |
67 | ||
68 | @property | |
69 | def next_rank(self): | |
70 | for n, r in enumerate(RANKS): | |
71 | if r == self.rank: | |
72 | return RANKS[n + 1] | |
73 | ||
74 | @property | |
75 | def costume_materials(self): | |
76 | return ( | |
77 | 'silk', 'leather', | |
78 | 'polyester', 'cotton', 'nylon', 'denim', | |
79 | 'rayon', 'dacron', 'crinkly foil', | |
80 | # 'woolen' is far too weird for most things, esp. footwear | |
81 | # 'suede' is likewise a little weird | |
82 | ) | |
83 | ||
84 | @property | |
85 | def costume_decorations(self): | |
86 | return ( | |
87 | ' with {colour} stripes', | |
88 | ' with {colour} {costume_material} trim', | |
89 | ) | |
90 | ||
91 | @property | |
92 | def costume_decoration(self): | |
93 | if random.chance(75): | |
94 | return '' | |
95 | return random.choice(self.costume_decorations).format( | |
96 | colour=self.colour, | |
97 | costume_material=random.choice(self.costume_materials), | |
98 | ) | |
99 | ||
100 | @property | |
101 | def costume_adjectives(self): | |
102 | return ( | |
103 | 'fine', | |
104 | 'snappy', | |
105 | 'handsome', | |
106 | ) | |
107 | ||
108 | @property | |
109 | def costume_adjective(self): | |
110 | if random.chance(60): | |
111 | return '' | |
112 | else: | |
113 | return random.choice(self.costume_adjectives) | |
114 | ||
115 | @property | |
116 | def wearing(self): | |
117 | return random.choice(( | |
118 | 'wearing', 'sporting', 'looking fine in', | |
119 | 'looking smashing in', 'looking delightful in', | |
120 | 'looking impressive in', 'decked out in' | |
121 | )) | |
122 | ||
123 | @property | |
124 | def yet(self): | |
125 | return '{} yet {}'.format( | |
126 | random.choice(( | |
127 | 'smooth', 'graceful', 'gentle', 'supple', 'soft', 'exquisite', | |
128 | )), | |
129 | random.choice(( | |
130 | 'powerful', 'forceful', 'firm', 'masterful', 'confident', 'strong', | |
131 | )) | |
132 | ) | |
133 | ||
134 | @property | |
135 | def motion(self): | |
136 | if random.chance(80): | |
137 | return '' | |
138 | return 'with a {} motion, '.format(self.yet) | |
139 | ||
140 | @property | |
141 | def simile(self): | |
142 | return 'like {} {}'.format( | |
143 | random.choice(( | |
144 | 'a tiger', | |
145 | 'charcoal', | |
146 | 'an elephant', | |
147 | 'a ninja', | |
148 | 'a gangster', | |
149 | 'a giraffe', | |
150 | 'a hurricane', | |
151 | 'a samurai', | |
152 | 'a rabid dog', | |
153 | 'a crazed bull', | |
154 | 'a baboon', | |
155 | 'a gorilla', | |
156 | )), | |
157 | random.choice(( | |
158 | 'in a snowstorm', | |
159 | 'in a rainstorm', | |
160 | 'piloting a helicopter', | |
161 | 'driving a race car', | |
162 | 'in a gymnasium parking lot', | |
163 | 'in a pet shop', | |
164 | 'in a bazaar', | |
165 | 'in a video arcade', | |
166 | 'at a baseball game', | |
167 | 'in a performance art piece', | |
168 | 'in a jewellery store', | |
169 | 'in a mosh pit', | |
170 | 'at a square dance', | |
171 | 'at a monster truck rally', | |
172 | )) | |
173 | ) | |
174 | ||
175 | @property | |
176 | def withvoice(self): | |
177 | return 'with a voice ' + self.simile | |
178 | ||
179 | ||
180 | class MasculineCharacter(MasculineMixin, Character): | |
181 | def __init__(self, names, **kwargs): | |
182 | super(MasculineCharacter, self).__init__(names, **kwargs) | |
183 | self.feature_adj = random.choice(( | |
184 | 'strong', 'deep', 'wide', 'large', | |
185 | )) | |
186 | self.feature = random.choice(( | |
187 | 'nose', 'mouth', 'brow', 'chin', | |
188 | )) | |
189 | ||
190 | ||
191 | class FeminineCharacter(FeminineMixin, Character): | |
192 | def __init__(self, names, **kwargs): | |
193 | super(FeminineCharacter, self).__init__(names, **kwargs) | |
194 | self.feature_adj = random.choice(( | |
195 | 'small', 'perky', 'narrow', 'large', | |
196 | )) | |
197 | self.feature = random.choice(( | |
198 | 'nose', 'mouth', 'forehead', 'chin', | |
199 | )) | |
200 | ||
201 | ||
202 | class MarySue(FeminineCharacter): | |
203 | def __init__(self, names, **kwargs): | |
204 | super(MarySue, self).__init__(names, **kwargs) | |
205 | self.feature_adj = random.choice(( | |
206 | 'wicked cute', 'perfect', 'beautiful', 'enchanting', | |
207 | )) | |
208 | self.feature = random.choice(( | |
209 | 'nose', 'mouth', 'forehead', 'chin', | |
210 | )) | |
211 | self.stature = random.choice(( | |
212 | 'enchantingly petite', | |
213 | 'a bit on the short side, but in a cute way', | |
214 | 'neither short nor tall but not average either', | |
215 | 'sort of tall for a girl, but not in a bad way', | |
216 | )) | |
217 | self.hair_length = random.choice(( | |
218 | 'exceptionally long (down to her knees)', | |
219 | 'wicked long (down to her knees)', | |
220 | 'beautiful long', | |
221 | 'perky shoulder length', | |
222 | )) | |
223 | self.hair_colour = random.choice(( | |
224 | 'purple', 'indigo', 'violet', 'midnight black', | |
225 | 'black and purple', | |
226 | 'multi coloured', 'rainbow coloured', 'multi hued', | |
227 | 'rainbow hued', 'shimmering rainbow coloured', | |
228 | 'shimmering multi hued', 'shimmering rainbow hued', | |
229 | )) | |
230 | self.eye_colour = random.choice(( | |
231 | 'purple', 'indigo', 'violet', 'icy blue', | |
232 | 'multi coloured', 'rainbow coloured', 'multi hued', 'rainbow hued', | |
233 | 'shimmering multi coloured', 'shimmering rainbow coloured', 'shimmering multi hued', 'shimmering rainbow hued', | |
234 | 'kaleidoscope coloured', 'shimmering kaleidoscope coloured', | |
235 | )) | |
236 | ||
237 | @property | |
238 | def colours(self): | |
239 | return ( | |
240 | 'purple', 'dark purple', 'pale purple', | |
241 | 'violet', 'dark violet', 'pale violet', | |
242 | 'indigo', 'dark indigo', 'pale indigo', | |
243 | 'red and purple', 'purple and gold', 'black and purple', | |
244 | 'crimson', 'crimson and purple', 'crimson and violet', | |
245 | 'purple and white', 'purple and violet', 'blue and purple', | |
246 | 'midnight black', 'white', | |
247 | ||
248 | 'multi coloured', 'rainbow coloured', 'multi hued', | |
249 | 'rainbow hued', 'shimmering rainbow coloured', | |
250 | 'shimmering multi hued', 'shimmering rainbow hued', | |
251 | ||
252 | 'gold coloured', 'silver coloured', 'silvery', | |
253 | 'silver and purple', 'shiny silver', 'shiny purple', | |
254 | 'deep violet', 'deep indigo', 'shimmering purple', | |
255 | ) | |
256 | ||
257 | @property | |
258 | def costume_decorations(self): | |
259 | return ( | |
260 | ' with frilly {colour} lace', | |
261 | ' with {colour} lacy frills', | |
262 | ' with {colour} lightning bolt patterns', | |
263 | ' with {colour} star patterns', | |
264 | ' with {colour} moon beam patterns', | |
265 | ' adorned with {gems}', | |
266 | ' with {colour} {costume_material} trim', | |
267 | ) | |
268 | ||
269 | @property | |
270 | def costume_decoration(self): | |
271 | return random.choice(self.costume_decorations).format( | |
272 | colour=self.colour, | |
273 | costume_material=random.choice(self.costume_materials), | |
274 | gems=random.choice(( | |
275 | 'jewels', 'gems', 'rubies', 'emeralds', 'sapphires', | |
276 | )), | |
277 | ) | |
278 | ||
279 | @property | |
280 | def costume_adjectives(self): | |
281 | return ( | |
282 | '', | |
283 | 'beautiful', | |
284 | 'elegant', | |
285 | 'exquisite', | |
286 | 'fantastic', | |
287 | 'marvellous', | |
288 | ) | |
289 | ||
290 | @property | |
291 | def costume_adjective(self): | |
292 | if random.chance(4): | |
293 | return 'WICKED AWESOME' | |
294 | else: | |
295 | return random.choice(self.costume_adjectives) | |
296 | ||
297 | ||
298 | class DreamBoat(MasculineCharacter): | |
299 | def __init__(self, names, **kwargs): | |
300 | super(DreamBoat, self).__init__(names, **kwargs) | |
301 | self.feature_adj = random.choice(( | |
302 | 'handsome', 'perfect', 'beautiful', 'enthralling', | |
303 | )) | |
304 | self.feature = random.choice(( | |
305 | 'nose', 'mouth', 'brow', 'chin', | |
306 | )) | |
307 | self.stature = random.choice(( | |
308 | 'a bit on the short side, but in a cute way', | |
309 | 'of perfectly normal height like a normal person should be', | |
310 | 'quite tall, but really handsomely tall, not freakishly tall', | |
311 | )) | |
312 | ||
313 | ||
314 | class Rival(FeminineCharacter): | |
315 | @property | |
316 | def wearing(self): | |
317 | return random.choice(( | |
318 | 'wearing', 'looking frumpy in', | |
319 | 'looking tastless in', 'looking completely unimpressive in', | |
320 | 'gotten up in', | |
321 | )) | |
322 | ||
323 | @property | |
324 | def costume_decorations(self): | |
325 | return ( | |
326 | ' with gaudy {colour} polka dots', | |
327 | ' with a tacky {colour} zig zag pattern', | |
328 | ) | |
329 | ||
330 | @property | |
331 | def costume_adjectives(self): | |
332 | return ( | |
333 | '', | |
334 | 'ill fitting', | |
335 | 'tacky', | |
336 | 'tasteless', | |
337 | 'gaudy', | |
338 | 'bleak', | |
339 | 'outdated', | |
340 | ) | |
341 | ||
342 | ||
343 | class TheOptimist(MasculineCharacter): | |
344 | @property | |
345 | def wearing(self): | |
346 | return random.choice(( | |
347 | 'wearing', 'sporting', 'looking exciting in', | |
348 | 'resplendent in', 'looking ready for action in', | |
349 | )) | |
350 | ||
351 | ||
352 | class BaddieMixin(object): | |
353 | @property | |
354 | def wearing(self): | |
355 | return random.choice(( | |
356 | 'wearing', 'looking menacing in', | |
357 | 'looking villanous in', 'looking impressive in', | |
358 | 'gotten up in', 'looking intimidating in', | |
359 | )) | |
360 | ||
361 | @property | |
362 | def colours(self): | |
363 | return ( | |
364 | 'black', 'midnight black', 'dark black', 'deep black', | |
365 | 'red', 'dark red', 'blood red', 'blood coloured', 'deep red', | |
366 | ) | |
367 | ||
368 | ||
369 | class BadGuy(BaddieMixin, MasculineCharacter): | |
370 | def __init__(self, names, **kwargs): | |
371 | super(BadGuy, self).__init__(names, **kwargs) | |
372 | self.feature_adj = random.choice(( | |
373 | 'ugly', 'jutting', 'overbearing', 'scarred', | |
374 | )) | |
375 | self.feature = random.choice(( | |
376 | 'nose', 'mouth', 'brow', 'chin', | |
377 | )) | |
378 | self.hair_colour = random.choice(( | |
379 | 'black', 'platinum blonde', 'white', 'green', | |
380 | )) | |
381 | ||
382 | ||
383 | class BadGal(BaddieMixin, FeminineCharacter): | |
384 | def __init__(self, names, **kwargs): | |
385 | super(BadGal, self).__init__(names, **kwargs) | |
386 | self.feature_adj = random.choice(( | |
387 | 'ugly', 'jutting', 'overbearing', 'scarred', | |
388 | )) | |
389 | self.feature = random.choice(( | |
390 | 'nose', 'mouth', 'brow', 'chin', | |
391 | )) | |
392 | self.hair_colour = random.choice(( | |
393 | 'black', 'platinum blonde', 'white', 'green', | |
394 | )) |
0 | import marysue.util as random | |
1 | from marysue.objects import Object, Plural, MasculineMixin, FeminineMixin | |
2 | ||
3 | ||
4 | # since the first argument is always a Character, arguably, these | |
5 | # functions should all be methods on Character instead. Oh well. | |
6 | ||
7 | ||
8 | def make_costume(character, item_choices): | |
9 | adjective = character.costume_adjective | |
10 | if adjective: | |
11 | adjective = adjective + ' ' | |
12 | colour = character.colour | |
13 | material = random.choice(character.costume_materials) | |
14 | (item, cls_) = random.choice(item_choices) | |
15 | with_ = character.costume_decoration | |
16 | ||
17 | names = ( | |
18 | '{}{} {} {}{}'.format(adjective, colour, material, item, with_), | |
19 | item | |
20 | ) | |
21 | ||
22 | return cls_(names=names) | |
23 | ||
24 | ||
25 | def make_torso_costume(character): | |
26 | item_choices = ( | |
27 | 'jacket', 'shirt', 'jerkin', 'top', 'jersey', 'suit jacket', | |
28 | 'sweater', 'hoodie', 'jumper', | |
29 | ) | |
30 | ||
31 | if isinstance(character, FeminineMixin): | |
32 | item_choices += ('blouse', 'halter top', 'tank top', 'frock',) | |
33 | ||
34 | if isinstance(character, MasculineMixin): | |
35 | item_choices += ('muscle shirt',) | |
36 | ||
37 | return make_costume(character, tuple((item, Object) for item in item_choices)) | |
38 | ||
39 | ||
40 | def make_legs_costume(character): | |
41 | item_choices = tuple((item, Plural) for item in ( | |
42 | 'trousers', 'leggings', 'slacks', 'culottes', | |
43 | # 'hose', | |
44 | )) | |
45 | ||
46 | if isinstance(character, FeminineMixin): | |
47 | item_choices += (('skirt', Object),) | |
48 | ||
49 | return make_costume(character, item_choices) | |
50 | ||
51 | ||
52 | def make_onesie_costume(character): | |
53 | item_choices = tuple((item, Object) for item in ( | |
54 | 'jumpsuit', 'track suit', 'robe', 'smock', | |
55 | 'long coat', 'trench coat', 'great coat', | |
56 | )) + (('coveralls', Plural),) | |
57 | ||
58 | if isinstance(character, FeminineMixin): | |
59 | item_choices += tuple((item, Object) for item in ( | |
60 | 'dress', 'gown', 'leotard', | |
61 | )) | |
62 | ||
63 | return make_costume(character, item_choices) | |
64 | ||
65 | ||
66 | def make_feet_costume(character): | |
67 | item_choices = tuple((item, Plural) for item in ( | |
68 | 'boots', 'shoes', 'sandals', 'sneakers', 'trainers', | |
69 | 'dress shoes' | |
70 | )) | |
71 | ||
72 | if isinstance(character, FeminineMixin): | |
73 | item_choices += (('pixie boots', Plural), ('pumps', Plural)) | |
74 | ||
75 | return make_costume(character, item_choices) |
0 | from marysue.objects import Object | |
1 | ||
2 | ||
3 | class Duty(Object): | |
4 | pass | |
5 | ||
6 | ||
7 | class RescueDuty(Duty): | |
8 | def __init__(self, object, **kwargs): | |
9 | name = 'rescue ' + object.name | |
10 | super(RescueDuty, self).__init__((name,), **kwargs) | |
11 | ||
12 | ||
13 | class RetrieveDuty(Duty): | |
14 | def __init__(self, object, **kwargs): | |
15 | name = 'retrieve ' + object.definite | |
16 | super(RetrieveDuty, self).__init__((name,), **kwargs) |
0 | import sys | |
1 | ||
2 | import marysue.util as random | |
3 | from marysue.objects import Object | |
4 | from marysue.characters import Character, MarySue, TheOptimist | |
5 | from marysue.storytree import Story, Scene, EventSequence, Paragraph | |
6 | from marysue.events import * | |
7 | from marysue.state import State | |
8 | from marysue.costume import ( | |
9 | make_torso_costume, make_legs_costume, | |
10 | make_onesie_costume, make_feet_costume | |
11 | ) | |
12 | ||
13 | ||
14 | def edit_story(story, introduced, **kwargs): | |
15 | """Standard story-revising pipeline. Takes a basic Story | |
16 | that was generated from a plot and returns a Story that is | |
17 | closer to something that you can actually read. | |
18 | ||
19 | `introduced` is a set of objects that have already been | |
20 | introduced in previous stories and that need no introduction | |
21 | here. Objects will be added to it as they are introduced in | |
22 | this story. | |
23 | """ | |
24 | ||
25 | ### Massage the generated story ### | |
26 | ||
27 | story = merge_adjacent_scenes(story) | |
28 | story = resolve_setting_references(story) | |
29 | ||
30 | ### Initialize story instant-states and context ### | |
31 | ||
32 | all_objects = set() | |
33 | collect_objects(story, all_objects) | |
34 | story = assign_empty_states(story, objects=all_objects) | |
35 | ||
36 | ### Assign locations ### | |
37 | ||
38 | story = assign_locations(story) | |
39 | ||
40 | ### Assign moods and duties ### | |
41 | ||
42 | story = assign_moods(story) | |
43 | story = remove_mood_modifier_events(story) | |
44 | ||
45 | story = assign_duties(story) | |
46 | story = remove_duty_acquisition_events(story) | |
47 | ||
48 | ### Elaborate the story ### | |
49 | ||
50 | story = describe_scene(story) | |
51 | ||
52 | story = assign_costumes(story) | |
53 | story = describe_costumes(story) | |
54 | ||
55 | newly_introduced = set() | |
56 | story = describe_characters(story, introduced, newly_introduced) | |
57 | reminded = set(newly_introduced) | |
58 | story = remind_characters(story, reminded) | |
59 | ||
60 | ### Diction ### | |
61 | ||
62 | story = assign_first_occurrence(story) | |
63 | ||
64 | story = story.flatten() | |
65 | ||
66 | story = split_into_paragraphs(story) | |
67 | story = assign_referents(story) | |
68 | story = conjoin_sentences(story) | |
69 | ||
70 | return story | |
71 | ||
72 | ||
73 | # - - - - for debugging - - - | |
74 | ||
75 | ||
76 | def show_state(ast, attr, cls): | |
77 | """Example usage: show_state(story, 'subject', StateDutyEvent)""" | |
78 | for child in ast: | |
79 | show_state(child, cls) | |
80 | if isinstance(ast, cls): | |
81 | print "%s state is: %r" % (attr, getattr(ast, attr)) | |
82 | ||
83 | ||
84 | ||
85 | # - - - - editor stages - - - - | |
86 | ||
87 | ||
88 | def collect_objects(ast, object_set): | |
89 | """Places all the objects found in the given story tree into the given | |
90 | object_set. Does not return anything, modifies object_set instead.""" | |
91 | for child in ast: | |
92 | collect_objects(child, object_set) | |
93 | for key, value in ast.iteritems(): | |
94 | if isinstance(value, Object): | |
95 | object_set.add(value) | |
96 | ||
97 | ||
98 | def assign_empty_states(ast, context=None, objects=None): | |
99 | """Replaces all Object references in a story tree with State objects | |
100 | which proxy for those Objects. These State objects are initially | |
101 | empty; further passes will make them reflect what's actually going on.""" | |
102 | ||
103 | if context is None: | |
104 | context = dict((object, State(object)) for object in objects) | |
105 | ||
106 | attrs = {} | |
107 | for key, value in ast.iteritems(): | |
108 | if isinstance(value, Object): | |
109 | attrs[key] = context[value] | |
110 | else: | |
111 | attrs[key] = value | |
112 | return ast.__class__(*[assign_empty_states(c, context=context) for c in ast], **attrs) | |
113 | ||
114 | ||
115 | def assign_locations(ast, context=None): | |
116 | # And this is entirely so we can say "Her scarf shone in the dim light of the tunnel!" | |
117 | ||
118 | if context is None: | |
119 | context = {} | |
120 | ||
121 | if isinstance(ast, Scene): | |
122 | context['location'] = ast.setting | |
123 | ||
124 | attrs = {} | |
125 | for role, state in ast.iteritems(): | |
126 | if isinstance(state, State) and isinstance(state.object, Character): | |
127 | state = state.clone(location=context['location']) | |
128 | attrs[role] = state | |
129 | ||
130 | return ast.__class__(*[assign_locations(c, context=context) for c in ast], **attrs) | |
131 | ||
132 | ||
133 | def assign_costumes(ast, context=None): | |
134 | if context is None: | |
135 | context = {} | |
136 | ||
137 | # reset costumes in each scene | |
138 | if isinstance(ast, Scene): | |
139 | context = {} | |
140 | ||
141 | attrs = {} | |
142 | for role, state in ast.iteritems(): | |
143 | if isinstance(state, State) and isinstance(state.object, Character): | |
144 | character = state.object | |
145 | if state.object not in context: | |
146 | context[character] = { | |
147 | 'feet': make_feet_costume(character), | |
148 | } | |
149 | if random.chance(66): | |
150 | context[character].update({ | |
151 | 'torso': make_torso_costume(character), | |
152 | 'legs': make_legs_costume(character) | |
153 | }) | |
154 | else: | |
155 | context[character].update({ | |
156 | 'torso': make_onesie_costume(character), | |
157 | 'legs': None | |
158 | }) | |
159 | ||
160 | entry = context[character] | |
161 | state = state.clone( | |
162 | torso_costume=entry['torso'], | |
163 | legs_costume=entry['legs'], | |
164 | feet_costume=entry['feet'], | |
165 | ) | |
166 | attrs[role] = state | |
167 | ||
168 | return ast.__class__(*[assign_costumes(c, context) for c in ast], **attrs) | |
169 | ||
170 | ||
171 | def assign_moods(ast, moods=None): | |
172 | if moods is None: | |
173 | moods = {} | |
174 | ||
175 | if isinstance(ast, MoodModifierEvent): | |
176 | moods[ast.subject.object] = ast.mood() | |
177 | ||
178 | attrs = {} | |
179 | for role, state in ast.iteritems(): | |
180 | if isinstance(state, State) and isinstance(state.object, Character): | |
181 | character = state.object | |
182 | if character not in moods: | |
183 | print "ERROR", character, "appears before mood assigned, assuming happy" | |
184 | moods[character] = 'happy' | |
185 | if isinstance(character, TheOptimist): | |
186 | # No, I'm not going to let it get me down! | |
187 | moods[character] = 'happy' | |
188 | state = state.clone(mood=moods[character]) | |
189 | attrs[role] = state | |
190 | ||
191 | return ast.__class__(*[assign_moods(c, moods=moods) for c in ast], **attrs) | |
192 | ||
193 | ||
194 | def remove_mood_modifier_events(ast): | |
195 | children = [] | |
196 | for child in ast: | |
197 | if isinstance(child, MoodModifierEvent): | |
198 | if random.chance(10) and isinstance(child.subject.object, TheOptimist) and child.mood() != 'happy': | |
199 | children.append(CharacterStaysHappyEvent(subject=child.subject)) | |
200 | else: | |
201 | children.append(remove_mood_modifier_events(child)) | |
202 | ||
203 | return ast.__class__(*children, **dict(ast.iteritems())) | |
204 | ||
205 | ||
206 | def assign_duties(ast, duties=None): | |
207 | if duties is None: | |
208 | duties = {} | |
209 | ||
210 | if isinstance(ast, AcquireDutyEvent): | |
211 | duties.setdefault(ast.subject.object, set()).add(ast.object.object) | |
212 | ||
213 | if isinstance(ast, RelieveDutyEvent): | |
214 | duties.setdefault(ast.subject.object, set()) | |
215 | if ast.object.object not in duties[ast.subject.object]: | |
216 | print >>sys.stderr, '%r not in %r`s %r' % ( | |
217 | ast.object.object, ast.subject.object, duties[ast.subject.object] | |
218 | ) | |
219 | else: | |
220 | duties[ast.subject.object].remove(ast.object.object) | |
221 | ||
222 | attrs = {} | |
223 | for role, state in ast.iteritems(): | |
224 | if isinstance(state, State) and isinstance(state.object, Character): | |
225 | character = state.object | |
226 | if character not in duties: | |
227 | duties[character] = set() | |
228 | state = state.clone(duties=set(duties[character])) | |
229 | attrs[role] = state | |
230 | ||
231 | return ast.__class__(*[assign_duties(c, duties=duties) for c in ast], **attrs) | |
232 | ||
233 | ||
234 | def remove_duty_acquisition_events(ast): | |
235 | children = [] | |
236 | for child in ast: | |
237 | if not isinstance(child, (AcquireDutyEvent, RelieveDutyEvent)): | |
238 | children.append(remove_duty_acquisition_events(child)) | |
239 | ||
240 | return ast.__class__(*children, **dict(ast.iteritems())) | |
241 | ||
242 | ||
243 | def assign_referents(ast, context=None): | |
244 | if context is None: | |
245 | context = {'referent': None} | |
246 | ||
247 | # reset referent in each... eventually this will be paragraph | |
248 | #if isinstance(ast, Turn): | |
249 | # context['referent'] = None | |
250 | ||
251 | attrs = {} | |
252 | for role, state in ast.iteritems(): | |
253 | if isinstance(state, State): | |
254 | attrs[role] = state.clone(is_referent=(state.object == context['referent'])) | |
255 | if role == 'subject': | |
256 | context['referent'] = state.object | |
257 | else: | |
258 | attrs[role] = state | |
259 | ||
260 | return ast.__class__(*[assign_referents(c, context) for c in ast], **attrs) | |
261 | ||
262 | ||
263 | def resolve_setting_references(ast, setting=None): | |
264 | if isinstance(ast, Scene): | |
265 | setting = ast.setting | |
266 | ||
267 | attrs = dict((k, v) for (k, v) in ast.iteritems()) | |
268 | ||
269 | if isinstance(ast, PoseDescription): | |
270 | attrs['object'] = State(setting.nearby_scenery) | |
271 | ||
272 | return ast.__class__(*[resolve_setting_references(c, setting=setting) for c in ast], **attrs) | |
273 | ||
274 | ||
275 | def assign_first_occurrence(ast, occurred=None): | |
276 | """We use this to select between definite and indefinite article""" | |
277 | # TODO: mentioning in dialogue does not count as first occurrence | |
278 | if occurred is None: | |
279 | occurred = set() | |
280 | ||
281 | attrs = {} | |
282 | for role, state in ast.iteritems(): | |
283 | # first of these is to avoid counting objects in Scene, etc as occurrence | |
284 | if isinstance(ast, Event) and isinstance(state, State): | |
285 | if state.object not in occurred: | |
286 | state = state.clone(first_occurrence=True) | |
287 | occurred.add(state.object) | |
288 | attrs[role] = state | |
289 | ||
290 | return ast.__class__(*[assign_first_occurrence(c, occurred=occurred) for c in ast], **attrs) | |
291 | ||
292 | ||
293 | def describe_scene(ast): | |
294 | children = [c for c in ast] | |
295 | ||
296 | if isinstance(ast, Scene): | |
297 | children = [EventSequence( | |
298 | SettingDescription( | |
299 | subject=ast.setting | |
300 | ), | |
301 | NearbyDescription( | |
302 | subject=ast.setting, | |
303 | object=State( | |
304 | object=ast.setting.nearby_scenery, | |
305 | location=ast.setting | |
306 | ) | |
307 | ), | |
308 | GenericSettingDescription( | |
309 | subject=ast.setting, | |
310 | ), | |
311 | EventSequence(*children) | |
312 | )] | |
313 | ||
314 | return ast.__class__(*[describe_scene(c) for c in children], **dict(ast.iteritems())) | |
315 | ||
316 | ||
317 | def describe_characters(ast, described, newly_introduced): | |
318 | """Describes them as if we are meeting them for the first time. | |
319 | `described` is a set of characters who have already been described. | |
320 | ||
321 | `described` persists across several stories, but anyone we do | |
322 | describe here, we put in newly_introduced (it's essentially output | |
323 | only) so that we can tell not to e.g. remind the reader of their | |
324 | appearance overmuch. | |
325 | """ | |
326 | ||
327 | if isinstance(ast, Event): | |
328 | if isinstance(ast.subject, State) and isinstance(ast.subject.object, Character): | |
329 | if ast.subject.object not in described: | |
330 | if isinstance(ast.subject.object, MarySue) or random.chance(50): | |
331 | described.add(ast.subject.object) | |
332 | newly_introduced.add(ast.subject.object) | |
333 | return EventSequence( | |
334 | ast, | |
335 | CharacterDescription(subject=ast.subject), | |
336 | CharacterFeaturesDescription(subject=ast.subject) | |
337 | ) | |
338 | ||
339 | return ast.__class__(*[describe_characters(c, described, newly_introduced) for c in ast], **dict(ast.iteritems())) | |
340 | ||
341 | ||
342 | def remind_characters(ast, reminded): | |
343 | """Describes them assuming we have already been introduced to them, | |
344 | by subtly (hah) reminding us about what they look like.""" | |
345 | ||
346 | if isinstance(ast, Event): | |
347 | if isinstance(ast.subject, State) and isinstance(ast.subject.object, Character): | |
348 | if ast.subject.object not in reminded: | |
349 | if random.chance(20): | |
350 | reminded.add(ast.subject.object) | |
351 | return EventSequence( | |
352 | ast, | |
353 | CharacterReminder(subject=ast.subject), | |
354 | ) | |
355 | ||
356 | return ast.__class__(*[remind_characters(c, reminded) for c in ast], **dict(ast.iteritems())) | |
357 | ||
358 | ||
359 | def describe_costumes(ast, described=None): | |
360 | if described is None: | |
361 | described = set() | |
362 | ||
363 | if isinstance(ast, Scene): | |
364 | described = set() | |
365 | ||
366 | if isinstance(ast, Event): | |
367 | if isinstance(ast.subject, State) and isinstance(ast.subject.object, Character): | |
368 | if ast.subject.object not in described: | |
369 | if isinstance(ast.subject.object, MarySue) or random.chance(50): | |
370 | described.add(ast.subject.object) | |
371 | return EventSequence( | |
372 | ast, | |
373 | TorsoCostumeReminder(subject=ast.subject) if random.chance(10) else TorsoCostumeDescription(subject=ast.subject), | |
374 | FeetCostumeReminder(subject=ast.subject) if random.chance(10) else FeetCostumeDescription(subject=ast.subject) | |
375 | ) | |
376 | ||
377 | return ast.__class__(*[describe_costumes(c, described=described) for c in ast], **dict(ast.iteritems())) | |
378 | ||
379 | ||
380 | def collect_objects_from_states(ast, object_set): | |
381 | for child in ast: | |
382 | collect_objects_from_states(child, object_set) | |
383 | for key, value in ast.iteritems(): | |
384 | if isinstance(value, State): | |
385 | object_set.add(value.object) | |
386 | ||
387 | ||
388 | def add_crickets(ast): | |
389 | if isinstance(ast, LookAtEvent): | |
390 | return EventSequence(ast, CricketsEvent()) | |
391 | ||
392 | return ast.__class__(*[add_crickets(c) for c in ast], **dict(ast.iteritems())) | |
393 | ||
394 | ||
395 | def merge_adjacent_scenes(ast): | |
396 | children = [] | |
397 | for child in ast: | |
398 | if isinstance(child, Scene) and children and isinstance(children[-1], Scene) and child.setting == children[-1].setting: | |
399 | new_scene_contents = [gc for gc in children[-1]] + [gc for gc in child] | |
400 | # TODO: what if the Scenes differ in OTHER attributes? | |
401 | # for now there are none (assume characters are assigned later on) | |
402 | children[-1] = Scene(*new_scene_contents, **dict(children[-1].iteritems())) | |
403 | else: | |
404 | # note that this does not recurse | |
405 | children.append(child) | |
406 | ||
407 | return ast.__class__(*children, **dict(ast.iteritems())) | |
408 | ||
409 | ||
410 | def split_into_paragraphs(ast): | |
411 | if isinstance(ast, Scene): | |
412 | assert len([c for c in ast]) == 1 | |
413 | eseq = ast[0] | |
414 | assert isinstance(eseq, EventSequence) | |
415 | ||
416 | children = [] | |
417 | parachilds = [] | |
418 | subject = None | |
419 | for child in eseq: | |
420 | assert isinstance(child, Event), repr(child) | |
421 | if child.new_para or (isinstance(child.subject, State) and child.subject.object != subject): | |
422 | if parachilds: | |
423 | children.append(parachilds) | |
424 | parachilds = [] | |
425 | if child.subject: | |
426 | subject = child.subject.object | |
427 | else: | |
428 | subject = None | |
429 | elif not isinstance(child.subject, State): | |
430 | if parachilds: | |
431 | children.append(parachilds) | |
432 | parachilds = [] | |
433 | subject = None | |
434 | parachilds.append(child) | |
435 | if parachilds: | |
436 | children.append(parachilds) | |
437 | ||
438 | children = [EventSequence(*[Paragraph(*r) for r in children])] | |
439 | else: | |
440 | children = [split_into_paragraphs(c) for c in ast] | |
441 | ||
442 | return ast.__class__(*children, **dict(ast.iteritems())) | |
443 | ||
444 | ||
445 | def conjoin_sentences(ast): | |
446 | if isinstance(ast, Paragraph): | |
447 | children = [] | |
448 | for child in ast: | |
449 | if not children or \ | |
450 | not child.is_conjoinable or \ | |
451 | isinstance(children[-1], ConjoinedEvent) or \ | |
452 | random.chance(100): # DISABLED because for now it's kind of awful to read | |
453 | children.append(child) | |
454 | else: | |
455 | last = children[-1] | |
456 | compound = ConjoinedEvent(event1=last, event2=child) | |
457 | children[-1] = compound | |
458 | else: | |
459 | children = [conjoin_sentences(c) for c in ast] | |
460 | ||
461 | return ast.__class__(*children, **dict(ast.iteritems())) |
0 | import marysue.util as random | |
1 | from marysue.ast import AST | |
2 | ||
3 | ||
4 | # - - - - | |
5 | ||
6 | ||
7 | class Event(AST): | |
8 | exciting = False | |
9 | new_para = False | |
10 | slots = ( | |
11 | 'subject', 'object', | |
12 | 'object2', # rarely used | |
13 | ) | |
14 | ||
15 | @property | |
16 | def is_conjoinable(self): | |
17 | if not self.templates: | |
18 | return False | |
19 | if any([t[0] in ('"', "'") for t in self.templates]): | |
20 | return False | |
21 | return True | |
22 | ||
23 | ||
24 | # - - - - mood modifier events | |
25 | ||
26 | ||
27 | class MoodModifierEvent(Event): | |
28 | def __init__(self, subject, **kwargs): | |
29 | # this is just to debug where we might be constructing it with wrong args | |
30 | assert subject is not None | |
31 | super(MoodModifierEvent, self).__init__(subject=subject, **kwargs) | |
32 | ||
33 | def mood(self): | |
34 | raise NotImplementedError | |
35 | ||
36 | ||
37 | class BecomeHappyEvent(MoodModifierEvent): | |
38 | templates = ( | |
39 | '{subj.pronoun} became HAPPY', | |
40 | ) | |
41 | ||
42 | def mood(self): | |
43 | return 'happy' | |
44 | ||
45 | ||
46 | class BecomeSadEvent(MoodModifierEvent): | |
47 | templates = ( | |
48 | '{subj.pronoun} became SAD', | |
49 | ) | |
50 | ||
51 | def mood(self): | |
52 | return 'sad' | |
53 | ||
54 | ||
55 | class BecomeAngryEvent(MoodModifierEvent): | |
56 | templates = ( | |
57 | '{subj.pronoun} became ANGRY', | |
58 | ) | |
59 | ||
60 | def mood(self): | |
61 | return 'angry' | |
62 | ||
63 | ||
64 | class BecomeEmbarrassedEvent(MoodModifierEvent): | |
65 | templates = ( | |
66 | '{subj.pronoun} became EMBARRASSED', | |
67 | ) | |
68 | ||
69 | def mood(self): | |
70 | return 'embarrassed' | |
71 | ||
72 | ||
73 | # - - - - duty acquisition events. object is the duty acquired, subject is the character acquiring it | |
74 | ||
75 | ||
76 | class AcquireDutyEvent(Event): | |
77 | templates = ( | |
78 | '{subj.pronoun} acquired DUTY `{obj.name}`', | |
79 | ) | |
80 | ||
81 | ||
82 | class RelieveDutyEvent(Event): | |
83 | templates = ( | |
84 | '{subj.pronoun} was released from DUTY `{obj.name}`', | |
85 | ) | |
86 | ||
87 | ||
88 | # - - - - actions and affect | |
89 | ||
90 | ||
91 | class PickUpEvent(Event): | |
92 | templates = ( | |
93 | '{subj.motion}{subj.pronoun} picked up {obj.pronoun} that {obj.was} nearby', | |
94 | ) | |
95 | ||
96 | ||
97 | class HoldEvent(Event): | |
98 | templates = ( | |
99 | '{subj.pronoun} held {obj.pronoun} in {subj.possessive} hand', | |
100 | '{subj.motion}{subj.pronoun} lifted {obj.pronoun}< to eye level| into the light|>', | |
101 | ) | |
102 | ||
103 | ||
104 | class ContemplateEvent(Event): | |
105 | templates = ( | |
106 | '"{obj.proximal} represents how I feel inside," {subj.pronoun} {subj.said} {subj.adverb}', | |
107 | ) | |
108 | ||
109 | ||
110 | class ApproachEvent(Event): | |
111 | templates = ( | |
112 | '{subj.motion}{subj.pronoun} walked towards {obj.accusative}', | |
113 | '{subj.motion}{subj.pronoun} took two steps in {obj.possessive} direction, then stopped', | |
114 | ) | |
115 | ||
116 | ||
117 | class GestureAtEvent(Event): | |
118 | templates = ( | |
119 | '{subj.motion}{subj.pronoun} <pointed|gestured> towards {obj.accusative}', | |
120 | '{subj.pronoun} moved {subj.her} {subj.yet} <arm|hand> in the direction of {obj.accusative}', | |
121 | ) | |
122 | ||
123 | ||
124 | class LookAtEvent(Event): | |
125 | templates = ( | |
126 | '{subj.pronoun} looked at {obj.accusative}', | |
127 | '{subj.pronoun} glanced {subj.adverb} at {obj.accusative}', | |
128 | '{subj.pronoun} glared in the direction of {obj.accusative}', | |
129 | ) | |
130 | ||
131 | ||
132 | class LookAroundEvent(Event): | |
133 | templates = ( | |
134 | '{subj.pronoun} looked around', | |
135 | '{subj.pronoun} surveyed the area<| with {subj.her} eyes>', | |
136 | ) | |
137 | ||
138 | ||
139 | class PunchPalmWithFistEvent(Event): | |
140 | # NOTUSED | |
141 | templates = ( | |
142 | '{subj.pronoun} made a fist and punched {subj.his} other palm with {subj.his} fist', | |
143 | ) | |
144 | ||
145 | ||
146 | class RepeatForEmphasisEvent(Event): | |
147 | # NOTUSED | |
148 | templates = ( | |
149 | '{subj.pronoun} did this <again|a second time>, for emphasis', | |
150 | ) | |
151 | ||
152 | ||
153 | class EmoteEvent(Event): | |
154 | templates = ( | |
155 | '{subj.pronoun} {subj.emoted} {subj.adverb}', | |
156 | ) | |
157 | ||
158 | ||
159 | class CackleEvent(Event): | |
160 | templates = ( | |
161 | '{subj.pronoun} cackled <evilly|wildly|maniacally|despicably|wickedly|hatefully|disdainfully>', | |
162 | ) | |
163 | ||
164 | ||
165 | class StateDutyEvent(Event): | |
166 | templates = ( | |
167 | '"We have a duty to {subj.pick_duty.name}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
168 | ) | |
169 | ||
170 | ||
171 | class GreetEvent(Event): | |
172 | templates = ( | |
173 | '"Hello, {obj.def}," {subj.said} {subj.pronoun} {subj.adverb}', | |
174 | '"Hello, {obj.def}," {subj.said} {subj.pronoun} with an enigmatic twitch of {sub.his} <nose|mouth|ears>', | |
175 | ) | |
176 | ||
177 | ||
178 | class CharacterStaysHappyEvent(Event): | |
179 | templates = ( | |
180 | 'this was bad, but {subj.pronoun} didn`t let it <get {subj.accusative_pronoun} down|affect {subj.his} mood|cramp {subj.his} style|dampen {subj.his} cheer>', | |
181 | '{subj.pronoun} wasn`t going to let a little thing like this <get {subj.accusative_pronoun} down|affect {subj.his} mood|cramp {subj.his} style|dampen {subj.his} cheer>, though', | |
182 | '{subj.pronoun} gritted {subj.his} teeth and determined to stay <cheerful|upbeat|chipper|positive|optimistic>', | |
183 | ) | |
184 | ||
185 | ||
186 | # - - - - events for chekov's gun - - - - | |
187 | ||
188 | ||
189 | class IntroduceItemEvent(Event): | |
190 | templates = ( | |
191 | '{subj.pronoun} was cracking open <walnuts|pecans> with {obj.pronoun}', | |
192 | '{subj.pronoun} was <studiously|meticulously|intently> <polishing|cleaning|buffing> {obj.pronoun}', | |
193 | ) | |
194 | ||
195 | ||
196 | class AskAboutItemEvent(Event): | |
197 | templates = ( | |
198 | '"{obj.distal} means a lot to you, doesn`t it?" asked {subj.pronoun}', | |
199 | '"{obj.distal}, it`s <kind of|pretty> special, isn`t it?" asked {subj.pronoun}', | |
200 | ) | |
201 | ||
202 | ||
203 | class ReplyAboutItemEvent(Event): | |
204 | templates = ( | |
205 | '"It means a lot to all of us, {obj.def}," {subj.pronoun} {subj.said} {subj.adverb}', | |
206 | '"Space Fighters Command doesn`t entrust us with just any old thing, {obj.def}," {subj.pronoun} {subj.said} {subj.adverb}', | |
207 | '"It`s not the <sort|kind|type> of thing you can just order <on|off|from> Omnizon, {obj.def}," {subj.pronoun} {subj.said} {subj.adverb}', | |
208 | ) | |
209 | ||
210 | ||
211 | # - - - - events for cave-in plot - - - - | |
212 | ||
213 | ||
214 | class RumblingSoundEvent(Event): | |
215 | templates = ( | |
216 | 'there was a rumbling sound', | |
217 | ) | |
218 | ||
219 | ||
220 | class WhatWasThatNoiseEvent(Event): | |
221 | templates = ( | |
222 | '"What was that noise?" {subj.said} {subj.pronoun}', | |
223 | '"Did you hear something?" {subj.pronoun} {subj.said}', | |
224 | ) | |
225 | ||
226 | ||
227 | class CaveInEvent(Event): | |
228 | # FIXME subj.pronoun works badly here :/ | |
229 | exciting = True | |
230 | templates = ( | |
231 | 'suddenly, {subj.def} caved in', | |
232 | 'without warning, with a <tremendous|stupendous|overwhelming> <crash|boom|bang>, {subj.def} caved in', | |
233 | 'too quickly for anyone to react, {subj.def} caved in', | |
234 | ) | |
235 | ||
236 | ||
237 | class StunnedEvent(Event): | |
238 | templates = ( | |
239 | '{subj.pronoun} just stared, seemingly paralyzed', | |
240 | '{subj.pronoun} stood frozen like a deer in headlights', | |
241 | ) | |
242 | ||
243 | ||
244 | class BuriedUnderRubbleEvent(Event): | |
245 | templates = ( | |
246 | '{subj.pronoun} was buried under rubble', | |
247 | ) | |
248 | ||
249 | ||
250 | class DigOutEvent(Event): | |
251 | templates = ( | |
252 | '{subj.pronoun} helped dig {obj.pronoun} out from the rubble', | |
253 | ) | |
254 | ||
255 | ||
256 | # - - - - events for kidnapping plot - - - - | |
257 | ||
258 | ||
259 | class AppearEvent(Event): | |
260 | exciting = True | |
261 | templates = ( | |
262 | '<then|suddenly|all of a sudden>, out of <nowhere|thin air>, {subj.pronoun} appeared', | |
263 | ) | |
264 | ||
265 | ||
266 | class DisappearEvent(Event): | |
267 | templates = ( | |
268 | 'in a flash, {subj.pronoun} disappeared into thin air, taking {obj.pronoun} with {subj.accusative}', | |
269 | ) | |
270 | ||
271 | ||
272 | class NoticeAntagonistEvent(Event): | |
273 | templates = ( | |
274 | '"<It`s |>YOU!" {subj.shouted} {subj.pronoun}', | |
275 | '"<It`s |>{obj.def}!" {subj.pronoun} {subj.shouted}', | |
276 | ) | |
277 | ||
278 | ||
279 | class AntagonistBanterEvent(Event): | |
280 | templates = ( | |
281 | '"I have you now!" {subj.said} {subj.pronoun} {subj.withvoice}', | |
282 | '"What have we here!" {subj.said} {subj.pronoun} {subj.withvoice}', | |
283 | '"We meet again, <kiddies|chums|do-gooders>!" {subj.said} {subj.pronoun} {subj.withvoice}', | |
284 | '"So sorry to spoil your <little|> party, <kiddies|chums|do-gooders>!" {subj.said} {subj.pronoun} {subj.withvoice}', | |
285 | ) | |
286 | ||
287 | ||
288 | class AbductEvent(Event): | |
289 | templates = ( | |
290 | '{subj.pronoun} <grabbed|snatched> {obj.pronoun} from behind', | |
291 | ) | |
292 | ||
293 | ||
294 | class WeMustFindThemEvent(Event): | |
295 | templates = ( | |
296 | '"We must find out where {obj.pronoun} is being held!" {subj.said} {subj.pronoun} {subj.adverb}', | |
297 | '"We must find out where that villian has taken {obj.pronoun}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
298 | ) | |
299 | ||
300 | ||
301 | class LocationHunchEvent(Event): | |
302 | templates = ( | |
303 | '"I have a strong feeling {obj.pronoun} is in {object2.pronoun}," {subj.said} {subj.pronoun} {subj.adverb}', | |
304 | ) | |
305 | ||
306 | ||
307 | class SetCourseEvent(Event): | |
308 | templates = ( | |
309 | '"Set course for {object.pronoun}!," {subj.shouted} {subj.pronoun} {subj.adverb}', | |
310 | ) | |
311 | ||
312 | ||
313 | class WeMustGoToEvent(Event): | |
314 | templates = ( | |
315 | '"Quickly! We must make our way to {object.pronoun}!," {subj.shouted} {subj.pronoun} {subj.adverb}', | |
316 | ) | |
317 | ||
318 | ||
319 | class RescueEvent(Event): | |
320 | templates = ( | |
321 | '{subj.pronoun} together broke the cage and pulled out {obj.def}', | |
322 | ) | |
323 | ||
324 | ||
325 | class BumpIntoForceFieldEvent(Event): | |
326 | templates = ( | |
327 | 'walking along, {subj.pronoun} <suddenly|unexpectedly|surprisedly> <smacked|whacked|bonked> {subj.his} head against an invisible force field', | |
328 | ) | |
329 | ||
330 | ||
331 | class MustRetraceStepsEvent(Event): | |
332 | templates = ( | |
333 | '"There`s no way we can get through this we`ll have to find another way in!" {subj.said} {subj.pronoun} {subj.adverb}', | |
334 | ) | |
335 | ||
336 | ||
337 | # - - - - events for lost item plot - - - - | |
338 | ||
339 | ||
340 | class CommentOnItemEvent(Event): | |
341 | # NOTUSED | |
342 | templates = ( | |
343 | '"<This is|What> a <great|superb> {obj.name} this is, <isn`t it|don`t you think|don`t you agree>?" {subj.pronoun} {subj.said} {subj.adverb}', | |
344 | ) | |
345 | ||
346 | ||
347 | class TripEvent(Event): | |
348 | templates = ( | |
349 | "<Just then|Suddenly|All of a sudden|Without warning>, {subj.pronoun} <tripped|stumbled|lost {subj.his} balance|stubbed {subj.his} toe|was distracted>", | |
350 | ) | |
351 | ||
352 | ||
353 | class DropEvent(Event): | |
354 | templates = ( | |
355 | "{subj.pronoun} lost {subj.his} grip on {obj.pronoun}", | |
356 | "{obj.pronoun} slipped out of {subj.possessive} <grasp|hand|grip>", | |
357 | ) | |
358 | ||
359 | ||
360 | class LoseEvent(Event): | |
361 | templates = ( | |
362 | "{subj.pronoun} tumbled and rolled away out of sight", | |
363 | "a <drone|Space Magpie> flew by and made off with {subj.pronoun}", | |
364 | ) | |
365 | ||
366 | ||
367 | class OopsEvent(Event): | |
368 | templates = ( | |
369 | '"<Whoops|Oops|Oopsie-daisy|Whoopsy-daisy|Uh-oh|Drat>," {subj.said} {subj.pronoun} quietly', | |
370 | 'a <sheepish|embarrassed|crestfallen|sour> look crept across {subj.possessive} face', | |
371 | ) | |
372 | ||
373 | ||
374 | class HunchEvent(Event): | |
375 | templates = ( | |
376 | '{subj.pronoun} <suddenly|all of a sudden> had a <funny|strange|unusual|odd|> <hunch|inkling|intuition>', | |
377 | ) | |
378 | ||
379 | ||
380 | class LookBehindEvent(Event): | |
381 | templates = ( | |
382 | '{subj.pronoun} looked behind the nearby {obj.name}', | |
383 | ) | |
384 | ||
385 | ||
386 | class FindEvent(Event): | |
387 | templates = ( | |
388 | '{subj.pronoun} found {obj.pronoun}', | |
389 | ) | |
390 | ||
391 | ||
392 | # - - - - events for fight plot | |
393 | ||
394 | ||
395 | class AttackEvent(Event): | |
396 | templates = ( | |
397 | 'suddenly, {subj.indef} attacked {obj.pronoun}', | |
398 | 'suddenly, {obj.pronoun} {obj.was} attacked by {subj.indef}', | |
399 | ) | |
400 | ||
401 | ||
402 | class EncounterEvent(Event): | |
403 | templates = ( | |
404 | 'suddenly, {obj.pronoun} spotted {subj.indef} in the distance', | |
405 | 'suddenly, {subj.indef} came around the corner', | |
406 | ) | |
407 | ||
408 | ||
409 | class GoonBanterEvent(Event): | |
410 | templates = ( | |
411 | '"{obj.name}!" <hissed|mouthed|squeaked> {subj.def}', | |
412 | '"<Uh oh, |>looks like <we`ve got company|they`ve spotted us|they`ve found us>," {subj.said} {subj.def} {subj.adverb}', | |
413 | ) | |
414 | ||
415 | ||
416 | class GoonParlayEvent(Event): | |
417 | templates = ( | |
418 | '"{subj.gibberish}!!!" shouted {subj.def} in {subj.his} <weird|strange> {subj.singular} language', | |
419 | ) | |
420 | ||
421 | ||
422 | class AfterBattleEvent(Event): | |
423 | templates = ( | |
424 | 'when the dust had <cleared|settled>, <stunned|dazed> {subj.name} <littered|were strewn across> the <battlefield|area>', | |
425 | ) | |
426 | ||
427 | ||
428 | class AfterBattleBanterEvent(Event): | |
429 | templates = ( | |
430 | '"That was a <close one|close call>!" {subj.said} {subj.pronoun} {subj.adverb}', | |
431 | ) | |
432 | ||
433 | ||
434 | class ObserveAfterBattleEvent(Event): | |
435 | templates = ( | |
436 | '"It looked like {obj.distal} had {object2.possessive} insignia on their <uniforms|jerseys|clothes|armor>," {subj.said} {subj.pronoun} {subj.adverb}', | |
437 | ) | |
438 | ||
439 | ||
440 | class RemindReportEvent(Event): | |
441 | templates = ( | |
442 | '"Remember to include that in the report when we get home."', | |
443 | ) | |
444 | ||
445 | ||
446 | class IfWeGetHomeEvent(Event): | |
447 | templates = ( | |
448 | '"IF we get home," {subj.said} {subj.pronoun} {subj.adverb}', | |
449 | ) | |
450 | ||
451 | ||
452 | class WarCryEvent(Event): | |
453 | templates = ( | |
454 | '"{subj.war_cry}!!!" {subj.shouted} {subj.pronoun}', | |
455 | ) | |
456 | ||
457 | ||
458 | class UnsheathEvent(Event): | |
459 | templates = ( | |
460 | '{subj.pronoun} drew {obj.pronoun} out of {subj.his} <belt|equipment bag|backpack>', | |
461 | ) | |
462 | ||
463 | ||
464 | class LiftWeaponEvent(Event): | |
465 | templates = ( | |
466 | '{subj.pronoun} <lifted|heaved|raised> {obj.pronoun} <above|over> {subj.her} head', | |
467 | ) | |
468 | ||
469 | ||
470 | class BringDownWeaponEvent(Event): | |
471 | templates = ( | |
472 | '{subj.pronoun} brought down {obj.pronoun} with a <mighty|tremendous|awesome> <force|movement|power>', | |
473 | ) | |
474 | ||
475 | ||
476 | class WeaponContactEvent(Event): | |
477 | templates = ( | |
478 | '{subj.pronoun} made contact with {obj.pronoun} with a <mighty|tremendous|awesome> <thud|whack|impact>', | |
479 | ) | |
480 | ||
481 | ||
482 | class FlyAcrossEvent(Event): | |
483 | templates = ( | |
484 | '{subj.pronoun} went flying across the room', | |
485 | ) | |
486 | ||
487 | ||
488 | class TryToGetBehindEvent(Event): | |
489 | templates = ( | |
490 | '{subj.pronoun} <ran|sprinted> to the side, <looking to|to try to|trying to> attack {obj.pronoun} from <behind|the rear>', | |
491 | ) | |
492 | ||
493 | ||
494 | class RushIntoFrayEvent(Event): | |
495 | templates = ( | |
496 | 'unperturbed, {subj.pronoun} rushed back into the <fray|melee|fight>', | |
497 | ) | |
498 | ||
499 | ||
500 | # - - - - romantic tension events - - - - | |
501 | ||
502 | ||
503 | class PullAsideEvent(Event): | |
504 | templates = ( | |
505 | '{subj.motion}{subj.pronoun} <motioned|gestured to> {obj.accusative} to step <away|aside|back> from the others for a moment', | |
506 | ) | |
507 | ||
508 | ||
509 | class WantToTalkToYouEvent(Event): | |
510 | templates = ( | |
511 | '"There`s <something|a thing> I <wanted|need> to <talk to you about|talk about|say to you>", {subj.pronoun} <began|{subj.said}> {subj.adverb}', | |
512 | ) | |
513 | ||
514 | ||
515 | class WhatIsItEvent(Event): | |
516 | templates = ( | |
517 | '"<Yes|What is it>, {obj.def}?" asked {subj.pronoun}', | |
518 | ) | |
519 | ||
520 | ||
521 | class RecallPastEventEvent(Event): | |
522 | templates = ( | |
523 | '"<Uh|Um|Well|You know>, about <that time|last night|the other day> in the <mess hall|engine room|loading bay>..." {subj.said} {subj.pronoun} {subj.adverb}', | |
524 | ) | |
525 | ||
526 | ||
527 | class SayNoMoreEvent(Event): | |
528 | templates = ( | |
529 | '"Hush, {obj.def}, there`s no need, you know that," {subj.said} {subj.pronoun} {subj.adverb}', | |
530 | '"It`s all right, {obj.def}, you don`t need to say anything," {subj.said} {subj.pronoun} {subj.adverb}', | |
531 | '"Speak not of it, {obj.def}" {subj.said} {subj.pronoun} {subj.adverb}', | |
532 | '{subj.pronoun} held {subj.her} finger up to {obj.possessive} lips', | |
533 | '"Words are not necessary, {obj.def}" {subj.said} {subj.pronoun} {subj.adverb}', | |
534 | ) | |
535 | ||
536 | ||
537 | class BumpIntoAwkwardlyEvent(Event): | |
538 | templates = ( | |
539 | '{subj.pronoun} wasn`t <looking|watching> where {sub.pronoun} was going and bumped into {obj.pronoun} in a most embarrassing fashion', | |
540 | ) | |
541 | ||
542 | ||
543 | class BlushEvent(Event): | |
544 | templates = ( | |
545 | '{subj.pronoun} blushed a deep red', | |
546 | '{subj.pronoun} went beet red with blushing', | |
547 | ) | |
548 | ||
549 | ||
550 | class PreludeToKissEvent(Event): | |
551 | templates = ( | |
552 | '"<Uh|Um|Well|You know>, ... there comes a time when..." {subj.said} {subj.pronoun} {subj.adverb}', | |
553 | ) | |
554 | ||
555 | ||
556 | class FidgetEvent(Event): | |
557 | templates = ( | |
558 | '"<Uh|Um|Well|You know>, ..." {subj.said} {subj.pronoun} {subj.adverb}', | |
559 | '{subj.pronoun} put {subj.his} hand behind {subj.his} head', | |
560 | '{subj.pronoun} scratched {subj.his} <head|knee|neck>', | |
561 | '{subj.pronoun} looked away', | |
562 | ) | |
563 | ||
564 | class FacesCloseTogetherEvent(Event): | |
565 | new_para = True | |
566 | templates = ( | |
567 | 'The faces of {subj.definite} and {obj.definite} moved centimeters closer', | |
568 | 'The faces of {obj.definite} and {subj.definite} moved centimeters closer', | |
569 | 'Their faces inched closer and closer', | |
570 | ) | |
571 | ||
572 | ||
573 | class MushyStuffEvent(Event): | |
574 | templates = ( | |
575 | '"{obj.def}, you are so exquisitely beautiful, like a jewel," {sub.said} {subj.pronoun}. "I must kiss you!"', | |
576 | ) | |
577 | ||
578 | ||
579 | class KissEvent(Event): | |
580 | exciting = True | |
581 | new_para = True | |
582 | templates = ( | |
583 | '{subj.def} and {obj.def} kissed', | |
584 | ) | |
585 | ||
586 | ||
587 | class AndTheyKissedEvent(Event): | |
588 | exciting = True | |
589 | templates = ( | |
590 | ('And they kissed' + (' and they kissed' * 20)), | |
591 | ) | |
592 | ||
593 | ||
594 | # - - - - events for drone "plot" - - - - | |
595 | ||
596 | ||
597 | class DroneEvent(Event): | |
598 | templates = ( | |
599 | '<Suddenly|From out of nowhere|Out of the blue>, a <courier|messenger|delivery|security|surveillance|cooking|cleaning> drone <whizzed past|buzzed by|flew close by> {subj.possessive} head', | |
600 | ) | |
601 | ||
602 | ||
603 | class WhatWasThatEvent(Event): | |
604 | templates = ( | |
605 | '"What <in blazes|in Sam Hill|in the world|the devil|> was that?" {subj.said} {subj.pronoun} {subj.adverb}', | |
606 | ) | |
607 | ||
608 | ||
609 | # - - - - generic plot-related events - - - - | |
610 | ||
611 | ||
612 | class KeepMovingEvent(Event): | |
613 | templates = ( | |
614 | '"Let`s keep moving," {subj.pronoun} {subj.said} {subj.adverb}', | |
615 | ) | |
616 | ||
617 | ||
618 | class TurnCornerEvent(Event): | |
619 | templates = ( | |
620 | '{subj.pronoun} <went around|turned> a corner', | |
621 | ) | |
622 | ||
623 | ||
624 | class PanicEvent(Event): | |
625 | templates = ( | |
626 | '{subj.pronoun} {subj.shouted} "<OH NO|NOOO|Nooooooooooooo>!"', | |
627 | '"<OH NO|NOOO|Nooooooooooooo>!" {subj.shouted} {subj.pronoun} <{subj.adverb}|{subj.withvoice}>', | |
628 | '{subj.pronoun} <put|clapped> {subj.her} hands ' | |
629 | '<on|to> <the sides of {subj.her} face|{subj.her} cheeks> in <disbelief|surprise|shock>', | |
630 | '{subj.possessive} mouth <opened|gaped|gawped> wide in <disbelief|surprise|shock>', | |
631 | '{subj.pronoun} made <motions|gestures> of <disbelief|surprise|shock> with {subj.her} <arms|hands>', | |
632 | ) | |
633 | ||
634 | ||
635 | class WasteNoTimeEvent(Event): | |
636 | templates = ( | |
637 | '{subj.pronoun} wasted no time', | |
638 | '"There`s no time to lose!" {subj.pronoun} {subj.shouted}', | |
639 | '"We must <act quickly|hurry>!" {subj.pronoun} {subj.said}', | |
640 | '"Hurry!" {subj.pronoun} {subj.said}', | |
641 | ) | |
642 | ||
643 | ||
644 | class ThankEvent(Event): | |
645 | templates = ( | |
646 | '"Thank you, {obj.def}," {subj.said} {subj.pronoun} {subj.adverb}', | |
647 | '"I would have been <lost|a goner|toast> without you, {obj.def}," {subj.said} {subj.pronoun} {subj.adverb}', | |
648 | '"I am <indebted|in debt> to you, {obj.def}," {subj.said} {subj.pronoun} {subj.adverb}', | |
649 | ) | |
650 | ||
651 | ||
652 | class ReliefEvent(Event): | |
653 | templates = ( | |
654 | '"<What|That`s> a relief!" {subj.shouted} {subj.pronoun}', | |
655 | ) | |
656 | ||
657 | ||
658 | # - - - - end-of-story events | |
659 | ||
660 | ||
661 | class ComplimentActionEvent(Event): | |
662 | templates = ( | |
663 | '"<That|there> was some <quick|top notch|ace> <thinking|action> <I saw|> from you today, {obj.def}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
664 | '"<You displayed|I saw you display> some <quick|top notch|ace> <thinking|action> today, {obj.def}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
665 | ) | |
666 | ||
667 | ||
668 | class OfferPromotionEvent(Event): | |
669 | templates = ( | |
670 | '"I think I should promote you to {obj.next_rank}, {obj.def}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
671 | '"How would you like a promotion to the rank of {obj.next_rank}, {obj.def}!" {subj.said} {subj.pronoun} {subj.adverb}', | |
672 | ) | |
673 | ||
674 | ||
675 | class OhYouEvent(Event): | |
676 | templates = ( | |
677 | '"Oh, {obj.def}!" <{subj.said}|blushed> {subj.pronoun} {subj.adverb}', | |
678 | '"Oh, {obj.def}!" {subj.said} {subj.pronoun}, blushing fiercely like <a tiger|a bonfire>', | |
679 | ) | |
680 | ||
681 | ||
682 | class RefusePromotionEvent(Event): | |
683 | templates = ( | |
684 | '"I`m too modest, I like being a {subj.rank} too!"', | |
685 | '"I`m not in this for rank, I do it out of love!"', | |
686 | '"it`s enough to know I`m fighting on the side of good!"', | |
687 | ) | |
688 | ||
689 | ||
690 | class ConvalesceEvent(Event): | |
691 | templates = ( | |
692 | '"What an adventure that was!" exclaimed {subj.pronoun}', | |
693 | '"I could really go for a <curry|pizza|hamburger|hot dog|soda|parmo> <after that|right now>," {subj.said} {subj.pronoun} {subj.adverb}', | |
694 | '"I think we all learned a valuable lesson today," {subj.said} {subj.pronoun} {subj.adverb}', | |
695 | '"All in a day`s work for the Space Fighters!" {subj.said} {subj.pronoun} {subj.adverb}', | |
696 | '{subj.motion}{subj.pronoun} <slumped|flumped> down on the <couch|sofa|bean bag chair>', | |
697 | ) | |
698 | ||
699 | ||
700 | class RememberMacGuffinEvent(Event): | |
701 | templates = ( | |
702 | '"Maybe next time you`ll be more careful with {object2.distal}, {obj.def}," {subj.said} {subj.pronoun} {subj.adverb}', | |
703 | ) | |
704 | ||
705 | ||
706 | class AllLaughEvent(Event): | |
707 | templates = ( | |
708 | "they all laughed", | |
709 | "everyone laughed", | |
710 | ) | |
711 | ||
712 | ||
713 | # - - - - travel events | |
714 | ||
715 | ||
716 | class TravelToEvent(Event): | |
717 | templates = ( | |
718 | '{subj.pronoun} travelled to {obj.pronoun}', | |
719 | ) | |
720 | ||
721 | ||
722 | class CommentOnPlaceEvent(Event): | |
723 | templates = ( | |
724 | '"I don`t like the look of this place," {subj.said} {subj.pronoun} {subj.adverb}', | |
725 | '"This place gives me the <creeps|chills|willies>," {subj.said} {subj.pronoun} {subj.adverb}', | |
726 | ) | |
727 | ||
728 | ||
729 | class TakeReadingsEvent(Event): | |
730 | templates = ( | |
731 | '"Enviro <readings|measurements|metrics> look normal," {subj.said} {subj.pronoun}, <looking|peering|gazing> down at <the enviro probe app on {subj.his} hyper crystal|{subj.his} hyper crystal`s enviro probe app>', | |
732 | '{subj.motion}{subj.pronoun} <dialed up|opened up|started|launched> the enviro probe app on {subj.his} hyper crystal. "Enviro <readings|measurements|metrics> look normal," {subj.said} {subj.pronoun} {subj.adverb}', | |
733 | ) | |
734 | ||
735 | ||
736 | class ReadingsBanterEvent(Event): | |
737 | templates = ( | |
738 | '"<Yeah, normal|Sure, normal|Normal, yeah|Normal, sure|>... for a <PIT|DUMP|DISASTER AREA|TOXIC WASTE SITE>!!" {subj.said} {subj.pronoun} {subj.adverb}', | |
739 | ) | |
740 | ||
741 | ||
742 | class BlastOfHotAirEvent(Event): | |
743 | templates = ( | |
744 | '{subj.pronoun} close {subj.his} eyes as a <blast|gust> of warm fetid air rushed through the passage', | |
745 | ) | |
746 | ||
747 | ||
748 | # - - - - misc | |
749 | ||
750 | ||
751 | class OutOfEggsEvent(Event): | |
752 | templates = ( | |
753 | '"I have just come back from the <kitchen|pantry> and it looks like we are <fresh|plum> out of <eggs|Space Sugar|nutri worms>," {subj.said} {subj.pronoun} {subj.adverb}', | |
754 | ) | |
755 | ||
756 | ||
757 | class GenericHyperCrystalUsageEvent(Event): | |
758 | templates = ( | |
759 | '{subj.pronoun} was playing that new match three game on {subj.her} hyper crystal', | |
760 | ) | |
761 | ||
762 | ||
763 | # - - - - descriptions, not events - - - - | |
764 | ||
765 | ||
766 | class PoseDescription(Event): | |
767 | templates = ( | |
768 | '{subj.pronoun} {subj.was} leaning on {obj.pronoun}', | |
769 | '{subj.pronoun} {subj.was} standing near {obj.pronoun}', | |
770 | '{subj.pronoun} {subj.was} standing near {obj.pronoun}', | |
771 | ) | |
772 | ||
773 | ||
774 | class SettingDescription(Event): | |
775 | templates = ( | |
776 | 'all was quiet {subj.preposition} {subj.def}', | |
777 | '{subj.def} was a sight to behold', | |
778 | "{subj.def} was pretty much what you'd expect", | |
779 | ) | |
780 | ||
781 | ||
782 | class NearbyDescription(Event): | |
783 | # subject is the setting, so it joins up with SettingDescription | |
784 | templates = ( | |
785 | '<Nearby|Not too far away|In the centre of the area> {obj.was} {obj.indef}', | |
786 | 'there {obj.was} {obj.indef} off in the <corner|distance>', | |
787 | '{obj.indef} seemed to dominate the environment', | |
788 | '{obj.indef} gleamed in the {obj.location.light}', | |
789 | ) | |
790 | ||
791 | ||
792 | class GenericSettingDescription(Event): | |
793 | # subject is the setting, so it joins up with SettingDescription | |
794 | templates = ( | |
795 | 'a Space Mouse scurried by', | |
796 | 'a few Space <Bees|Gnats|Mosquitoes> <flew|buzzed> around', | |
797 | 'there seemed to be electricity in the air', | |
798 | 'it was truly an unusual sight', | |
799 | ) | |
800 | ||
801 | ||
802 | class CharacterDescription(Event): | |
803 | is_conjoinable = False | |
804 | templates = ( | |
805 | '{subj.pronoun} was {subj.stature}, with {subj.hair_length} {subj.hair_colour} hair and {subj.eye_colour} eyes', | |
806 | ) | |
807 | ||
808 | ||
809 | class CharacterFeaturesDescription(Event): | |
810 | templates = ( | |
811 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature}, the <sort|kind|type> you only see in old movies', | |
812 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature}, the <sort|kind|type> you only see in Westerns', | |
813 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature}, the <sort|kind|type> you only see in comic books', | |
814 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature}, the <sort|kind|type> you only see in the Outer Colonies', | |
815 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature} that {subj.she} <inherited|inherited from|got> from {subj.her} <mother|father><`s side|`s side of the family|>', | |
816 | '{subj.pronoun} had a {subj.feature_adj} {subj.feature} that the other kids <teased|made fun of> back when {subj.he} <was in school|was a kid>', | |
817 | '{subj.her} {subj.feature_adj} {subj.feature} complemented {subj.her} other features <well|strongly|nicely|perfectly|highly>', | |
818 | ) | |
819 | ||
820 | ||
821 | class CharacterReminder(Event): | |
822 | templates = ( | |
823 | '{subj.pronoun} put {subj.his} hand up to {subj.his} {subj.feature_adj} {subj.feature}', | |
824 | '{subj.his} {subj.feature_adj} {subj.feature} seemed to <gleam|glow|shine> in the {subj.location.light}', | |
825 | '{subj.his} {subj.hair_length} {subj.hair_colour} hair seemed to <gleam|glow|shine> in the {subj.location.light}', | |
826 | 'the {subj.location.light} of {subj.location.definite} cast highlights on {subj.his} {subj.hair_length} {subj.hair_colour} <hair|locks>', | |
827 | '{subj.his} {subj.feature_adj} {subj.feature} in profile cast a shadow against {subj.location.nearby_scenery.definite}', | |
828 | '{subj.his} {subj.feature_adj} {subj.feature} in profile threw a shadow on {subj.location.nearby_scenery.definite}', | |
829 | ) | |
830 | ||
831 | ||
832 | class TorsoCostumeDescription(Event): | |
833 | two_piece_templates = ( | |
834 | '{subj.pronoun} {subj.was} {subj.wearing} {subj.torso_costume.indef} and {subj.legs_costume.indef}', | |
835 | ) | |
836 | ||
837 | one_piece_templates = ( | |
838 | '{subj.pronoun} {subj.was} {subj.wearing} {subj.torso_costume.indef}', | |
839 | ) | |
840 | ||
841 | def render(self): | |
842 | if self.subject.legs_costume: | |
843 | template = random.choice(self.two_piece_templates) | |
844 | else: | |
845 | template = random.choice(self.one_piece_templates) | |
846 | return self.render_t(template) | |
847 | ||
848 | ||
849 | class FeetCostumeDescription(Event): | |
850 | templates = ( | |
851 | 'on {subj.her} feet {subj.pronoun} wore {subj.feet_costume.indef}', | |
852 | '{subj.feet_costume.indef} <graced|were on> {subj.her} feet', | |
853 | '{subj.his} feet were shod <with|in> {subj.feet_costume.indef}', | |
854 | '{subj.feet_costume.def} that <were on {subj.his} feet|{subj.pronoun} had on> looked almost new', | |
855 | ) | |
856 | ||
857 | ||
858 | class TorsoCostumeReminder(Event): | |
859 | two_piece_templates = ( | |
860 | '{subj.torso_costume.definite} {subj.pronoun} {subj.was} {subj.wearing} seemed to <gleam|glow|shine> in the {subj.location.light}', | |
861 | ) | |
862 | ||
863 | one_piece_templates = ( | |
864 | '{subj.torso_costume.definite} {subj.pronoun} {subj.was} {subj.wearing} seemed to <gleam|glow|shine> in the {subj.location.light}', | |
865 | ) | |
866 | ||
867 | def render(self): | |
868 | if self.subject.legs_costume: | |
869 | template = random.choice(self.two_piece_templates) | |
870 | else: | |
871 | template = random.choice(self.one_piece_templates) | |
872 | return self.render_t(template) | |
873 | ||
874 | ||
875 | class FeetCostumeReminder(Event): | |
876 | templates = ( | |
877 | '{subj.feet_costume.definite} {subj.pronoun} {subj.was} {subj.wearing} seemed to <gleam|glow|shine> in the {subj.location.light}', | |
878 | '{subj.feet_costume.definite} on {subj.her} feet seemed to <gleam|glow|shine> in the {subj.location.light}', | |
879 | ) | |
880 | ||
881 | ||
882 | class BehindBarsDescription(Event): | |
883 | exciting = True | |
884 | new_para = True | |
885 | templates = ( | |
886 | '{subj.pronoun} was there, locked in a cage', | |
887 | ) | |
888 | ||
889 | ||
890 | class GenericBattleDescription(Event): | |
891 | exciting = True | |
892 | new_para = True | |
893 | templates = ( | |
894 | 'a <fierce|intense|harrowing|epic> <battle|skirmish> <ensued|took place|began|had begun|commenced|started>', | |
895 | ) | |
896 | ||
897 | ||
898 | # - - - - | |
899 | ||
900 | ||
901 | class ConjoinedEvent(Event): | |
902 | slots = ( | |
903 | 'subject', 'object', 'object2', # MEH | |
904 | 'event1', 'event2', | |
905 | ) | |
906 | ||
907 | def render(self): | |
908 | return self.event1.render() + ", and " + self.event2.render() | |
909 | ||
910 | ||
911 | # - - - - | |
912 | ||
913 | ||
914 | def get_event_class(name): | |
915 | cls = globals().get(name) | |
916 | return cls | |
917 | ||
918 | ||
919 | def get_all_event_classes(): | |
920 | return [c for c in globals().values() if c.__class__ == type and issubclass(c, Event)] |
0 | """Objects in the story. | |
1 | ||
2 | This includes characters, which are merely animate objects. And settings. | |
3 | ||
4 | The root object of all objects is Object. | |
5 | ||
6 | Objects here contain only the properties of the object which do not change during the | |
7 | course of the story. See marysue.state.State for states of objects over time. | |
8 | ||
9 | """ | |
10 | ||
11 | import string | |
12 | ||
13 | import marysue.util as random | |
14 | ||
15 | ||
16 | class Object(object): | |
17 | """Objects are immutable.""" | |
18 | ||
19 | def __init__(self, names, takeable=False, home=None, rank=None, weapon=None): | |
20 | self.names = tuple(names) | |
21 | self.takeable = takeable | |
22 | self.home = home | |
23 | self.rank = rank | |
24 | self.weapon = weapon | |
25 | ||
26 | def __hash__(self): | |
27 | return hash(self.names) | |
28 | # ...and the other stuff too, I guess, but in practice no. | |
29 | ||
30 | def __eq__(self, other): | |
31 | return isinstance(other, Object) and self.names == other.names | |
32 | # ...and the other stuff too, I guess, but in practice no. | |
33 | ||
34 | def __repr__(self): | |
35 | return "%s(names=%r)" % (self.__class__.__name__, self.names) | |
36 | # ...and the other stuff too, I guess, but in practice no. | |
37 | ||
38 | @property | |
39 | def name(self): | |
40 | return self.names[0].replace('{rank}', str(getattr(self, 'rank', ''))) | |
41 | ||
42 | @property | |
43 | def indefinite_article(self): | |
44 | return 'a ' | |
45 | ||
46 | @property | |
47 | def definite_article(self): | |
48 | return 'the ' | |
49 | ||
50 | # Not sure how I feel about these three methods -- see marysue.state.State | |
51 | ||
52 | @property | |
53 | def definite(self): | |
54 | article = self.definite_article | |
55 | fnite = self._fnite() | |
56 | return article + fnite | |
57 | ||
58 | @property | |
59 | def indefinite(self): | |
60 | article = self.indefinite_article | |
61 | fnite = self._fnite() | |
62 | if article == 'a 'and fnite[0].upper() in ('A', 'E', 'I', 'O', 'U'): | |
63 | article = 'an ' | |
64 | return article + fnite | |
65 | ||
66 | def _fnite(self): | |
67 | return self.name | |
68 | ||
69 | @property | |
70 | def possessive(self): | |
71 | return self.definite + "'s" | |
72 | ||
73 | @property | |
74 | def possessive_pronoun(self): | |
75 | return 'its' | |
76 | ||
77 | @property | |
78 | def accusative_pronoun(self): | |
79 | return "it" | |
80 | ||
81 | @property | |
82 | def accusative(self): | |
83 | return self.accusative_pronoun | |
84 | ||
85 | @property | |
86 | def pronoun(self): | |
87 | return "it" | |
88 | ||
89 | @property | |
90 | def distal_pronoun(self): | |
91 | return 'that' | |
92 | ||
93 | @property | |
94 | def proximal_pronoun(self): | |
95 | return 'this' | |
96 | ||
97 | @property | |
98 | def distal(self): | |
99 | return self.distal_pronoun + " " + self._fnite() | |
100 | ||
101 | @property | |
102 | def proximal(self): | |
103 | return self.proximal_pronoun + " " + self._fnite() | |
104 | ||
105 | @property | |
106 | def was(self): | |
107 | return "was" | |
108 | ||
109 | @property | |
110 | def colours(self): | |
111 | return ( | |
112 | 'red', 'yellow', 'blue', 'purple', 'green', 'orange', | |
113 | 'mauve', 'brown', 'black', 'white', | |
114 | 'tan', 'chartreuse', 'maroon', 'pink', 'violet', | |
115 | 'orange red', 'blue green', 'navy blue', | |
116 | 'gold coloured', 'silver coloured', | |
117 | #'fuschia', 'ecru', 'puce', | |
118 | ||
119 | 'floral print', 'plaid', 'tie dyed', 'pinstripe', | |
120 | # 'herringbone', | |
121 | # 'houndstooth', | |
122 | ) | |
123 | ||
124 | @property | |
125 | def colour(self): | |
126 | return random.choice(self.colours) | |
127 | ||
128 | @property | |
129 | def gibberish(self): | |
130 | def w(): | |
131 | return ''.join(random.lowercase() for _ in xrange(0, random.randint(3, 8))) | |
132 | words = [w() for _ in xrange(0, random.randint(3, 9))] | |
133 | return ' '.join(words) | |
134 | ||
135 | ||
136 | class PluralMixin(object): | |
137 | @property | |
138 | def indefinite_article(self): | |
139 | return 'some ' | |
140 | ||
141 | @property | |
142 | def definite_article(self): | |
143 | return 'the ' | |
144 | ||
145 | @property | |
146 | def possessive_pronoun(self): | |
147 | return "their" | |
148 | ||
149 | @property | |
150 | def accusative_pronoun(self): | |
151 | return "them" | |
152 | ||
153 | @property | |
154 | def pronoun(self): | |
155 | return "them" | |
156 | ||
157 | @property | |
158 | def distal_pronoun(self): | |
159 | return 'those' | |
160 | ||
161 | @property | |
162 | def proximal_pronoun(self): | |
163 | return 'these' | |
164 | ||
165 | @property | |
166 | def was(self): | |
167 | return "were" | |
168 | ||
169 | @property | |
170 | def singular(self): | |
171 | if self.name.endswith('s'): | |
172 | return self.name[:-1] | |
173 | else: | |
174 | return self.name | |
175 | ||
176 | ||
177 | class Plural(PluralMixin, Object): | |
178 | pass | |
179 | ||
180 | ||
181 | class ProperMixin(object): | |
182 | @property | |
183 | def indefinite_article(self): | |
184 | return '' | |
185 | ||
186 | @property | |
187 | def definite_article(self): | |
188 | return '' | |
189 | ||
190 | ||
191 | class Proper(ProperMixin, Object): | |
192 | pass | |
193 | ||
194 | ||
195 | class MasculineMixin(ProperMixin): | |
196 | @property | |
197 | def possessive_pronoun(self): | |
198 | return "his" | |
199 | ||
200 | @property | |
201 | def accusative_pronoun(self): | |
202 | return "him" | |
203 | ||
204 | @property | |
205 | def pronoun(self): | |
206 | return "he" | |
207 | ||
208 | ||
209 | class FeminineMixin(Object): | |
210 | @property | |
211 | def possessive_pronoun(self): | |
212 | return "her" | |
213 | ||
214 | @property | |
215 | def accusative_pronoun(self): | |
216 | return "her" | |
217 | ||
218 | @property | |
219 | def pronoun(self): | |
220 | return "she" | |
221 | ||
222 | ||
223 | class Group(PluralMixin): | |
224 | def __init__(self, *subjects): | |
225 | if len(subjects) == 0: | |
226 | raise ValueError("Group must include at least one object") | |
227 | self.subjects = subjects | |
228 | ||
229 | def __repr__(self): | |
230 | children = ', '.join([repr(c) for c in self.subjects]) | |
231 | return "%s(%s)" % (self.__class__.__name__, children) | |
232 | ||
233 | def __iter__(self): | |
234 | return self.subjects.__iter__() | |
235 | ||
236 | def __getitem__(self, index): | |
237 | return self.subjects[index] | |
238 | ||
239 | def __len__(self): | |
240 | return len(self.subjects) | |
241 | ||
242 | def __add__(self, other): | |
243 | total = self.subjects + tuple(o for o in other) | |
244 | return Group(*total) | |
245 | ||
246 | @property | |
247 | def definite(self): | |
248 | if len(self.subjects) == 0: | |
249 | return 'no one' | |
250 | elif len(self.subjects) == 1: | |
251 | return self.subjects[0].definite | |
252 | elif len(self.subjects) == 2: | |
253 | return self.subjects[0].definite + ' and ' + self.subjects[1].definite | |
254 | return ', '.join([s.definite for s in self.subjects[:-1]]) + ', and ' + self.subjects[-1].definite | |
255 | ||
256 | @property | |
257 | def pronoun(self): | |
258 | return self.definite |
0 | import marysue.util as random | |
1 | from marysue.ast import AST | |
2 | from marysue.storytree import EventSequence | |
3 | from marysue.events import * | |
4 | from marysue.duties import RescueDuty, RetrieveDuty | |
5 | from marysue.characters import Character, TheOptimist, MarySue | |
6 | ||
7 | ||
8 | class Plot(AST): | |
9 | slots = ( | |
10 | 'subject', # often a Group instead of a single character | |
11 | 'object', # usually a single characters | |
12 | 'object2', # used rarely | |
13 | 'bystanders', # other characters witnessing this | |
14 | 'exeunt', # characters eligible to travel to another setting | |
15 | 'disqualified', | |
16 | 'requalified', | |
17 | 'setting', # the current setting | |
18 | ) | |
19 | ||
20 | ||
21 | class PlotSequence(Plot): | |
22 | """Acts as a container for (sub)sequences of PlotDevelopments.""" | |
23 | ||
24 | def render(self): | |
25 | return '. '.join([child.render() for child in self]) + '.' | |
26 | ||
27 | def print_synopsis(self): | |
28 | for n, point in enumerate(self): | |
29 | assert point.setting, point | |
30 | print "%s. %s %s,\n %s." % ( | |
31 | n, point.setting.preposition, point.setting.definite, | |
32 | point.render() | |
33 | ) | |
34 | ||
35 | def flatten(self): | |
36 | return self.__class__(*list(self.all_children()), **dict(self.iteritems())) | |
37 | ||
38 | def all_children(self): | |
39 | for child in self: | |
40 | if isinstance(child, self.__class__): | |
41 | for descendant in child.all_children(): | |
42 | yield descendant | |
43 | else: | |
44 | yield child | |
45 | ||
46 | ||
47 | class PlotDevelopment(Plot): | |
48 | def create_events(self): | |
49 | raise NotImplementedError | |
50 | ||
51 | def all_involved_characters(self): | |
52 | return Character.characters_to_set(self.subject, self.object, self.object2, self.bystanders) | |
53 | ||
54 | ||
55 | ||
56 | # - - - - | |
57 | ||
58 | ||
59 | class Introduction(PlotDevelopment): | |
60 | templates = ( | |
61 | "We are introduced to {subject.def}", | |
62 | ) | |
63 | ||
64 | def create_events(self): | |
65 | e = [BecomeHappyEvent(subject=s) for s in self.subject] | |
66 | seen = set() | |
67 | for s in self.subject: | |
68 | if not seen or random.chance(66): | |
69 | if random.chance(85): | |
70 | e.append(PoseDescription(subject=s)) | |
71 | else: | |
72 | e.append(random.choice(( | |
73 | OutOfEggsEvent(subject=s), | |
74 | GenericHyperCrystalUsageEvent(subject=s), | |
75 | ))) | |
76 | if seen and random.chance(66): | |
77 | e.append(GreetEvent(subject=s, object=random.choice(seen))) | |
78 | seen.add(s) | |
79 | return e | |
80 | ||
81 | ||
82 | class ChekovsGun(PlotDevelopment): | |
83 | templates = ( | |
84 | "{object.def} is foreshadowed by {subject.def}, witnessed by {bystanders.def}", | |
85 | ) | |
86 | ||
87 | def create_events(self): | |
88 | holder = self.subject | |
89 | asker = random.choice(self.bystanders) | |
90 | return [ | |
91 | EventSequence( | |
92 | IntroduceItemEvent(subject=holder, object=self.object), | |
93 | AskAboutItemEvent(subject=asker, object=self.object), | |
94 | ReplyAboutItemEvent(subject=holder, object=asker), | |
95 | ) | |
96 | ] | |
97 | ||
98 | ||
99 | class ChekovsFun(PlotDevelopment): | |
100 | templates = ( | |
101 | "{subject.def} is reminded of their mishaps with {object.def}, witnessed by {bystanders.def}", | |
102 | ) | |
103 | ||
104 | def create_events(self): | |
105 | holder = self.subject | |
106 | asker = random.choice(self.bystanders) | |
107 | return [ | |
108 | EventSequence( | |
109 | RememberMacGuffinEvent(subject=asker, object=holder, object2=self.object), | |
110 | ) | |
111 | ] | |
112 | ||
113 | ||
114 | class Convalescence(PlotDevelopment): | |
115 | templates = ( | |
116 | "{subject.def} convalesce after their adventures", | |
117 | ) | |
118 | ||
119 | def create_events(self): | |
120 | prots = set(self.subject) | |
121 | ||
122 | ms = random.extract(prots, filter=lambda x: isinstance(x, MarySue)) | |
123 | capt = random.extract(prots, filter=lambda x: isinstance(x, TheOptimist)) | |
124 | ||
125 | assert ms and capt | |
126 | e = [ | |
127 | ComplimentActionEvent(subject=capt, object=ms), | |
128 | OfferPromotionEvent(subject=capt, object=ms), | |
129 | OhYouEvent(subject=ms, object=capt), | |
130 | RefusePromotionEvent(subject=ms, object=capt), | |
131 | ] | |
132 | e += [ | |
133 | EventSequence( | |
134 | BecomeHappyEvent(subject=s), | |
135 | ConvalesceEvent(subject=s), | |
136 | ) for s in self.subject | |
137 | ] | |
138 | return e | |
139 | ||
140 | ||
141 | class AllLaugh(PlotDevelopment): | |
142 | # It may seem odd for this to be a plot development, but it | |
143 | # turns out to be the easiest way to do it in the current architecture | |
144 | templates = ( | |
145 | "{subject.def} all laugh", | |
146 | ) | |
147 | ||
148 | def create_events(self): | |
149 | return [ | |
150 | AllLaughEvent(), | |
151 | ] | |
152 | ||
153 | ||
154 | # - - - - | |
155 | ||
156 | ||
157 | class Kidnapping(PlotDevelopment): | |
158 | templates = ( | |
159 | "{subject.def} kidnaps {obj.def}, witnessed by {bystanders.def}", | |
160 | ) | |
161 | ||
162 | def create_events(self): | |
163 | all_protagonists = list(self.bystanders) + [self.object] | |
164 | perp = self.subject | |
165 | duty = RescueDuty(self.object) | |
166 | return [ | |
167 | BecomeHappyEvent(subject=perp), | |
168 | AppearEvent(subject=perp), | |
169 | ] + [ | |
170 | BecomeAngryEvent(subject=s) for s in all_protagonists | |
171 | ] + [ | |
172 | NoticeAntagonistEvent(subject=random.choice(self.bystanders), object=perp), | |
173 | CackleEvent(subject=perp), | |
174 | AntagonistBanterEvent(subject=perp), | |
175 | AbductEvent(subject=perp, object=self.object), | |
176 | DisappearEvent(subject=perp, object=self.object), | |
177 | ] + [ | |
178 | EventSequence( | |
179 | BecomeAngryEvent(subject=s), | |
180 | PanicEvent(subject=s), | |
181 | AcquireDutyEvent(subject=s, object=duty), | |
182 | ) for s in self.bystanders | |
183 | ] | |
184 | ||
185 | ||
186 | class LocateAbductee(PlotDevelopment): | |
187 | templates = ( | |
188 | "{subject.def} must locate where {object.def} is being held", | |
189 | ) | |
190 | ||
191 | def create_events(self): | |
192 | prots = set(self.subject) | |
193 | b = random.extract(prots, filter=lambda x: isinstance(x, MarySue)) | |
194 | if not b: | |
195 | b = random.extract(prots) | |
196 | a = random.extract(prots) | |
197 | if not a: | |
198 | a = b | |
199 | e = [ | |
200 | WeMustFindThemEvent(subject=a, object=self.object), | |
201 | LocationHunchEvent(subject=b, object=self.object, object2=self.object2), | |
202 | ] | |
203 | if self.setting == a.home: | |
204 | e.append(SetCourseEvent(subject=a, object=self.object2)) | |
205 | else: | |
206 | e.append(WeMustGoToEvent(subject=a, object=self.object2)) | |
207 | return e | |
208 | ||
209 | ||
210 | class WayBlocked(PlotDevelopment): | |
211 | templates = ( | |
212 | "{subject.def} finds {subject.his} way is blocked", | |
213 | ) | |
214 | ||
215 | def create_events(self): | |
216 | prots = set(self.subject) | |
217 | a = random.extract(prots) | |
218 | b = random.extract(prots) | |
219 | return [ | |
220 | BumpIntoForceFieldEvent(subject=a), | |
221 | BecomeSadEvent(subject=a), | |
222 | EventSequence( | |
223 | WhatWasThatEvent(subject=b), | |
224 | BumpIntoForceFieldEvent(subject=b), | |
225 | BecomeSadEvent(subject=b), | |
226 | ) if b else None, | |
227 | MustRetraceStepsEvent(subject=a), | |
228 | StateDutyEvent(subject=a), | |
229 | ] | |
230 | ||
231 | ||
232 | class Rescue(PlotDevelopment): | |
233 | templates = ( | |
234 | "{subject.def} rescues {obj.def}", | |
235 | ) | |
236 | ||
237 | def create_events(self): | |
238 | # Note: even though this is a different Duty object than was | |
239 | # put in the character's duties list, apparently it will work | |
240 | # because we made these things immutable -- they hash the same. | |
241 | duty = RescueDuty(self.object) | |
242 | return [ | |
243 | KeepMovingEvent(subject=random.choice(self.subject)), | |
244 | #WhatWasThatNoiseEvent(subject=random.choice(self.subject)), | |
245 | TurnCornerEvent(subject=self.subject), | |
246 | BehindBarsDescription(subject=self.object), | |
247 | ] + [ | |
248 | RescueEvent(subject=self.subject, object=self.object), | |
249 | BecomeHappyEvent(subject=self.object), | |
250 | ] + [ | |
251 | RelieveDutyEvent(subject=s, object=duty) for s in self.subject | |
252 | ] + [ | |
253 | EventSequence( | |
254 | ThankEvent(subject=self.object, object=s), | |
255 | BecomeHappyEvent(subject=s), | |
256 | ) for s in self.subject | |
257 | ] | |
258 | ||
259 | ||
260 | # - - - - | |
261 | ||
262 | ||
263 | class LoseItem(PlotDevelopment): | |
264 | templates = ( | |
265 | "{subject.def} loses {obj.def}, witnessed by {bystanders.def}", | |
266 | ) | |
267 | ||
268 | def create_events(self): | |
269 | s = self.subject | |
270 | duty = RetrieveDuty(self.object) | |
271 | e = [ | |
272 | LookAroundEvent(subject=s), | |
273 | HoldEvent(subject=s, object=self.object), | |
274 | #CommentOnItemEvent(subject=s, object=self.object), | |
275 | TripEvent(subject=s), | |
276 | DropEvent(subject=s, object=self.object), | |
277 | LoseEvent(subject=self.object), | |
278 | BecomeEmbarrassedEvent(subject=s), | |
279 | OopsEvent(subject=s), | |
280 | AcquireDutyEvent(subject=s, object=duty), | |
281 | ] | |
282 | for bystander in self.bystanders: | |
283 | e.append(BecomeSadEvent(subject=bystander)) | |
284 | e.append(AcquireDutyEvent(subject=bystander, object=duty)) | |
285 | e.append( | |
286 | random.choice(( | |
287 | LookAtEvent(subject=bystander, object=s), | |
288 | GestureAtEvent(subject=bystander, object=s), | |
289 | )) | |
290 | ) | |
291 | ||
292 | return e | |
293 | ||
294 | ||
295 | class RecoverItem(PlotDevelopment): | |
296 | templates = ( | |
297 | "{subject.def} recovers {obj.def}, witnessed by {bystanders.def}", | |
298 | ) | |
299 | ||
300 | def create_events(self): | |
301 | s = self.subject | |
302 | duty = RetrieveDuty(self.object) | |
303 | return [ | |
304 | LookAroundEvent(subject=s), | |
305 | HunchEvent(subject=s), | |
306 | LookBehindEvent(subject=s, object=self.setting.nearby_scenery), | |
307 | FindEvent(subject=s, object=self.object), | |
308 | RelieveDutyEvent(subject=s, object=duty), | |
309 | BecomeHappyEvent(subject=s), | |
310 | ] + [ | |
311 | EventSequence( | |
312 | RelieveDutyEvent(subject=bystander, object=duty), | |
313 | BecomeHappyEvent(subject=bystander), | |
314 | ) for bystander in self.bystanders | |
315 | ] + [ | |
316 | ReliefEvent(subject=random.choice(self.bystanders)) | |
317 | ] | |
318 | ||
319 | ||
320 | # - - - - | |
321 | ||
322 | ||
323 | class TrappedInRubble(PlotDevelopment): | |
324 | templates = ( | |
325 | "{subject.def} is trapped under rubble, witnessed by {bystanders.def}", | |
326 | ) | |
327 | ||
328 | def create_events(self): | |
329 | s = self.subject | |
330 | duty = RescueDuty(self.subject) | |
331 | return [ | |
332 | RumblingSoundEvent(), | |
333 | WhatWasThatNoiseEvent(subject=random.choice(self.bystanders)), | |
334 | CaveInEvent(subject=self.setting.roof), | |
335 | StunnedEvent(subject=s), | |
336 | BuriedUnderRubbleEvent(subject=s), | |
337 | BecomeSadEvent(subject=s), | |
338 | ] + [ | |
339 | EventSequence( | |
340 | BecomeSadEvent(subject=bystander), | |
341 | PanicEvent(subject=bystander), | |
342 | AcquireDutyEvent(subject=bystander, object=duty), | |
343 | ) for bystander in self.bystanders | |
344 | ] | |
345 | ||
346 | ||
347 | class ExtractedFromRubble(PlotDevelopment): | |
348 | templates = ( | |
349 | "{subject.def} dig {obj.def} out of the rubble", | |
350 | ) | |
351 | ||
352 | def create_events(self): | |
353 | duty = RescueDuty(self.object) | |
354 | return [ | |
355 | EventSequence( | |
356 | WasteNoTimeEvent(subject=s), | |
357 | DigOutEvent(subject=s, object=self.object) | |
358 | ) for s in self.subject | |
359 | ] + [ | |
360 | BecomeHappyEvent(subject=self.object), | |
361 | ] + [ | |
362 | EventSequence( | |
363 | ThankEvent(subject=self.object, object=s), | |
364 | BecomeHappyEvent(subject=s), | |
365 | RelieveDutyEvent(subject=s, object=duty), | |
366 | ) for s in self.subject | |
367 | ] | |
368 | ||
369 | ||
370 | # - - - - | |
371 | ||
372 | ||
373 | class GoonAmbush(PlotDevelopment): | |
374 | templates = ( | |
375 | "{subject.def} are ambushed by {obj.def}", | |
376 | ) | |
377 | ||
378 | def create_events(self): | |
379 | return [ | |
380 | AttackEvent(subject=self.object, object=self.subject), | |
381 | ] + [ | |
382 | EventSequence( | |
383 | BecomeAngryEvent(subject=s), | |
384 | ) for s in self.subject | |
385 | ] + [ | |
386 | EventSequence( | |
387 | GenericBattleDescription(), | |
388 | ) | |
389 | ] | |
390 | ||
391 | ||
392 | class GoonEncounter(PlotDevelopment): | |
393 | templates = ( | |
394 | "{subject.def} encounter a band of {obj.def}", | |
395 | ) | |
396 | ||
397 | def create_events(self): | |
398 | prots = set(self.subject) | |
399 | a = random.extract(prots) | |
400 | return [ | |
401 | EncounterEvent(subject=self.object, object=self.subject), | |
402 | GoonBanterEvent(subject=a, object=self.object), | |
403 | GoonParlayEvent(subject=self.object), | |
404 | ] + [ | |
405 | EventSequence( | |
406 | BecomeAngryEvent(subject=s), | |
407 | ) for s in self.subject | |
408 | ] + [ | |
409 | EventSequence( | |
410 | GenericBattleDescription(), | |
411 | ) | |
412 | ] | |
413 | ||
414 | ||
415 | class ProtagonistAttack(PlotDevelopment): | |
416 | templates = ( | |
417 | "{subject.def} attack {obj.def}", | |
418 | ) | |
419 | ||
420 | def create_events(self): | |
421 | e = [] | |
422 | # maybe later a fight will have all subjects. FOR NOW, | |
423 | s = random.choice(self.subject) | |
424 | w = s.weapon | |
425 | e.append( | |
426 | EventSequence( | |
427 | WarCryEvent(subject=s), | |
428 | UnsheathEvent(subject=s, object=w), | |
429 | LiftWeaponEvent(subject=s, object=w), | |
430 | BringDownWeaponEvent(subject=s, object=w), | |
431 | WeaponContactEvent(subject=w, object=self.object), | |
432 | FlyAcrossEvent(subject=self.object), | |
433 | ) | |
434 | ) | |
435 | return e | |
436 | ||
437 | ||
438 | class AwkwardCombat(PlotDevelopment): | |
439 | templates = ( | |
440 | "{subject.def} get in awkward situation during combat with {obj.def}", | |
441 | ) | |
442 | ||
443 | def create_events(self): | |
444 | mary_sue = self.subject[0] | |
445 | dreamboat = self.subject[1] | |
446 | return [ | |
447 | EventSequence( | |
448 | TryToGetBehindEvent(subject=mary_sue, object=self.object), | |
449 | BumpIntoAwkwardlyEvent(subject=mary_sue, object=dreamboat), | |
450 | LookAtEvent(subject=dreamboat, object=mary_sue), | |
451 | BlushEvent(subject=mary_sue), | |
452 | BecomeEmbarrassedEvent(subject=mary_sue), | |
453 | RushIntoFrayEvent(subject=dreamboat), | |
454 | ) | |
455 | ] | |
456 | ||
457 | ||
458 | class Vanquished(PlotDevelopment): | |
459 | templates = ( | |
460 | "{subject.def} vanquish {obj.def} (they were sent by {object2.def})", | |
461 | ) | |
462 | ||
463 | def create_events(self): | |
464 | e = [ | |
465 | AfterBattleEvent(subject=self.object), | |
466 | ] | |
467 | s = random.choice(self.subject) | |
468 | e.append( | |
469 | EventSequence( | |
470 | AfterBattleBanterEvent(subject=random.choice(self.subject)), | |
471 | ) | |
472 | ) | |
473 | for s in self.subject: | |
474 | e.append(BecomeHappyEvent(subject=s)) | |
475 | ||
476 | subjects = set(self.subject) | |
477 | a = random.extract(subjects) | |
478 | b = random.extract(subjects, filter=lambda x: not isinstance(x, TheOptimist)) | |
479 | e.append( | |
480 | EventSequence( | |
481 | BecomeAngryEvent(subject=self.object2), # b/c they need to have a mood assigned first! | |
482 | ObserveAfterBattleEvent(subject=a, object=self.object, object2=self.object2), | |
483 | RemindReportEvent(subject=a), | |
484 | ) | |
485 | ) | |
486 | if b: | |
487 | e.append( | |
488 | EventSequence( | |
489 | BecomeSadEvent(subject=b), | |
490 | IfWeGetHomeEvent(subject=b), | |
491 | ) | |
492 | ) | |
493 | ||
494 | return e | |
495 | ||
496 | ||
497 | # - - - - | |
498 | ||
499 | ||
500 | class AwkwardTension(PlotDevelopment): | |
501 | templates = ( | |
502 | "There is an awkward moment between {subject.def} and {obj.def}", | |
503 | ) | |
504 | ||
505 | def create_events(self): | |
506 | s = self.subject | |
507 | return [ | |
508 | BecomeEmbarrassedEvent(subject=self.subject), | |
509 | PullAsideEvent(subject=self.subject, object=self.object), | |
510 | WantToTalkToYouEvent(subject=self.subject, object=self.object), | |
511 | WhatIsItEvent(subject=self.object, object=self.subject), | |
512 | RecallPastEventEvent(subject=self.subject, object=self.object), | |
513 | BecomeEmbarrassedEvent(subject=self.object), | |
514 | BlushEvent(subject=self.object), | |
515 | SayNoMoreEvent(subject=self.object, object=self.subject), | |
516 | BecomeSadEvent(subject=self.subject), | |
517 | BecomeHappyEvent(subject=self.object), | |
518 | ] | |
519 | ||
520 | ||
521 | class RomanticTension(PlotDevelopment): | |
522 | templates = ( | |
523 | "{subject.def} and {obj.def} do mushy stuff (almost)", | |
524 | ) | |
525 | ||
526 | def create_events(self): | |
527 | s = self.subject | |
528 | e = [ | |
529 | BecomeEmbarrassedEvent(subject=self.subject), | |
530 | PullAsideEvent(subject=self.subject, object=self.object), | |
531 | WantToTalkToYouEvent(subject=self.subject, object=self.object), | |
532 | WhatIsItEvent(subject=self.object, object=self.subject), | |
533 | ] | |
534 | done = False | |
535 | while not done: | |
536 | e.append(EventSequence( | |
537 | FidgetEvent(subject=self.subject, object=self.object), | |
538 | BecomeEmbarrassedEvent(subject=self.object), | |
539 | WhatIsItEvent(subject=self.object, object=self.subject), | |
540 | FacesCloseTogetherEvent(subject=self.object, object=self.subject), | |
541 | )) | |
542 | if random.chance(33): | |
543 | done = True | |
544 | e.append(PreludeToKissEvent(subject=self.subject, object=self.object)) | |
545 | return e | |
546 | ||
547 | ||
548 | class RomanticResolution(PlotDevelopment): | |
549 | templates = ( | |
550 | "{subject.def} and {obj.def} do mushy stuff", | |
551 | ) | |
552 | ||
553 | def create_events(self): | |
554 | s = self.subject | |
555 | e = [ | |
556 | BecomeHappyEvent(subject=self.subject), | |
557 | BecomeHappyEvent(subject=self.object), | |
558 | PullAsideEvent(subject=self.subject, object=self.object), | |
559 | WhatIsItEvent(subject=self.object, object=self.subject), | |
560 | MushyStuffEvent(subject=self.subject, object=self.object), | |
561 | OhYouEvent(subject=self.object, object=self.subject), | |
562 | KissEvent(subject=self.object, object=self.subject), | |
563 | AndTheyKissedEvent(subject=self.object, object=self.subject), | |
564 | OhYouEvent(subject=self.object, object=self.subject), | |
565 | ] | |
566 | return e | |
567 | ||
568 | ||
569 | # - - - - | |
570 | ||
571 | ||
572 | class Drone(PlotDevelopment): | |
573 | templates = ( | |
574 | "{subject.def} is startled by a drone", | |
575 | ) | |
576 | ||
577 | def create_events(self): | |
578 | s = self.subject | |
579 | return [ | |
580 | DroneEvent(subject=s), | |
581 | BecomeAngryEvent(subject=s), | |
582 | WhatWasThatEvent(subject=s), | |
583 | EmoteEvent(subject=s), | |
584 | ] | |
585 | ||
586 | ||
587 | class ContemplateRock(PlotDevelopment): | |
588 | templates = ( | |
589 | "{subject.def} contemplates {obj.indef}", | |
590 | ) | |
591 | ||
592 | def create_events(self): | |
593 | s = self.subject | |
594 | return [ | |
595 | PickUpEvent(subject=s, object=self.object), | |
596 | HoldEvent(subject=s, object=self.object), | |
597 | ContemplateEvent(subject=s, object=self.object), | |
598 | BecomeSadEvent(subject=s), | |
599 | ] | |
600 | ||
601 | ||
602 | # - - - - | |
603 | ||
604 | ||
605 | class Journey(PlotDevelopment): | |
606 | templates = ( | |
607 | "{subject.def} travel to {obj.def}", | |
608 | ) | |
609 | ||
610 | def create_events(self): | |
611 | return [ | |
612 | TravelToEvent(subject=self.subject, object=self.object) | |
613 | ] | |
614 | ||
615 | ||
616 | class EncounterNewSetting(PlotDevelopment): | |
617 | templates = ( | |
618 | "{subject.def} encounter a new setting", #, {setting.def}", | |
619 | ) | |
620 | ||
621 | def create_events(self): | |
622 | prots = set(self.subject) | |
623 | snarker = random.extract(prots, filter=lambda x: not isinstance(x, TheOptimist)) | |
624 | reader = random.extract(prots) | |
625 | e = [] | |
626 | if snarker: | |
627 | e.append(EventSequence( | |
628 | BecomeSadEvent(subject=snarker), | |
629 | CommentOnPlaceEvent(subject=snarker), | |
630 | )) | |
631 | if reader: | |
632 | e.append(EventSequence( | |
633 | TakeReadingsEvent(subject=reader), | |
634 | ReadingsBanterEvent(subject=snarker), | |
635 | )) | |
636 | elif reader: | |
637 | e.append(TakeReadingsEvent(subject=reader)) | |
638 | return e | |
639 | ||
640 | ||
641 | class DreamSequence(PlotDevelopment): | |
642 | """purely proof-of-concept""" | |
643 | def create_events(self): | |
644 | from marysue.events import get_all_event_classes | |
645 | ||
646 | classes = get_all_event_classes() | |
647 | z = [random.choice(classes) for x in xrange(0, 20)] | |
648 | z = [cls(subject=random.choice(subjects), | |
649 | object=random.choice(subjects)) for cls in z] | |
650 | ||
651 | return z | |
652 | ||
653 | # - - - - | |
654 | ||
655 | ||
656 | def all_plot_classes(): | |
657 | return [ | |
658 | c for c in globals().values() if c.__class__ == type and | |
659 | issubclass(c, Plot) and | |
660 | c != Plot | |
661 | ] | |
662 | ||
663 | ||
664 | def get_plot_class(name): | |
665 | try: | |
666 | return globals()[name] | |
667 | except KeyError: | |
668 | for cls in all_plot_classes(): | |
669 | print cls.__name__ | |
670 | raise |
0 | from marysue.util import log | |
1 | from marysue.storytree import Story, Scene, EventSequence | |
2 | from marysue.events import * | |
3 | from marysue.objects import Group | |
4 | from marysue.characters import MarySue, DreamBoat | |
5 | ||
6 | from marysue.plot import * | |
7 | ||
8 | ||
9 | # - - - - | |
10 | ||
11 | ||
12 | class PlotHole(AST): | |
13 | """Represents a place in the plot tree that will be filled in with | |
14 | a subplot.""" | |
15 | ||
16 | slots = ( | |
17 | 'rules', | |
18 | 'setting', | |
19 | 'goons', | |
20 | 'abductee', | |
21 | 'abductee_location', | |
22 | ) | |
23 | ||
24 | def flatten(self): | |
25 | raise NotImplementedError | |
26 | ||
27 | def render(self): | |
28 | return "((to be written))" | |
29 | ||
30 | ||
31 | # - - - - | |
32 | ||
33 | ||
34 | class InapplicableRuleError(ValueError): | |
35 | pass | |
36 | ||
37 | ||
38 | class PlotRewritingRule(object): | |
39 | def assign_participants(self, plotter, plot_hole, unavailable): | |
40 | """plotter is the Plotter object that is applying this rule. | |
41 | ||
42 | plot_hole is the PlotHole that is being rewritten into a (sub)plot. | |
43 | ||
44 | """ | |
45 | self.plotter = plotter | |
46 | self.plot_hole = plot_hole | |
47 | self.setting = plot_hole.setting | |
48 | self.unavailable = unavailable | |
49 | try: | |
50 | self.assign_participants_impl() | |
51 | return True | |
52 | except InapplicableRuleError: | |
53 | return False | |
54 | ||
55 | def assign_participants_impl(self): | |
56 | raise NotImplementedError | |
57 | ||
58 | def only_at_home(self): | |
59 | if self.plot_hole.setting != self.plotter.home: | |
60 | raise InapplicableRuleError | |
61 | ||
62 | def not_at_home(self): | |
63 | if self.plot_hole.setting == self.plotter.home: | |
64 | raise InapplicableRuleError | |
65 | ||
66 | def compute_protagonists(self): | |
67 | self.protagonists = set(self.plotter.protagonists) - self.unavailable | |
68 | ||
69 | def pick_protagonist_and_others(self, filter=None): | |
70 | if filter is None: | |
71 | filter = lambda x: True | |
72 | eligible = set([x for x in self.protagonists if filter(x)]) | |
73 | if not eligible: | |
74 | raise InapplicableRuleError | |
75 | self.p1 = random.choice(eligible) | |
76 | self.others = set(self.protagonists) | |
77 | self.others.remove(self.p1) | |
78 | if not self.others: | |
79 | raise InapplicableRuleError | |
80 | ||
81 | def pick_antagonist(self): | |
82 | self.a1 = random.choice(self.plotter.antagonists) | |
83 | ||
84 | def pick_macguffin(self): | |
85 | eligible = set(self.plotter.macguffins) - self.unavailable | |
86 | if not eligible: | |
87 | raise InapplicableRuleError | |
88 | self.m1 = random.choice(eligible) | |
89 | ||
90 | def pick_mary_sue_and_dreamboat(self): | |
91 | self.mary_sues = tuple([p for p in self.protagonists if isinstance(p, MarySue)]) | |
92 | self.dreamboats = tuple([p for p in self.protagonists if isinstance(p, DreamBoat)]) | |
93 | if not self.mary_sues or not self.dreamboats: | |
94 | raise InapplicableRuleError | |
95 | self.mary_sue = random.choice(self.mary_sues) | |
96 | self.dreamboat = random.choice(self.dreamboats) | |
97 | ||
98 | def generate(self, plotter): | |
99 | raise NotImplementedError | |
100 | ||
101 | ||
102 | def not_mary_sue(obj): | |
103 | return not isinstance(obj, MarySue) | |
104 | ||
105 | ||
106 | # - - - - | |
107 | ||
108 | ||
109 | class KidnappingRule(PlotRewritingRule): | |
110 | def assign_participants_impl(self): | |
111 | self.compute_protagonists() | |
112 | self.pick_protagonist_and_others(filter=not_mary_sue) | |
113 | self.pick_antagonist() | |
114 | ||
115 | def generate(self, plotter): | |
116 | bystanders = Group(*self.others) | |
117 | abductee_location = self.a1.home | |
118 | return PlotSequence( | |
119 | Kidnapping( | |
120 | subject=self.a1, object=self.p1, setting=self.setting, | |
121 | bystanders=bystanders, disqualified=self.p1, | |
122 | exeunt=bystanders | |
123 | ), | |
124 | PlotHole( | |
125 | setting=self.setting, | |
126 | abductee=self.p1, | |
127 | abductee_location=abductee_location, | |
128 | rules=( | |
129 | FindAbducteeRule, | |
130 | ContemplateRockRule, | |
131 | ) | |
132 | ), | |
133 | PlotHole( | |
134 | setting=abductee_location, | |
135 | rules=( | |
136 | KidnappingRule, | |
137 | FindAnotherWayRule, | |
138 | LostItemRule, | |
139 | CaveInRule, | |
140 | GoonSkirmishRule, | |
141 | AwkwardTensionRule, | |
142 | RomanticTensionRule, | |
143 | RomanticResolutionRule, | |
144 | ContemplateRockRule, | |
145 | ) | |
146 | ), | |
147 | Rescue( | |
148 | subject=Group(*self.others), object=self.p1, | |
149 | setting=self.a1.home, requalified=self.p1, | |
150 | exeunt=Group(*self.protagonists) | |
151 | ), | |
152 | ) | |
153 | ||
154 | ||
155 | class FindAbducteeRule(PlotRewritingRule): | |
156 | def assign_participants_impl(self): | |
157 | self.compute_protagonists() | |
158 | ||
159 | def generate(self, plotter): | |
160 | return PlotSequence( | |
161 | LocateAbductee( | |
162 | subject=Group(*self.protagonists), | |
163 | object=self.plot_hole.abductee, | |
164 | object2=self.plot_hole.abductee_location, | |
165 | setting=self.setting | |
166 | ), | |
167 | PlotHole( | |
168 | setting=self.setting, | |
169 | abductee=self.plot_hole.abductee_location, | |
170 | rules=( | |
171 | ContemplateRockRule, | |
172 | ) | |
173 | ), | |
174 | ) | |
175 | ||
176 | ||
177 | class FindAnotherWayRule(PlotRewritingRule): | |
178 | def assign_participants_impl(self): | |
179 | if not self.plot_hole.setting.outside_setting: | |
180 | raise InapplicableRuleError | |
181 | self.plot_hole.setting | |
182 | self.compute_protagonists() | |
183 | ||
184 | def generate(self, plotter): | |
185 | new_setting = self.plot_hole.setting.outside_setting | |
186 | return PlotSequence( | |
187 | WayBlocked( | |
188 | subject=Group(*self.protagonists), setting=self.setting | |
189 | ), | |
190 | PlotHole( | |
191 | setting=new_setting, | |
192 | rules=( | |
193 | KidnappingRule, | |
194 | LostItemRule, | |
195 | CaveInRule, | |
196 | GoonSkirmishRule, | |
197 | AwkwardTensionRule, | |
198 | RomanticTensionRule, | |
199 | ContemplateRockRule, | |
200 | ) | |
201 | ), | |
202 | ) | |
203 | ||
204 | ||
205 | class LostItemRule(PlotRewritingRule): | |
206 | def assign_participants_impl(self): | |
207 | self.compute_protagonists() | |
208 | self.pick_protagonist_and_others() | |
209 | self.pick_macguffin() | |
210 | self.mary_sues = tuple([p for p in self.protagonists if isinstance(p, MarySue)]) | |
211 | ||
212 | def generate(self, plotter): | |
213 | recoverer = self.p1 | |
214 | if self.mary_sues: | |
215 | recoverer = random.choice(self.mary_sues) | |
216 | ||
217 | # recompute bystanders so that we dont have them overlapping w/ subject | |
218 | non_losers = self.protagonists - set([self.p1]) | |
219 | non_recoverers = self.protagonists - set([recoverer]) | |
220 | ||
221 | return PlotSequence( | |
222 | LoseItem( | |
223 | subject=self.p1, object=self.m1, setting=self.setting, | |
224 | bystanders=Group(*non_losers), disqualified=self.m1 | |
225 | ), | |
226 | PlotHole( | |
227 | setting=self.setting, | |
228 | rules=( | |
229 | KidnappingRule, | |
230 | CaveInRule, | |
231 | GoonSkirmishRule, | |
232 | AwkwardTensionRule, | |
233 | RomanticTensionRule, | |
234 | ContemplateRockRule, | |
235 | DroneRule, | |
236 | ) | |
237 | ), | |
238 | RecoverItem( | |
239 | subject=recoverer, object=self.m1, setting=self.setting, | |
240 | bystanders=Group(*non_recoverers), requalified=self.m1 | |
241 | ), | |
242 | ) | |
243 | ||
244 | ||
245 | class CaveInRule(PlotRewritingRule): | |
246 | def assign_participants_impl(self): | |
247 | self.not_at_home() | |
248 | self.compute_protagonists() | |
249 | self.pick_protagonist_and_others(filter=not_mary_sue) | |
250 | ||
251 | def generate(self, plotter): | |
252 | return PlotSequence( | |
253 | TrappedInRubble(subject=self.p1, bystanders=Group(*self.others), | |
254 | setting=self.setting, disqualified=self.p1), | |
255 | PlotHole( | |
256 | setting=self.setting, | |
257 | rules=( | |
258 | KidnappingRule, | |
259 | GoonSkirmishRule, | |
260 | ) | |
261 | ), | |
262 | ExtractedFromRubble( | |
263 | subject=Group(*self.others), object=self.p1, | |
264 | setting=self.setting, requalified=self.p1 | |
265 | ), | |
266 | PlotHole( | |
267 | setting=self.setting, | |
268 | rules=( | |
269 | GoonSkirmishRule, | |
270 | AwkwardTensionRule, | |
271 | RomanticTensionRule, | |
272 | ContemplateRockRule, | |
273 | ) | |
274 | ), | |
275 | ) | |
276 | ||
277 | ||
278 | class GoonSkirmishRule(PlotRewritingRule): | |
279 | def assign_participants_impl(self): | |
280 | self.not_at_home() | |
281 | self.compute_protagonists() | |
282 | self.goons = random.choice(self.plotter.goons) | |
283 | ||
284 | def generate(self, plotter): | |
285 | meeting = random.choice(( | |
286 | GoonEncounter( | |
287 | setting=self.setting, | |
288 | subject=Group(*self.protagonists), | |
289 | object=self.goons, | |
290 | ), | |
291 | GoonAmbush( | |
292 | setting=self.setting, | |
293 | subject=Group(*self.protagonists), | |
294 | object=self.goons, | |
295 | ), | |
296 | )) | |
297 | ||
298 | return PlotSequence( | |
299 | meeting, | |
300 | PlotHole( | |
301 | setting=self.setting, | |
302 | goons=self.goons, | |
303 | rules=( | |
304 | ProtagonistAttackRule, | |
305 | AwkwardCombatRule, | |
306 | ) | |
307 | ), | |
308 | Vanquished( | |
309 | setting=self.setting, | |
310 | subject=Group(*self.protagonists), | |
311 | object=self.goons, | |
312 | object2=random.choice(self.plotter.antagonists), | |
313 | ), | |
314 | #PlotHole(setting=self.setting), | |
315 | ) | |
316 | ||
317 | ||
318 | class ProtagonistAttackRule(PlotRewritingRule): | |
319 | def assign_participants_impl(self): | |
320 | self.compute_protagonists() | |
321 | self.goons = self.plot_hole.goons | |
322 | # for hilarious effect: | |
323 | # self.goons = random.choice(self.plotter.goons) | |
324 | ||
325 | def generate(self, plotter): | |
326 | return PlotSequence( | |
327 | PlotHole( | |
328 | setting=self.setting, | |
329 | goons=self.goons, | |
330 | rules=( | |
331 | ProtagonistAttackRule, | |
332 | AwkwardCombatRule, | |
333 | ) | |
334 | ), | |
335 | ProtagonistAttack( | |
336 | subject=Group(*self.protagonists), | |
337 | object=self.goons, setting=self.setting | |
338 | ), | |
339 | ) | |
340 | ||
341 | ||
342 | class AwkwardCombatRule(PlotRewritingRule): | |
343 | def assign_participants_impl(self): | |
344 | self.compute_protagonists() | |
345 | self.pick_mary_sue_and_dreamboat() | |
346 | self.goons = self.plot_hole.goons | |
347 | ||
348 | def generate(self, plotter): | |
349 | return PlotSequence( | |
350 | PlotHole( | |
351 | setting=self.setting, | |
352 | goons=self.goons, | |
353 | rules=( | |
354 | ProtagonistAttackRule, | |
355 | AwkwardCombatRule, | |
356 | ) | |
357 | ), | |
358 | AwkwardCombat( | |
359 | subject=Group(self.mary_sue, self.dreamboat), | |
360 | object=self.goons, | |
361 | setting=self.setting | |
362 | ), | |
363 | ) | |
364 | ||
365 | ||
366 | class AwkwardTensionRule(PlotRewritingRule): | |
367 | def assign_participants_impl(self): | |
368 | self.compute_protagonists() | |
369 | self.pick_mary_sue_and_dreamboat() | |
370 | ||
371 | def generate(self, plotter): | |
372 | a = self.dreamboat | |
373 | b = self.mary_sue | |
374 | if random.chance(33): | |
375 | a, b = b, a | |
376 | ||
377 | return PlotSequence( | |
378 | AwkwardTension( | |
379 | setting=self.setting, | |
380 | subject=a, object=b, | |
381 | ), | |
382 | PlotHole( | |
383 | setting=self.setting, | |
384 | rules=( | |
385 | KidnappingRule, | |
386 | LostItemRule, | |
387 | CaveInRule, | |
388 | GoonSkirmishRule, | |
389 | ContemplateRockRule, | |
390 | DroneRule, | |
391 | ) | |
392 | ), | |
393 | ) | |
394 | ||
395 | ||
396 | class RomanticTensionRule(PlotRewritingRule): | |
397 | def assign_participants_impl(self): | |
398 | self.compute_protagonists() | |
399 | self.pick_mary_sue_and_dreamboat() | |
400 | ||
401 | def generate(self, plotter): | |
402 | a = self.dreamboat | |
403 | b = self.mary_sue | |
404 | if random.chance(33): | |
405 | a, b = b, a | |
406 | ||
407 | return PlotSequence( | |
408 | RomanticTension( | |
409 | setting=self.setting, | |
410 | subject=a, object=b, | |
411 | ), | |
412 | PlotHole( | |
413 | setting=self.setting, | |
414 | # we should try to interrupt this with something exciting! | |
415 | rules=( | |
416 | KidnappingRule, | |
417 | LostItemRule, | |
418 | CaveInRule, | |
419 | GoonSkirmishRule, | |
420 | ) | |
421 | ), | |
422 | ) | |
423 | ||
424 | ||
425 | class RomanticResolutionRule(PlotRewritingRule): | |
426 | def assign_participants_impl(self): | |
427 | self.compute_protagonists() | |
428 | self.pick_mary_sue_and_dreamboat() | |
429 | ||
430 | def generate(self, plotter): | |
431 | return PlotSequence( | |
432 | RomanticResolution( | |
433 | setting=self.setting, | |
434 | subject=self.dreamboat, object=self.mary_sue, | |
435 | ) | |
436 | ) | |
437 | ||
438 | ||
439 | class ContemplateRockRule(PlotRewritingRule): | |
440 | def assign_participants_impl(self): | |
441 | self.compute_protagonists() | |
442 | self.pick_protagonist_and_others() # Note: this prevents solo | |
443 | self.o1 = self.setting.nearby_takeable | |
444 | ||
445 | def generate(self, plotter): | |
446 | return PlotSequence( | |
447 | ContemplateRock(subject=self.p1, object=self.o1, | |
448 | setting=self.setting,), | |
449 | PlotHole( | |
450 | setting=self.setting, | |
451 | rules=( | |
452 | KidnappingRule, | |
453 | LostItemRule, | |
454 | CaveInRule, | |
455 | GoonSkirmishRule, | |
456 | AwkwardTensionRule, | |
457 | RomanticTensionRule, | |
458 | DroneRule, | |
459 | ) | |
460 | ), | |
461 | ) | |
462 | ||
463 | ||
464 | class DroneRule(PlotRewritingRule): | |
465 | def assign_participants_impl(self): | |
466 | if not self.plot_hole.setting.has_drones: | |
467 | raise InapplicableRuleError | |
468 | self.compute_protagonists() | |
469 | self.pick_protagonist_and_others() # Note: this prevents solo | |
470 | ||
471 | def generate(self, plotter): | |
472 | return PlotSequence( | |
473 | Drone(subject=self.p1, setting=self.setting), | |
474 | PlotHole( | |
475 | setting=self.setting, | |
476 | rules=( | |
477 | KidnappingRule, | |
478 | LostItemRule, | |
479 | AwkwardTensionRule, | |
480 | RomanticTensionRule, | |
481 | ) | |
482 | ), | |
483 | ) | |
484 | ||
485 | ||
486 | def all_rules(): | |
487 | return [ | |
488 | c for c in globals().values() if c.__class__ == type and | |
489 | issubclass(c, PlotRewritingRule) and | |
490 | c != PlotRewritingRule | |
491 | ] | |
492 | ||
493 | ||
494 | # - - - - | |
495 | ||
496 | ||
497 | class Plotter(object): | |
498 | def __init__(self, protagonists, antagonists, goons, macguffins, settings): | |
499 | """All arguments should be iterables""" | |
500 | self.protagonists = tuple(protagonists) | |
501 | self.antagonists = tuple(antagonists) | |
502 | self.goons = tuple(goons) | |
503 | self.macguffins = tuple(macguffins) | |
504 | self.settings = tuple(settings) | |
505 | self.home = self.settings[0] | |
506 | ||
507 | def fill_plot_hole(self, plot_hole, unavailable): | |
508 | generators = [cls() for cls in plot_hole.rules] | |
509 | generators = [ | |
510 | g for g in generators | |
511 | if g.assign_participants(self, plot_hole, unavailable) | |
512 | ] | |
513 | if not generators: | |
514 | #print "no applicable plot rule found!" | |
515 | return plot_hole | |
516 | else: | |
517 | # NOTE: must be tuple()! Not set()! Much badness with set()! | |
518 | # (It's because these Rule objects are not hashable/immutable.) | |
519 | generators = tuple(generators) | |
520 | return random.choice(generators).generate(self) | |
521 | ||
522 | def complicate_plot(self, plot, unavailable=None): | |
523 | if unavailable is None: | |
524 | unavailable = set() | |
525 | ||
526 | if isinstance(plot, PlotHole): | |
527 | return self.fill_plot_hole(plot, unavailable) | |
528 | ||
529 | if plot.disqualified: | |
530 | unavailable.add(plot.disqualified) | |
531 | if plot.requalified: | |
532 | unavailable.remove(plot.requalified) | |
533 | ||
534 | children = [ | |
535 | self.complicate_plot(c, unavailable=unavailable) for c in plot | |
536 | ] | |
537 | return plot.__class__(*children, **dict(plot.iteritems())) | |
538 | ||
539 | # - - - - | |
540 | ||
541 | def remove_plot_holes(self, plot): | |
542 | if isinstance(plot, PlotHole): | |
543 | raise ValueError | |
544 | ||
545 | children = [ | |
546 | self.remove_plot_holes(c) for c in plot if not isinstance(c, PlotHole) | |
547 | ] | |
548 | return plot.__class__(*children, **dict(plot.iteritems())) | |
549 | ||
550 | def commute_commutable_plots(self, plot): | |
551 | # requires flat plot | |
552 | children = [] | |
553 | for child in plot: | |
554 | if children: | |
555 | last_scene = children[-1] | |
556 | if last_scene.setting == child.setting and \ | |
557 | isinstance(last_scene, (Rescue, RecoverItem)) and \ | |
558 | isinstance(child, (Rescue, RecoverItem)): | |
559 | p1 = last_scene.all_involved_characters() | |
560 | p2 = child.all_involved_characters() | |
561 | ||
562 | if p2 <= p1: | |
563 | self.commuted_plots.append( | |
564 | (last_scene.__class__.__name__, | |
565 | last_scene.object.definite, | |
566 | child.__class__.__name__, | |
567 | child.object.definite) | |
568 | ) | |
569 | children[-1] = child | |
570 | child = last_scene | |
571 | children.append(child) | |
572 | ||
573 | return plot.__class__(*[self.commute_commutable_plots(c) for c in children], **dict(plot.iteritems())) | |
574 | ||
575 | def remove_repetitive_plots(self, plot): | |
576 | # requires flat plot | |
577 | children = [] | |
578 | for child in plot: | |
579 | add_it = True | |
580 | if children: | |
581 | last_scene = children[-1] | |
582 | # other possibilities: ContemplateRock | |
583 | if isinstance(last_scene, AwkwardTension) and isinstance(child, AwkwardTension): | |
584 | add_it = False | |
585 | if add_it: | |
586 | children.append(child) | |
587 | ||
588 | return plot.__class__(*children, **dict(plot.iteritems())) | |
589 | ||
590 | def add_journeys(self, plot): | |
591 | # requires flat plot | |
592 | children = [] | |
593 | for child in plot: | |
594 | if children: | |
595 | last_scene = children[-1] | |
596 | if not isinstance(last_scene, Journey) and last_scene.setting != child.setting: | |
597 | travellers = last_scene.exeunt | |
598 | if not travellers: | |
599 | # ideally, we should always compute exeunt in the | |
600 | # plot node, but for now we try to do it here | |
601 | travellers = [] | |
602 | if isinstance(last_scene.bystanders, Group): | |
603 | for b in last_scene.bystanders: | |
604 | travellers.append(b) | |
605 | elif last_scene.bystanders: | |
606 | raise AssertionError | |
607 | if isinstance(last_scene.subject, Group): | |
608 | for s in last_scene.subject: | |
609 | travellers.append(s) | |
610 | elif last_scene.subject: | |
611 | travellers.append(last_scene.subject) | |
612 | children.append(Journey( | |
613 | subject=Group(*travellers), | |
614 | setting=children[-1].setting, | |
615 | object=child.setting, | |
616 | )) | |
617 | children.append(child) | |
618 | ||
619 | return plot.__class__(*children, **dict(plot.iteritems())) | |
620 | ||
621 | def encounter_new_settings(self, plot, context=None): | |
622 | # rerquires flat plot (for reasons which are not *very* good) | |
623 | if context is None: | |
624 | context = { | |
625 | 'setting': plot.setting, | |
626 | 'seen': set([self.home]), | |
627 | 'unavailable': set(), | |
628 | } | |
629 | ||
630 | if plot.disqualified: | |
631 | context['unavailable'].add(plot.disqualified) | |
632 | if plot.requalified: | |
633 | context['unavailable'].remove(plot.requalified) | |
634 | ||
635 | available = set(self.protagonists) - context['unavailable'] | |
636 | ||
637 | if available and plot.setting != context['setting'] and \ | |
638 | plot.setting not in context['seen']: | |
639 | context['setting'] = plot.setting | |
640 | context['seen'].add(plot.setting) | |
641 | plot = PlotSequence( | |
642 | EncounterNewSetting( | |
643 | subject=Group(*list(available)), | |
644 | setting=plot.setting, | |
645 | ), | |
646 | plot, | |
647 | ) | |
648 | return plot | |
649 | ||
650 | children = [self.encounter_new_settings(c, context=context) for c in plot] | |
651 | return plot.__class__(*children, **dict(plot.iteritems())) | |
652 | ||
653 | def find_chekovs_guns(self, plot, gun_holders): | |
654 | if isinstance(plot, LoseItem): | |
655 | if plot.object not in gun_holders: | |
656 | gun_holders.setdefault(plot.object, set()).add(plot.subject) | |
657 | ||
658 | for child in plot: | |
659 | self.find_chekovs_guns(child, gun_holders) | |
660 | ||
661 | def add_chekovs_gun(self, plot, holder, chekovs_gun): | |
662 | if isinstance(plot, Introduction): | |
663 | others = set(plot.subject) | |
664 | others.remove(holder) | |
665 | return PlotSequence( | |
666 | plot, | |
667 | ChekovsGun( | |
668 | subject=holder, | |
669 | object=chekovs_gun, | |
670 | bystanders=Group(*list(others)), | |
671 | setting=plot.setting | |
672 | ) | |
673 | ) | |
674 | elif not self.added_chekovs_fun and isinstance(plot, Convalescence): | |
675 | self.added_chekovs_fun = True | |
676 | others = set(plot.subject) | |
677 | others.remove(holder) | |
678 | return PlotSequence( | |
679 | plot, | |
680 | ChekovsFun( | |
681 | subject=holder, | |
682 | object=chekovs_gun, | |
683 | bystanders=Group(*list(others)), | |
684 | setting=plot.setting | |
685 | ) | |
686 | ) | |
687 | else: | |
688 | children = [self.add_chekovs_gun(c, holder, chekovs_gun) for c in plot] | |
689 | return plot.__class__(*children, **dict(plot.iteritems())) | |
690 | ||
691 | # - - - - | |
692 | ||
693 | def write_plot(self, depth=5): | |
694 | home = self.home | |
695 | plot = PlotSequence( | |
696 | Introduction( | |
697 | setting=home, | |
698 | subject=Group(*self.protagonists), | |
699 | ), | |
700 | PlotHole( | |
701 | setting=home, | |
702 | rules=( | |
703 | ContemplateRockRule, | |
704 | LostItemRule, | |
705 | AwkwardTensionRule, | |
706 | KidnappingRule, | |
707 | DroneRule, | |
708 | ), | |
709 | ), | |
710 | Convalescence( | |
711 | setting=home, | |
712 | subject=Group(*self.protagonists), | |
713 | ), | |
714 | AllLaugh( | |
715 | setting=home, | |
716 | subject=Group(*self.protagonists), | |
717 | ) | |
718 | ) | |
719 | ||
720 | # we flatten the plot frequently to help stay under max recursion depth | |
721 | for i in xrange(0, depth): | |
722 | plot = self.complicate_plot(plot) | |
723 | plot = plot.flatten() | |
724 | plot = self.remove_plot_holes(plot) | |
725 | plot = plot.flatten() | |
726 | plot = self.remove_repetitive_plots(plot) | |
727 | ||
728 | self.commuted_plots = [] | |
729 | plot = plot.flatten() | |
730 | plot = self.commute_commutable_plots(plot) | |
731 | ||
732 | plot = plot.flatten() | |
733 | plot = self.encounter_new_settings(plot) | |
734 | plot = plot.flatten() | |
735 | plot = self.add_journeys(plot) | |
736 | ||
737 | chekovs_guns = {} | |
738 | self.find_chekovs_guns(plot, chekovs_guns) | |
739 | ||
740 | self.added_chekovs_fun = False | |
741 | for chekovs_gun, holders in chekovs_guns.iteritems(): | |
742 | # TODO interesting variation: could we have BOTH of them as holders? | |
743 | holder = random.choice(holders) | |
744 | plot = self.add_chekovs_gun(plot, holder, chekovs_gun) | |
745 | plot = plot.flatten() | |
746 | ||
747 | return plot | |
748 | ||
749 | def plot_to_story(self, plot): | |
750 | plot = plot.flatten() | |
751 | scenes = [] | |
752 | for plot_point in plot: | |
753 | events = [e for e in plot_point.create_events() if e is not None] | |
754 | scenes.append(Scene(EventSequence(*events), setting=plot_point.setting)) | |
755 | ||
756 | return Story(*scenes) | |
757 | ||
758 | def count_plot_classes(self, plot, cls): | |
759 | if isinstance(plot, cls): | |
760 | return 1 | |
761 | return sum([self.plot_contains_class(c, cls) for c in plot]) | |
762 | ||
763 | def plot_contains_class(self, plot, cls): | |
764 | if isinstance(plot, cls): | |
765 | return True | |
766 | return any([self.plot_contains_class(c, cls) for c in plot]) | |
767 | ||
768 | def generate_acceptable_plot(self, plot_min=(), plot_max=(), | |
769 | plot_depth=5, **kwargs): | |
770 | acceptable_plot = False | |
771 | while not acceptable_plot: | |
772 | plot = self.write_plot(depth=plot_depth) | |
773 | acceptable_plot = True | |
774 | for (cls, min) in plot_min: | |
775 | if self.count_plot_classes(plot, cls) < min: | |
776 | acceptable_plot = False | |
777 | #log('unacceptable, violates', constraint) | |
778 | break | |
779 | for (cls, max) in plot_max: | |
780 | if self.count_plot_classes(plot, cls) > max: | |
781 | acceptable_plot = False | |
782 | #log('unacceptable, violates', constraint) | |
783 | break | |
784 | #log('commuted plots:', self.commuted_plots) | |
785 | return plot |
0 | """Final transformation passes that work solely on text.""" | |
1 | ||
2 | import re | |
3 | ||
4 | from marysue.objects import Object | |
5 | from marysue.state import State | |
6 | ||
7 | ||
8 | def proofread(text): | |
9 | text = text.replace("`", "'") | |
10 | ||
11 | dingus = State(Object(names=('dingus',))) | |
12 | verbs = [] | |
13 | for k, v in dingus.saids.iteritems(): | |
14 | verbs.extend(list(v)) | |
15 | for k, v in dingus.shouteds.iteritems(): | |
16 | verbs.extend(list(v)) | |
17 | ||
18 | for verb in verbs: | |
19 | text = text.replace(verb + ' he', 'he ' + verb) | |
20 | text = text.replace(verb + ' she', 'she ' + verb) | |
21 | ||
22 | return text | |
23 | ||
24 | ||
25 | def word_count(text): | |
26 | return len([ | |
27 | z for z in re.split(r'\s', text) if z not in ('', '-', '#', '##', '###') | |
28 | ]) |
0 | """High-level orchestration of the writing of the novel.""" | |
1 | ||
2 | import os | |
3 | import sys | |
4 | from tempfile import mkstemp | |
5 | ||
6 | import marysue.util as random | |
7 | from marysue.util import capitalize, log | |
8 | from marysue.editor import edit_story | |
9 | from marysue.proofreader import proofread, word_count | |
10 | ||
11 | ||
12 | class Novel(object): | |
13 | front_matter = """\ | |
14 | # A Time for Destiny | |
15 | ||
16 | ### The Illustrious Career of Serenity Starlight Warhammer O'James during her First Three Years in the Space Fighters | |
17 | ||
18 | _SPACE SURGEON GENERALS WARNING:_ THE SPACE SURGEON GENERAL HAS DETERMINED THAT | |
19 | EXPOSURE TO LARGE AMOUNTS OF COMPUTER-GENERATED FICTION MAY CAUSE HEADACHES, | |
20 | DIZZINESS, NAUSEA, AND AN ACUTE DESIRE TO SKIP AROUND TO FIND THE GOOD BITS. | |
21 | ||
22 | FOR YOUR OWN WELL-BEING, DO NOT EXCEED THE RECOMMENDED MAXIMUM INTAKE OF 2 (TWO) | |
23 | CHAPTERS IN ANY 48 (FORTY EIGHT) HOUR PERIOD. | |
24 | ||
25 | """ | |
26 | ||
27 | def __init__(self, chapters, generate_front_matter=True, synopsis=False, dump=False): | |
28 | self.chapters = chapters | |
29 | self.generate_front_matter = generate_front_matter | |
30 | self.synopsis = synopsis | |
31 | self.dump = dump | |
32 | ||
33 | self.text = '' | |
34 | self.used_titles = set() | |
35 | self.introduced = set() | |
36 | ||
37 | def suggest_title_objects(self, plot, objects=None): | |
38 | from marysue.plot import Kidnapping, LoseItem, Vanquished | |
39 | ||
40 | if objects is None: | |
41 | objects = set() | |
42 | if isinstance(plot, Kidnapping): | |
43 | objects.add(plot.subject) | |
44 | if isinstance(plot, (LoseItem, Vanquished)): | |
45 | objects.add(plot.object) | |
46 | for child in plot: | |
47 | self.suggest_title_objects(child, objects=objects) | |
48 | return objects | |
49 | ||
50 | def pick_title(self, plot): | |
51 | titles = [o.definite for o in self.suggest_title_objects(plot)] | |
52 | if not titles: | |
53 | titles = ['The Destiny of Fate'] | |
54 | base_title = capitalize(random.choice(tuple(titles))) | |
55 | prefixes = [ | |
56 | 'The Return of ', | |
57 | 'The Revenge of ', | |
58 | 'The Scourge of ', | |
59 | 'The Menace of ', | |
60 | 'The Secret of ', | |
61 | 'The Time of ', | |
62 | 'The Scourge of ', | |
63 | 'The Mystery of ', | |
64 | 'The Phantom of ', | |
65 | ] | |
66 | multipref = [] | |
67 | for a in prefixes: | |
68 | for b in prefixes: | |
69 | multipref.append(a + b) | |
70 | prefixes.extend(multipref) | |
71 | ||
72 | title = base_title | |
73 | while title in self.used_titles: | |
74 | title = prefixes.pop(0) + base_title | |
75 | self.used_titles.add(title) | |
76 | ||
77 | return title | |
78 | ||
79 | def generate_chapter(self, n, plotter, **kwargs): | |
80 | ### tell the plotter to give us an acceptable plot ### | |
81 | ||
82 | plot = plotter.generate_acceptable_plot(**kwargs) | |
83 | ||
84 | ### dump the synopsis, if requested ### | |
85 | ||
86 | if self.synopsis: | |
87 | plot.print_synopsis() | |
88 | sys.exit(0) | |
89 | ||
90 | ### write a story around the plot, then edit it ### | |
91 | ||
92 | story = plotter.plot_to_story(plot) | |
93 | ||
94 | story = edit_story(story, self.introduced, **kwargs) | |
95 | ||
96 | ### dump the story, if requested ### | |
97 | ||
98 | if self.dump: | |
99 | story.dump(sys.stdout) | |
100 | sys.exit(0) | |
101 | ||
102 | ### give the thing a title that hasn't been used yet ### | |
103 | ||
104 | self.chapters[n]['commuted_plots'] = plotter.commuted_plots | |
105 | self.chapters[n]['plot'] = plot | |
106 | self.chapters[n]['title'] = self.pick_title(plot) | |
107 | ||
108 | ### install it in the chapters ### | |
109 | ||
110 | text = story.render() | |
111 | text = proofread(text) | |
112 | ||
113 | self.chapters[n]['text'] = text | |
114 | self.chapters[n]['word_count'] = word_count(text) | |
115 | ||
116 | def assemble_novel_text(self): | |
117 | self.text = '' | |
118 | ||
119 | # [WHARRGARBL](http://i1.kym-cdn.com/photos/images/newsfeed/000/032/388/wharrgarbl.jpg). | |
120 | ||
121 | if self.generate_front_matter: | |
122 | self.text += self.front_matter | |
123 | self.text += '\n\n## Contents\n\n' | |
124 | self.text += '<a name="contents"></a>\n\n' | |
125 | for n, chapter in enumerate(self.chapters): | |
126 | self.text += '1. [%s](#%s) \n' % (chapter['title'], n) | |
127 | self.text += '\n\n' | |
128 | ||
129 | for n, chapter in enumerate(self.chapters): | |
130 | self.text += ( | |
131 | ('<a name="%s"></a>\n\n' % n) + | |
132 | ('## %s. %s' % (n+1, chapter['title'])) + | |
133 | #('(%s)' % chapter['commuted_plots']) + | |
134 | '\n\n' + | |
135 | chapter['text'] + | |
136 | '\n\n' | |
137 | ) | |
138 | #self.text += '[Up to Table of Contents](#contents)\n\n' | |
139 | ||
140 | def trim(self): | |
141 | ### trim the novel to reasonable length ### | |
142 | ||
143 | # Note: this isn't perfect, because every time we cut a chapter, | |
144 | # we make the table of contents shorter too. But, it's usually OK. | |
145 | # The total number of words outside of any chapter is usually around 500. | |
146 | ||
147 | done = False | |
148 | while not done: | |
149 | self.assemble_novel_text() | |
150 | self.novel_wc = word_count(self.text) | |
151 | overrun = self.novel_wc - 50000 | |
152 | could_be_cut = [(n, c) for (n, c) in enumerate(self.chapters) if c['word_count'] < overrun and c['position'] == 'middle'] | |
153 | could_be_cut.sort(key=lambda pair: pair[1]['word_count']) | |
154 | if could_be_cut: | |
155 | n, chapter = could_be_cut[0] | |
156 | log('cutting:', n, chapter['title'], chapter['word_count']) | |
157 | self.chapters.remove(chapter) | |
158 | else: | |
159 | done = True | |
160 | ||
161 | self.total_wc = sum([chapter['word_count'] for chapter in self.chapters]) | |
162 | self.avg_wc = (self.total_wc * 1.0) / (len(self.chapters) * 1.0) | |
163 | ||
164 | self.retitle_chapters() | |
165 | self.assemble_novel_text() | |
166 | self.novel_wc = word_count(self.text) | |
167 | ||
168 | for n, c in enumerate(self.chapters): | |
169 | if c['commuted_plots']: | |
170 | log(n+1, c['commuted_plots']) | |
171 | ||
172 | def retitle_chapters(self): | |
173 | self.used_titles = set() | |
174 | for n, c in enumerate(self.chapters): | |
175 | c['title'] = self.pick_title(c['plot']) | |
176 | ||
177 | def publish(self): | |
178 | fd, temp_filename = mkstemp(suffix='.md') | |
179 | os.close(fd) | |
180 | with open(temp_filename, 'w') as f: | |
181 | f.write(self.text) | |
182 | f.write("## word counts for this novel\n\n") | |
183 | for n, chapter in enumerate(self.chapters): | |
184 | f.write("Chapter %02d: %s \n" % (n + 1, chapter['word_count'])) | |
185 | f.write("Chapter total: %0.2d \n" % self.total_wc) | |
186 | f.write("Average: %0.2d \n" % self.avg_wc) | |
187 | f.write("Entire Novel: %s \n" % self.novel_wc) | |
188 | log(temp_filename) | |
189 | html_filename = 'temp.html' | |
190 | output_filename = 'Serenity_Starlight.html' | |
191 | os.system("pandoc --from=markdown --to=html5 --css=marysue.css <%s >%s" % (temp_filename, html_filename)) | |
192 | with open(html_filename, 'r') as f_in: | |
193 | with open(output_filename, 'w') as f_out: | |
194 | for line in f_in: | |
195 | if 'rel="stylesheet"' in line: | |
196 | f_out.write('<style type="text/css">\n') | |
197 | f_out.write(open('marysue.css', 'r').read()) | |
198 | f_out.write('</style>\n') | |
199 | else: | |
200 | f_out.write(line) | |
201 | os.system("firefox %s &" % output_filename) |
0 | """Settings for the story. | |
1 | ||
2 | """ | |
3 | ||
4 | import marysue.util as random | |
5 | from marysue.objects import Object | |
6 | ||
7 | ||
8 | class Setting(Object): | |
9 | def __init__(self, names, roof, nearby, indoors=True, | |
10 | outside_setting=None, # if given, this is another setting | |
11 | preposition='on', has_drones=False, | |
12 | light='light'): | |
13 | self.names = tuple(names) | |
14 | self._nearby = nearby | |
15 | self.roof = roof | |
16 | self.preposition = preposition | |
17 | self.has_drones = has_drones | |
18 | self.indoors = indoors | |
19 | self.outside_setting = outside_setting | |
20 | self.light = light | |
21 | assert all([isinstance(o, Object) for o in self._nearby]) | |
22 | ||
23 | @property | |
24 | def nearby(self): | |
25 | return random.choice(self._nearby) | |
26 | ||
27 | @property | |
28 | def nearby_takeable(self): | |
29 | return random.choice(tuple(x for x in self._nearby if x.takeable)) | |
30 | ||
31 | @property | |
32 | def nearby_scenery(self): | |
33 | return random.choice(tuple(x for x in self._nearby if not x.takeable)) |
0 | import marysue.util as random | |
1 | from marysue.ast import Properties | |
2 | from marysue.objects import Object | |
3 | from marysue.duties import Duty | |
4 | ||
5 | ||
6 | class State(Properties): | |
7 | """State of a character (or other object) at a given point in the story. | |
8 | ||
9 | Each State references an Object, which contains the properties of | |
10 | that story-object that do not change over the course of the story. | |
11 | ||
12 | AST nodes should generally reference a State instead of an Object. | |
13 | We have a pass which replaces Objects in ASTs with the State of that | |
14 | Object at that particular time (depth-first, i.e. leftmost innermost | |
15 | traversal, mapping to time.) Further passes read and update that | |
16 | State on each instantaneous appearance on each Object. | |
17 | ||
18 | """ | |
19 | slots = ('object', | |
20 | 'is_referent', | |
21 | 'first_occurrence', | |
22 | 'torso_costume', | |
23 | 'legs_costume', | |
24 | 'feet_costume', | |
25 | 'hands_costume', | |
26 | 'head_costume', | |
27 | 'duties', | |
28 | 'mood', | |
29 | 'location',) | |
30 | ||
31 | def __init__(self, object, **kwargs): | |
32 | assert isinstance(object, Object), repr(object) | |
33 | kwargs['object'] = object | |
34 | super(State, self).__init__(**kwargs) | |
35 | ||
36 | def __getattr__(self, name): | |
37 | if name in self.slots: | |
38 | return self._attrs[name] | |
39 | if not hasattr(self.object, name) and name not in dir(self.object): | |
40 | # FIXME should be AttributeError but something catches that | |
41 | raise KeyError(name) | |
42 | return getattr(self.object, name) | |
43 | ||
44 | # Not sure how I feel about these three methods, but, OK | |
45 | # (Because State just delegates to Object, and Object doesn't know to do this from inside itself) | |
46 | ||
47 | @property | |
48 | def definite(self): | |
49 | article = self.definite_article | |
50 | fnite = self._fnite() | |
51 | return article + fnite | |
52 | ||
53 | @property | |
54 | def indefinite(self): | |
55 | article = self.indefinite_article | |
56 | fnite = self._fnite() | |
57 | if article == 'a 'and fnite[0].upper() in ('A', 'E', 'I', 'O', 'U'): | |
58 | article = 'an ' | |
59 | return article + fnite | |
60 | ||
61 | def _fnite(self): | |
62 | # helper function | |
63 | if not self.first_occurrence: | |
64 | if len(self.object.names) == 1: | |
65 | return self.object.name | |
66 | else: | |
67 | r = random.choice(self.object.names[1:]) | |
68 | return r.replace('{rank}', str(getattr(self.object, 'rank', ''))) | |
69 | else: | |
70 | return self.object.name | |
71 | ||
72 | @property | |
73 | def pronoun(self): | |
74 | if self.is_referent: | |
75 | return self.object.pronoun | |
76 | else: | |
77 | return self.definite | |
78 | ||
79 | @property | |
80 | def accusative(self): | |
81 | if self.is_referent: | |
82 | return self.object.accusative | |
83 | else: | |
84 | return self.definite | |
85 | ||
86 | @property | |
87 | def possessive(self): | |
88 | if self.is_referent: | |
89 | return self.object.possessive_pronoun | |
90 | else: | |
91 | return self.object.possessive | |
92 | ||
93 | @property | |
94 | def adverb(self): | |
95 | assert self.mood, 'mood not assigned to %r' % self | |
96 | adverbs = { | |
97 | 'happy': ( | |
98 | 'courageously', 'obsequiously', 'fawningly', 'blissfully', | |
99 | 'virtuously', 'happily', 'lightly', 'energetically', | |
100 | 'laughingly', 'placidly', 'peacefully', 'calmly', | |
101 | 'enigmatically', | |
102 | 'strongly', 'firmly', | |
103 | ), | |
104 | 'sad': ( | |
105 | 'drily', 'heavily', 'irritatedly', 'exasperatedly', | |
106 | 'slowly', 'drably', 'unhappily', 'sadly', 'irksomely', | |
107 | 'weakly', 'downheartedly', 'grumpily', | |
108 | 'enigmatically', | |
109 | ), | |
110 | 'angry': ( | |
111 | 'viciously', 'menacingly', 'drily', 'gratingly', 'heavily', | |
112 | 'threateningly', 'firmly', 'impatiently', | |
113 | 'icily', 'acidly', 'sternly', | |
114 | 'emphatically', 'annoyingly', 'irritatedly', 'exasperatedly', | |
115 | 'outrageously', 'strongly', | |
116 | 'enigmatically', | |
117 | # 'cunningly', 'slyly', | |
118 | ), | |
119 | 'embarrassed': ( | |
120 | 'awkwardly', 'slumblingly', 'coyly', 'slowly', | |
121 | 'carefully', 'painfully', 'painingly', | |
122 | 'enigmatically', | |
123 | # 'cunningly', 'slyly', | |
124 | ), | |
125 | } | |
126 | return random.choice(adverbs[self.mood]) | |
127 | ||
128 | @property | |
129 | def saids(self): | |
130 | return { | |
131 | 'happy': ( | |
132 | 'said', 'stated', 'chirped', 'smiled', | |
133 | 'sighed', 'breathed', 'whispered', 'giggled', | |
134 | 'drawled', | |
135 | ), | |
136 | 'sad': ( | |
137 | 'said', 'groaned', 'muttered', 'intoned', 'droned', | |
138 | 'gasped', 'gulped', 'mumbled', 'bleated', | |
139 | ), | |
140 | 'angry': ( | |
141 | 'said', 'groaned', 'muttered', | |
142 | 'intoned', 'barked', 'splurted', | |
143 | 'bellowed', 'shouted', 'yelled', 'raged', | |
144 | ), | |
145 | 'embarrassed': ( | |
146 | 'said', 'groaned', 'muttered', | |
147 | 'whispered', 'mumbled', | |
148 | 'gasped', 'gulped', 'sighed', 'breathed', | |
149 | ), | |
150 | } | |
151 | ||
152 | @property | |
153 | def said(self): | |
154 | assert self.mood, 'mood not assigned to %r' % self | |
155 | return random.choice(self.saids[self.mood]) | |
156 | ||
157 | @property | |
158 | def shouteds(self): | |
159 | return { | |
160 | 'happy': ( | |
161 | 'exclaimed', 'yelled', 'squealed', 'yelped', 'shouted', | |
162 | 'cried', | |
163 | ), | |
164 | 'sad': ( | |
165 | 'exclaimed', 'yelped', 'shouted', | |
166 | 'gasped', 'gulped', 'screeched', 'cried', | |
167 | ), | |
168 | 'angry': ( | |
169 | 'yelled', 'screeched', 'shouted', | |
170 | 'screamed', 'bellowed', 'barked', | |
171 | ), | |
172 | 'embarrassed': ( | |
173 | 'yelled', 'yelped', 'screamed', 'shouted', | |
174 | 'gasped', 'gulped', 'screeched', 'cried', | |
175 | ), | |
176 | } | |
177 | ||
178 | @property | |
179 | def shouted(self): | |
180 | assert self.mood, 'mood not assigned to %r' % self | |
181 | return random.choice(self.shouteds[self.mood]) | |
182 | ||
183 | @property | |
184 | def emoted(self): | |
185 | assert self.mood, 'mood not assigned to %r' % self | |
186 | emoteds = { | |
187 | 'happy': ( | |
188 | 'smiled', 'whistled', 'grinned', 'beamed', 'winked', | |
189 | ), | |
190 | 'sad': ( | |
191 | 'frowned', 'gasped', 'blinked', 'pouted', | |
192 | ), | |
193 | 'angry': ( | |
194 | 'frowned', 'grimaced', 'blinked', | |
195 | ), | |
196 | 'embarrassed': ( | |
197 | 'blushed', 'frowned', 'looked away', | |
198 | ), | |
199 | } | |
200 | return random.choice(emoteds[self.mood]) | |
201 | ||
202 | @property | |
203 | def pick_duty(self): | |
204 | if self.duties: | |
205 | return random.choice(self.duties) | |
206 | else: | |
207 | return Duty(names=('uphold the code of the Star Fighters',)) |
0 | from marysue.ast import AST | |
1 | from marysue.util import capitalize | |
2 | ||
3 | ||
4 | class Story(AST): | |
5 | def render(self): | |
6 | return ( | |
7 | '\n\n- - - -\n\n'.join([child.render() for child in self]) | |
8 | ) | |
9 | ||
10 | ||
11 | class Scene(AST): | |
12 | slots = ('setting',) | |
13 | ||
14 | def render(self): | |
15 | return '\n\n'.join([child.render() for child in self]) | |
16 | ||
17 | ||
18 | class EventSequence(AST): | |
19 | def render(self): | |
20 | return '\n\n'.join([capitalize(child.render()) for child in self]) | |
21 | ||
22 | def flatten(self): | |
23 | return EventSequence(*list(self.all_children()), **dict(self.iteritems())) | |
24 | ||
25 | def all_children(self): | |
26 | for child in self: | |
27 | if isinstance(child, EventSequence): | |
28 | for descendant in child.all_children(): | |
29 | yield descendant | |
30 | else: | |
31 | yield child | |
32 | ||
33 | ||
34 | # - - - - | |
35 | ||
36 | ||
37 | class Paragraph(AST): | |
38 | """Only used at the end""" | |
39 | def render(self): | |
40 | from marysue.events import Event | |
41 | ||
42 | s = '' | |
43 | for child in self: | |
44 | s += capitalize(child.render()) | |
45 | if isinstance(child, Event) and child.exciting: | |
46 | s += '! ' | |
47 | else: | |
48 | s += '. ' | |
49 | return s.rstrip() | |
50 | ||
51 | def flatten(self): | |
52 | return Paragraph(*list(self.all_children()), **dict(self.iteritems())) | |
53 | ||
54 | def all_children(self): | |
55 | for child in self: | |
56 | if isinstance(child, Paragraph): | |
57 | for descendant in child.all_children(): | |
58 | yield descendant | |
59 | else: | |
60 | yield child |
0 | # encoding: UTF-8 | |
1 | ||
2 | import string | |
3 | import sys | |
4 | import random | |
5 | ||
6 | ||
7 | # ...stolen from seedbank https://github.com/catseye/seedbank/ ... | |
8 | def autoseed(): | |
9 | from datetime import datetime | |
10 | import os | |
11 | import sys | |
12 | ||
13 | base_filename = 'seedbank.log' | |
14 | filename = os.path.join(os.getenv('HOME'), base_filename) | |
15 | if not os.path.exists(filename): | |
16 | filename = base_filename | |
17 | ||
18 | seed_ = os.getenv('SEEDBANK_SEED', None) | |
19 | if seed_ == 'LAST': | |
20 | with open(filename, 'r') as f: | |
21 | for line in f: | |
22 | pass | |
23 | seed_ = int(line.split(':')[-1].strip()) | |
24 | try: | |
25 | seed_ = int(seed_) | |
26 | except TypeError: | |
27 | seed_ = None | |
28 | if seed_ is None: | |
29 | seed_ = random.randint(0, 1000000) | |
30 | ||
31 | with open(filename, 'a') as f: | |
32 | f.write('%s: %s: %s\n' % (sys.argv[0], datetime.now(), seed_)) | |
33 | random.seed(seed_) | |
34 | return seed_ | |
35 | ||
36 | autoseed() | |
37 | ||
38 | ||
39 | # - - - - | |
40 | ||
41 | ||
42 | def chance(percent, obj=True): | |
43 | return obj if random.randint(1, 100) <= percent else None | |
44 | ||
45 | ||
46 | def lowercase(): | |
47 | return random.choice(string.lowercase) | |
48 | ||
49 | ||
50 | def extract(v, filter=lambda x: True): | |
51 | """Given a `set` of values `v`, randomly select a value from `v`, | |
52 | remove it from `v` (changing `v` as a side-effect), and return it. | |
53 | Only values for which the `filter` is true are considered. | |
54 | If `v` is empty or no values in `v` qualify for the filter, `v` | |
55 | is not modified and the function returns None.""" | |
56 | ||
57 | z = [x for x in v if filter(x)] | |
58 | if not z: | |
59 | return None | |
60 | p = random.choice(z) | |
61 | v.remove(p) | |
62 | return p | |
63 | ||
64 | ||
65 | class ShuffleDemon(object): | |
66 | """ | |
67 | https://www.youtube.com/watch?v=KZnLjRi_g9o | |
68 | https://www.youtube.com/watch?v=q6_sLmuze98 | |
69 | ||
70 | """ | |
71 | ||
72 | def __init__(self): | |
73 | self.registry = {} | |
74 | self.enabled = True | |
75 | ||
76 | def choice(self, tup): | |
77 | ||
78 | if not self.enabled: | |
79 | return random.choice(tup) | |
80 | ||
81 | if tup not in self.registry: | |
82 | #log(repr(tup)) | |
83 | self.registry[tup] = set() | |
84 | ||
85 | if not self.registry[tup]: | |
86 | self.registry[tup] = set(tup) | |
87 | ||
88 | return extract(self.registry[tup]) | |
89 | ||
90 | ||
91 | shuffle_demon = ShuffleDemon() | |
92 | ||
93 | ||
94 | def choice(v): | |
95 | if isinstance(v, set): | |
96 | v = tuple(v) | |
97 | return random.choice(v) | |
98 | ||
99 | ||
100 | def _choice(v): | |
101 | """This variant of Python's `random.choice` is enhanced to allow it to | |
102 | operate on `set`s, and to allow it to use the ShuffleDemon when | |
103 | it works on tuples.""" | |
104 | ||
105 | from marysue.objects import Object, Group # sigh!!! | |
106 | from marysue.ast import AST | |
107 | if isinstance(v, set): | |
108 | return random.choice(tuple(v)) | |
109 | if isinstance(v, Group): | |
110 | return random.choice(v) | |
111 | ||
112 | assert isinstance(v, tuple), repr(v) + ' ... ' + v.__class__.__name__ | |
113 | for i in v: | |
114 | assert isinstance( | |
115 | i, (basestring, Object, AST, tuple) | |
116 | ), i.__class__.__name__ | |
117 | assert sorted(v) == sorted(set(v)), repr(v) | |
118 | ||
119 | return shuffle_demon.choice(v) | |
120 | ||
121 | ||
122 | def randint(*args): | |
123 | return random.randint(*args) | |
124 | ||
125 | ||
126 | # - - - - non-randomness-related things | |
127 | ||
128 | ||
129 | def capitalize(s): | |
130 | i = 0 | |
131 | while i < len(s) and not s[i].isalpha(): | |
132 | i += 1 | |
133 | if i == 0: | |
134 | return s[0].upper() + s[1:] | |
135 | else: | |
136 | return s[:i] + s[i].upper() + s[i+1:] | |
137 | ||
138 | ||
139 | def log(*args): | |
140 | sys.stderr.write(' '.join([str(a) for a in args]) + '\n') |
0 | from marysue.objects import ( | |
1 | Object, Plural, | |
2 | ) | |
3 | from marysue.characters import ( | |
4 | MasculineCharacter, FeminineCharacter, | |
5 | MarySue, DreamBoat, Rival, TheOptimist, | |
6 | BadGuy, BadGal, | |
7 | ) | |
8 | from marysue.settings import Setting | |
9 | ||
10 | ||
11 | # - - - - settings - - - - | |
12 | ||
13 | ||
14 | bridge = Setting( | |
15 | names=( | |
16 | 'bridge of the Starship Dolphin', | |
17 | 'bridge' | |
18 | ), | |
19 | roof=Object(names=('roof of the bridge',)), | |
20 | nearby=( | |
21 | Object(names=("Captain's chair",)), | |
22 | Object(names=('navigation dashboard',)), | |
23 | Object(names=("communications panel",)), | |
24 | Object(names=("air conditioning unit",)), | |
25 | Object(names=("view screen",)), | |
26 | Plural(names=("pulsing red and green lights",)), | |
27 | Object(names=("electro wrench",), takeable=True), | |
28 | Object(names=("cup of coffee",), takeable=True), | |
29 | Plural(names=("star charts",), takeable=True), | |
30 | ), | |
31 | has_drones=True, | |
32 | light='fluorescent light', | |
33 | ) | |
34 | ||
35 | arch = Object(names=('crumbling arch',)) | |
36 | ||
37 | wasteland = Setting( | |
38 | names=( | |
39 | 'rugged wastes of the planet Forbak', | |
40 | 'wasteland', | |
41 | 'blasted surface of Forbak', | |
42 | ), | |
43 | roof=arch, | |
44 | nearby=( | |
45 | Object(names=('gravel pit',)), | |
46 | arch, | |
47 | Object(names=('foundation of a ruined building',)), | |
48 | Plural(names=('sickly shrubs',)), | |
49 | Object(names=('oddly shaped rock',), takeable=True), | |
50 | Object(names=('dead snake',), takeable=True), | |
51 | Object(names=('vulture egg',), takeable=True), | |
52 | ), | |
53 | indoors=False, | |
54 | light='harsh sun light', | |
55 | ) | |
56 | ||
57 | fortress = Setting( | |
58 | names=( | |
59 | "Nebulon's fortress stronghold", | |
60 | 'stronghold fortress of Nebulon', | |
61 | ), | |
62 | roof=Object(names=('roof of the passage',)), | |
63 | nearby=( | |
64 | Plural(names=('manacles',)), | |
65 | Object(names=('door to the dungeon',)), | |
66 | Object(names=('torch in a thing on the wall',)), | |
67 | Object(names=('puddle of icky looking liquid',)), | |
68 | Plural(names=('bottle caps',), takeable=True), | |
69 | Plural(names=('shards of broken glass',), takeable=True), | |
70 | Object(names=('skull of a Space Rat',), takeable=True), | |
71 | ), | |
72 | preposition='in', | |
73 | outside_setting=wasteland, | |
74 | light='dim torch light', | |
75 | ) | |
76 | ||
77 | ||
78 | settings = [bridge, wasteland, fortress] | |
79 | ||
80 | ||
81 | # - - - - characters - - - - | |
82 | ||
83 | ||
84 | serenity = MarySue( | |
85 | names=( | |
86 | "{rank} Serenity Starlight Warhammer O'James", | |
87 | "{rank} Serenity", | |
88 | "Serenity Starlight", | |
89 | "Serenity Starlight Warhammer O'James", | |
90 | "Ms. O'James", | |
91 | ), | |
92 | rank='Ensign', | |
93 | home=bridge, | |
94 | weapon=Object(names=( | |
95 | 'Venusian Katana of Power', | |
96 | )), | |
97 | ) | |
98 | ||
99 | joe = DreamBoat( | |
100 | names=( | |
101 | "{rank} Joe Mulbury", | |
102 | "Joe Mulbury", | |
103 | "{rank} Joe", | |
104 | ), | |
105 | home=bridge, | |
106 | rank='Commander', | |
107 | weapon=Object(names=('Jovian battle axe',)), | |
108 | ) | |
109 | ||
110 | dwight = TheOptimist( | |
111 | names=( | |
112 | "{rank} Dwight Edgmont", | |
113 | "Dwight Edgmont", | |
114 | "{rank} Dwight", | |
115 | ), | |
116 | home=bridge, | |
117 | rank='Captain', | |
118 | weapon = Object(names=('vibro sword',)), | |
119 | ) | |
120 | ||
121 | tammy = Rival( | |
122 | names=( | |
123 | "Navigator Tammy Smith", | |
124 | "Navigator Tammy", | |
125 | "Tammy Smith", | |
126 | ), | |
127 | home=bridge, | |
128 | rank='Lieutenant', | |
129 | weapon = Object(names=('electro mace',)), | |
130 | ) | |
131 | ||
132 | protagonists = [serenity, joe, dwight, tammy] | |
133 | ||
134 | nebulon = BadGuy( | |
135 | names=('Nebulon the Dastardly', 'Nebulon'), | |
136 | home=fortress | |
137 | ) | |
138 | ||
139 | skull_witch = BadGal( | |
140 | names=('The Skull Witch',), | |
141 | home=fortress | |
142 | ) | |
143 | ||
144 | antagonists = [nebulon, skull_witch] | |
145 | ||
146 | power_staff = Object(names=('Venusian Staff of Power',)) | |
147 | eternity_jewel = Object(names=('Star Jewel of Eternity',)) | |
148 | hope_orb = Object(names=('Saturnian Orb of Hope',)) | |
149 | dream_chalice = Object(names=('Chalice of Dreams',)) | |
150 | ||
151 | macguffins = [power_staff, eternity_jewel, hope_orb, dream_chalice] | |
152 | ||
153 | space_slugs = Plural( | |
154 | names=('Space Slugs',) | |
155 | ) | |
156 | space_badgers = Plural( | |
157 | names=('Space Badgers',) | |
158 | ) | |
159 | space_ferrets = Plural( | |
160 | names=('Space Ferrets',) | |
161 | ) | |
162 | space_snails = Plural( | |
163 | names=('Space Snails',) | |
164 | ) | |
165 | space_otters = Plural( | |
166 | names=('Space Otters',) | |
167 | ) | |
168 | space_weasels = Plural( | |
169 | names=('Space Weasels',) | |
170 | ) | |
171 | space_wallabies = Plural( | |
172 | names=('Space Wallabies',) | |
173 | ) | |
174 | space_ostriches = Plural( | |
175 | names=('Space Ostriches',) | |
176 | ) | |
177 | ||
178 | goons = [ | |
179 | space_slugs, space_badgers, space_ferrets, space_snails, | |
180 | space_otters, space_weasels, space_wallabies, space_ostriches, | |
181 | ] |