git @ Cat's Eye Technologies The-Swallows / master src / swallows / engine / events.py
master

Tree @master (Download .tar.gz)

events.py @masterraw · history · blame

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
import random
import sys

# TODO

# Diction:
# - use indef art when they have no memory of an item that they see
# - dramatic irony would be really nice, but hard to pull off.  Well, a certain
#   amount happens naturally now, with character pov.  but more could be done
# - "Chapter 3.  _In which Bob hides the stolen jewels in the mailbox, etc_" --
#   i.e. chapter summaries -- that's a little too fancy to hope for, but with
#   a sufficiently smart Editor it could be done

### EVENTS ###

class Event(object):
    def __init__(self, phrase, participants, excl=False,
                 previous_location=None,
                 speaker=None,
                 addressed_to=None,
                 exciting=False):
        """participants[0] is always the initiator, and we
        record the location that the event was initiated in.

        For now, we assume such an event can be:
        - observed by every actor at that location
        - affects only actors at that location

        In the future, we *may* have:
        - active and passive participants
        - active participants all must be present at the location
        - passive participants need not be
        (probably done by passing a number n: the first n
        participants are to be considered active)

        speaker and addressed_to apply to dialogue.
        If speaker == None, it means the narrator is speaking.
        If addressed_to == None, it means the reader is being spoken to.

        """
        self.phrase = phrase
        self.participants = participants
        self.location = participants[0].location
        self._previous_location = previous_location
        self.excl = excl
        self.speaker = speaker
        self.addressed_to = addressed_to
        self.exciting = exciting

    def rephrase(self, new_phrase):
        """Does not modify the event.  Returns a new copy."""
        return Event(new_phrase, self.participants, excl=self.excl)

    def initiator(self):
        return self.participants[0]

    def previous_location(self):
        return self._previous_location

    def render(self):
        phrase = self.phrase
        i = 0
        for participant in self.participants:
            phrase = phrase.replace('<%d>' % (i + 1), participant.render(event=self))
            phrase = phrase.replace('<indef-%d>' % (i + 1), participant.indefinite())
            phrase = phrase.replace('<his-%d>' % (i + 1), participant.posessive())
            phrase = phrase.replace('<him-%d>' % (i + 1), participant.accusative())
            phrase = phrase.replace('<he-%d>' % (i + 1), participant.pronoun())
            phrase = phrase.replace('<was-%d>' % (i + 1), participant.was())
            phrase = phrase.replace('<is-%d>' % (i + 1), participant.is_())
            i = i + 1
        return phrase

    def __str__(self):
        phrase = self.render()
        if self.excl:
            phrase = phrase + '!'
        else:
            phrase = phrase + '.'
        return phrase[0].upper() + phrase[1:]


class AggregateEvent(Event):
    """Attempt at a way to combine multiple events into a single
    sentence.  Each constituent event must have the same initiator.

    This is definitely not as nice as it could be.

    """
    def __init__(self, template, events, excl=False):
        self.template = template
        self.events = events
        self.excl = excl
        self.phrase = 'SEE SUBEVENTS PLZ'
        self._initiator = self.events[0].initiator()
        for event in self.events:
            assert event.initiator() == self._initiator
        self.location = self._initiator.location

    def rephrase(self, new_phrase):
        #raise NotImplementedError
        return self

    def initiator(self):
        return self._initiator

    def previous_location(self):
        return self.events[0].previous_location()

    def __str__(self):
        phrase = self.template % tuple([x.render() for x in self.events])
        if self.excl:
            phrase = phrase + '!'
        else:
            phrase = phrase + '.'
        return phrase[0].upper() + phrase[1:]


class EventCollector(object):
    def __init__(self):
        self.events = []
    
    def collect(self, event):
        if self.events and str(event) == str(self.events[-1]):
            raise ValueError('Duplicate event: %s' % event)
        if event.phrase == '<1> went to <2>':
            assert event.previous_location() is not None
            assert event.previous_location() != event.location
        self.events.append(event)


# not really needed, as emit() does nothing if there is no collector
class Oblivion(EventCollector):
    def collect(self, event):
        pass


oblivion = Oblivion()


### EDITOR AND PUBLISHER ###

