git @ Cat's Eye Technologies MARYSUE / 1b50ef2
Initial import of MARYSUE sources. Chris Pressey 9 years ago
21 changed file(s) with 4836 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 }
(New empty file)
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 print
110 print "!!! error in template '%s' on ast %r" % (template, self)
111 print
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 print
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')
(New empty file)
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 ]