class Editor(object):
    """The Editor is remarkably similar to the _peephole optimizer_ in
    compiler construction.  Instead of replacing sequences of instructions
    with more efficient but semantically equivalent sequences of
    instructions, it replaces sequences of sentences with more readable
    but semantically equivalent sequences of sentences.

    The Editor is also responsible for chopping up the sequence of
    sentences into "sensible" paragraphs.  (This might be like a compiler
    code-rewriting pass that inserts NOPs to ensure instructions are on a
    word boundary, or some such.)
    
    The Editor is also responsible for picking which character to
    follow.  (I don't think there's a compiler construction analogy for
    that.)

    Note that the event stream must start with "<Character> was in <place>"
    as the first event for each character.  Otherwise the Editor don't know
    who started where.

    Well, OK, it *used* to look a lot like a peephole optimizer.  Soon, it
    will make multiple passes.  It still looks a lot like the optimization
    phase of a compiler, though.

    """
 
    def __init__(self, collector, main_characters):
        self.events = list(reversed(collector.events))
        self.main_characters = main_characters
        self.pov_index = 0
        self.transformers = []
        # maps main characters to where they currently are (omnisciently)
        self.character_location = {}
        # maps main characters to where the reader last saw them
        self.last_seen_at = {}
        # maps characters to things that happened to them while not narrated
        self.exciting_developments = {}

    def add_transformer(self, transformer):
        self.transformers.append(transformer)

    def publish(self):
        paragraph_num = 1
        while len(self.events) > 0:
            pov_actor = self.main_characters[self.pov_index]
            paragraph_events = self.generate_paragraph_events(pov_actor)
            for transformer in self.transformers:
                if paragraph_events:
                    paragraph_events = transformer.transform(
                        self, paragraph_events, paragraph_num
                    )
            self.publish_paragraph(paragraph_events)
            self.pov_index += 1
            if self.pov_index >= len(self.main_characters):
                self.pov_index = 0
            paragraph_num += 1

    def generate_paragraph_events(self, pov_actor):
        quota = random.randint(10, 25)
        paragraph_events = []
        while len(paragraph_events) < quota and len(self.events) > 0:
            event = self.events.pop()

            if not paragraph_events:
                # this is the first sentence of the paragraph
                # if the reader wasn't aware they were here, add an event
                if self.last_seen_at.get(pov_actor, None) != event.location:
                    if not (('went to' in event.phrase) or
                            ('made <his-1> way to' in event.phrase) or
                            (event.phrase == '<1> <was-1> in <2>')):
                        paragraph_events.append(Event('<1> <was-1> in <2>', [pov_actor, event.location]))
                # if something exciting happened, tell the reader
                for (obj, loc) in self.exciting_developments.get(pov_actor, []):
                    paragraph_events.append(Event('<1> had found <2> in <3>', [pov_actor, obj, loc]))
                self.exciting_developments[pov_actor] = []

            # update our idea of where the character is, even if these are
            # not events we will be dumping out
            self.character_location[event.initiator()] = event.location

            if event.location == self.character_location[pov_actor]:
                paragraph_events.append(event)
                # update the reader's idea of where the character is
                self.last_seen_at[event.initiator()] = event.location
            else:
                if event.exciting:
                   self.exciting_developments.setdefault(event.initiator(), []).append(
                       (event.participants[1], event.participants[2])
                   )

        return paragraph_events

    def publish_paragraph(self, paragraph_events):
        for event in paragraph_events:
            sys.stdout.write(str(event) + "  ")
            #sys.stdout.write("\n")
        print
        print


class Transformer(object):
    pass


class DeduplicateTransformer(Transformer):
    # check for verbatim repeated. this could be 'dangerous' if, say,
    # you have two characters, Bob Jones and Bob Smith, and both are
    # named 'Bob', and they are actually two different events... but...
    # for now that is an edge case.
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        for event in incoming_events:
            if events:
                if str(event) == str(events[-1]):
                    events[-1].phrase = event.phrase + ', twice'
                elif str(event.rephrase(event.phrase + ', twice')) == str(events[-1]):
                    events[-1].phrase = event.phrase + ', several times'
                elif str(event.rephrase(event.phrase + ', several times')) == str(events[-1]):
                    pass
                else:
                    events.append(event)
            else:
                events.append(event)
        return events


class UsePronounsTransformer(Transformer):
    # replace repeated proper nouns with pronouns
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        for event in incoming_events:
            if events:
                if event.initiator() == events[-1].initiator():
                    event.phrase = event.phrase.replace('<1>', '<he-1>')
                events.append(event)
            else:
                events.append(event)
        return events


class MadeTheirWayToTransformer(Transformer):
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        for event in incoming_events:
            if (events and
                event.initiator() == events[-1].initiator()):
                if (events[-1].phrase in ('<1> went to <2>',) and
                     event.phrase == '<1> went to <2>'):
                     assert event.location == event.participants[1]
                     assert events[-1].previous_location() is not None
                     assert events[-1].location == events[-1].participants[1]
                     events[-1].phrase = '<1> made <his-1> way to <2>'
                     events[-1].participants[1] = event.participants[1]
                     events[-1].location = event.participants[1]
                elif (events[-1].phrase in ('<1> made <his-1> way to <2>',) and
                     event.phrase == '<1> went to <2>'):
                     assert event.location == event.participants[1]
                     assert events[-1].previous_location() is not None
                     assert events[-1].location == events[-1].participants[1]
                     events[-1].phrase = '<1> made <his-1> way to <2>'
                     events[-1].participants[1] = event.participants[1]
                     events[-1].location = event.participants[1]
                else:
                    events.append(event)
            else:
                events.append(event)
        return events


# well well well
from swallows.engine.objects import Actor
weather = Actor('the weather')


class AddWeatherFrifferyTransformer(Transformer):
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        if paragraph_num == 1:
            choice = random.randint(0, 3)
            if choice == 0:
                events.append(Event("It was raining", [weather]))
            if choice == 1:
                events.append(Event("It was snowing", [weather]))
            if choice == 2:
                events.append(Event("The sun was shining", [weather]))
            if choice == 3:
                events.append(Event("The day was overcast and humid", [weather]))
        return events + incoming_events


class AddParagraphStartFrifferyTransformer(Transformer):
    def transform(self, editor, incoming_events, paragraph_num):
        first_event = incoming_events[0]
        if paragraph_num == 1:
            return incoming_events
        if str(first_event).startswith("'"):
            return incoming_events
        if " had found " in str(first_event):
            return incoming_events
        if " was in " in str(first_event):
            return incoming_events
        choice = random.randint(0, 8)
        if choice == 0:
            first_event = first_event.rephrase(
                "Later on, " + first_event.phrase
            )
        if choice == 1:
            first_event = first_event.rephrase(
                "Suddenly, " + first_event.phrase
            )
        if choice == 2:
            first_event = first_event.rephrase(
                "After a moment's consideration, " + first_event.phrase
            )
        if choice == 3:
            first_event = first_event.rephrase(
                "Feeling anxious, " + first_event.phrase
            )
        return [first_event] + incoming_events[1:]


class AggregateEventsTransformer(Transformer):
    # replace "Bob went to the kitchen.  Bob saw the toaster"
    # with "Bob went to the kitchen, where he saw the toaster"
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        for event in incoming_events:
            if events:
                if ( event.initiator() == events[-1].initiator() and
                     events[-1].phrase in ('<1> went to <2>',) and
                     event.phrase in ('<1> saw <2>',) ):
                    # this *might* be better if we only do it when <1>
                    # is the pov character for this paragraph.  but it
                    # does work...
                    event.phrase = event.phrase.replace('<1>', '<he-1>')
                    events[-1] = AggregateEvent(
                        "%s, where %s", [events[-1], event],
                        excl = event.excl)
                else:
                    events.append(event)
            else:
                events.append(event)
        return events


class DetectWanderingTransformer(Transformer):
    # not used yet
    # if they 'made their way' to their current location...
    def transform(self, editor, incoming_events, paragraph_num):
        events = []
        for event in incoming_events:
            if (event.phrase == '<1> made <his-1> way to <2>' and
                event.location == event.previous_location()):
                event.phrase = '<1> wandered around for a bit, then came back to <2>'
            events.append(event)
        return events


class Publisher(object):
    def __init__(self, characters=(), setting=(), friffery=False,
                 debug=False, title='Untitled', chapters=18,
                 events_per_chapter=810):
        self.characters = characters
        self.setting = setting
        self.friffery = friffery
        self.debug = debug
        self.title = title
        self.chapters = chapters
        self.events_per_chapter = events_per_chapter

    def publish_chapter(self, chapter_num):
        collector = EventCollector()
        
        for character in self.characters:
            character.collector = collector
            # don't continue a conversation from the previous chapter, please
            character.topic = None
            character.place_in(random.choice(self.setting))

        while len(collector.events) < self.events_per_chapter:
            for character in self.characters:
                character.live()
                #print len(collector.events) # , repr([str(e) for e in collector.events])

        if self.debug:
            for character in self.characters:
                print "%s'S EVENTS:" % character.name.upper()                
                for event in collector.events:
                    if event.participants[0] != character:
                        continue
                    print "%r in %s: %s" % (
                        [p.render(event=event) for p in event.participants],
                        event.location.render(),
                        event.phrase
                    )
                print
            for character in self.characters:
                print "%s'S STATE:" % character.name.upper()
                character.dump_beliefs()
                print
            print "- - - - -"
            print

        editor = Editor(collector, self.characters)
        editor.add_transformer(MadeTheirWayToTransformer())
        editor.add_transformer(DeduplicateTransformer())
        editor.add_transformer(AggregateEventsTransformer())
        editor.add_transformer(DetectWanderingTransformer())
        # this one should be last, so prior transformers don't
        # have to worry themselves about looking for pronouns
        editor.add_transformer(UsePronounsTransformer())
        # this should be a matter of configuring what transformers
        # to use, when you instantiate a Publisher
        if self.friffery:
            editor.add_transformer(AddWeatherFrifferyTransformer())
            editor.add_transformer(AddParagraphStartFrifferyTransformer())
        editor.publish()

    def publish(self):
        print self.title
        print "=" * len(self.title)
        print

        for chapter in range(1, self.chapters+1):
            print "Chapter %d." % chapter
            print "-----------"
            print

            self.publish_chapter(chapter)