├── src
└── swallows
│ ├── __init__.py
│ ├── engine
│ ├── __init__.py
│ ├── objects.py
│ └── events.py
│ └── story
│ ├── __init__.py
│ ├── world.py
│ └── characters.py
├── .hgtags
├── .gitignore
├── script
└── the_swallows.py
├── eg
├── not_the_swallows.py
└── the_swallows++.py
├── LICENSE
└── README.md
/src/swallows/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/swallows/engine/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/swallows/story/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.hgtags:
--------------------------------------------------------------------------------
1 | ea8fb05d8f6e0d3a3098370e3e8c3c54cffec9d5 1.0
2 | ea8fb05d8f6e0d3a3098370e3e8c3c54cffec9d5 1.0
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
--------------------------------------------------------------------------------
/script/the_swallows.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | #
4 | # the_swallows.py: a novel generator.
5 | # Chris Pressey, Cat's Eye Technologies
6 | #
7 |
8 | from os.path import realpath, dirname, join
9 | import sys
10 |
11 | # get the ../src/ directory onto the Python module search path
12 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
13 |
14 | # now we can import things, like:
15 | from swallows.engine.events import Publisher
16 | from swallows.story.world import alice, bob, house
17 |
18 | ### main ###
19 |
20 | publisher = Publisher(
21 | characters=(alice, bob),
22 | setting=house,
23 | title="Dial S for Swallows",
24 | friffery=True,
25 | #debug=True,
26 | #chapters=1,
27 | )
28 | publisher.publish()
29 |
--------------------------------------------------------------------------------
/eg/not_the_swallows.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | #
4 | # Example of using _The Swallows_ engine, but not its world,
5 | # to produce a different story.
6 | #
7 |
8 | from os.path import realpath, dirname, join
9 | import sys
10 |
11 | # get the ../src/ directory onto the Python module search path
12 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
13 |
14 | # now we can:
15 | from swallows.engine.events import Publisher
16 | from swallows.engine.objects import Location, ProperLocation, Male, Female
17 |
18 | ### world ###
19 |
20 | main_street = ProperLocation("Main Street", noun="street")
21 | butchers = Location("butcher's", noun="store")
22 | bakery = Location("bakery", noun="store")
23 | candlestick_factory = Location("candlestick factory", noun="building")
24 |
25 | main_street.set_exits(butchers, bakery, candlestick_factory)
26 | butchers.set_exits(main_street)
27 | bakery.set_exits(main_street)
28 | candlestick_factory.set_exits(main_street)
29 |
30 | downtown = (main_street, butchers, bakery, candlestick_factory)
31 |
32 | class Tweedle(Male):
33 | def live(self):
34 | self.wander()
35 |
36 | tweedledee = Tweedle('Tweedledee')
37 | tweedledum = Tweedle('Tweedledum')
38 |
39 | ### main ###
40 |
41 | publisher = Publisher(
42 | characters=(
43 | tweedledee,
44 | tweedledum,
45 | ),
46 | setting=downtown,
47 | title="TERRIBLE EXAMPLE STORY",
48 | #debug=True,
49 | )
50 | publisher.publish()
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This distribution is covered under the following licenses:
2 |
3 | All example files (in the `eg` directory) and all files for the engine
4 | (in the `src/swallows/engine` directory) are in the public domain,
5 | specifically, under the UNLICENSE, the text of which follows:
6 |
7 | -----------------------------------------------------------------------------
8 |
9 | This is free and unencumbered software released into the public domain.
10 |
11 | Anyone is free to copy, modify, publish, use, compile, sell, or
12 | distribute this software, either in source code form or as a compiled
13 | binary, for any purpose, commercial or non-commercial, and by any
14 | means.
15 |
16 | In jurisdictions that recognize copyright laws, the author or authors
17 | of this software dedicate any and all copyright interest in the
18 | software to the public domain. We make this dedication for the benefit
19 | of the public at large and to the detriment of our heirs and
20 | successors. We intend this dedication to be an overt act of
21 | relinquishment in perpetuity of all present and future rights to this
22 | software under copyright law.
23 |
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 |
32 | For more information, please refer to
33 |
34 | -----------------------------------------------------------------------------
35 |
36 | The generated novels (in the `doc` directory) are released under the
37 | Creative Commons Attribution 3.0 Unported license ("CC BY 3.0"):
38 |
39 | http://creativecommons.org/licenses/by/3.0/
40 |
41 | The Swallows world and characters (in the `src/swallows/story` directory)
42 | are released under an MIT-style license; see those files for the license
43 | text.
44 |
--------------------------------------------------------------------------------
/eg/the_swallows++.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | #
4 | # Example of extending _The Swallows_ world to produce a different story.
5 | #
6 |
7 | from os.path import realpath, dirname, join
8 | import sys
9 |
10 | # get the ../src/ directory onto the Python module search path
11 | sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
12 |
13 | # now we can import the classes we will work with
14 | from swallows.engine.events import Publisher
15 | from swallows.story.characters import MaleCharacter
16 | from swallows.story.world import (
17 | alice, bob, house, upstairs_hall,
18 | revolver, brandy, dead_body
19 | )
20 | from swallows.engine.objects import (
21 | ProperContainer, Item, Location
22 | )
23 |
24 | # we extend the world of The Swallows by adding a new character.
25 | # note that we have to inform the new character of certain important objects
26 | # in the world are, so that he can react sensibly to them.
27 | # (you *can* pass other objects here, for example 'revolver=brandy', in which
28 | # case the character will act fairly nonsensibly, threatening other characters
29 | # with the bottle of brandy and so forth)
30 | fred = MaleCharacter('Fred')
31 | fred.configure_objects(
32 | revolver=revolver,
33 | brandy=brandy,
34 | dead_body=dead_body,
35 | )
36 |
37 | # we extend the world by adding new locations and objects
38 | # note that locations exited-to and from must be imported from swallows.story.world (above)
39 | # "Location" is imported from swallows.engine.objects
40 | freds_office = ProperLocation("<*> office", owner=fred)
41 | freds_office.set_exits(upstairs_hall)
42 |
43 | upstairs_hall.set_exits(freds_office) # adds to existing (unknown) exits
44 |
45 | # we extend the world by adding some Objects
46 | # "ProperContainer" and "Item" are imported from swallows.engine.objects
47 | desk = ProperContainer("<*> desk", owner=fred, location=freds_office)
48 | pencils = Item('box of pencils', location=desk)
49 |
50 |
51 | ### main ###
52 |
53 | publisher = Publisher(
54 | characters=(
55 | alice,
56 | bob,
57 | fred,
58 | ),
59 | setting=house,
60 | title="My _The Swallows_ Fanfic",
61 | #debug=True,
62 | )
63 | publisher.publish()
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | The Swallows
2 | ============
3 |
4 | _See also:_ [NaNoGenLab](https://git.catseye.tc/NaNoGenLab/) (2014)
5 | ∘ [MARYSUE](https://git.catseye.tc/MARYSUE/) (2015)
6 | ∘ [2017 Entries](https://git.catseye.tc/NaNoGenMo-Entries-2017/)
7 | ∘ [2018 Entries](https://git.catseye.tc/NaNoGenMo-Entries-2018/)
8 | ∘ [2019 Entries](https://git.catseye.tc/NaNoGenMo-Entries-2019/)
9 |
10 | - - - -
11 |
12 | _The Swallows_ is a series of computer-generated novels meta-written for
13 | [NaNoGenMo 2013](https://github.com/dariusk/NaNoGenMo) by
14 | Cat's Eye Technologies. Our submission issue can be found
15 | [here](https://github.com/dariusk/NaNoGenMo/issues/39).
16 |
17 | _The Swallows_ is also the name of the first novel in the series.
18 | It follows the madcap adventures of Alice and Bob as they both try
19 | to acquire the golden falcon, which is priceless, or at the very least
20 | [irrationally desirable by all involved](http://tvtropes.org/pmwiki/pmwiki.php/Main/MacGuffin).
21 |
22 | _The Swallows of Summer_, the sequel to _The Swallows_, revisits
23 | Alice and Bob's life three years later. They have a much bigger house
24 | now. They also own more things.
25 |
26 | _Swallows and Sorrows_, the third book in the series, shows us another
27 | period in Alice and Bob's life, not much later. They have taken up drinking —
28 | at least when they are disturbed by things — to calm their nerves. And then
29 | they start arguing. Oh, and the narrator knows how to use pronouns now.
30 |
31 | The fourth novel, _Dial S for Swallows_, probably takes place shortly
32 | thereafter, or possibly at the same time as _Swallows and Sorrows_ but in an
33 | alternate universe. Alice and Bob are no longer psychic (although this may
34 | not have been apparent in the previous books, the fact is that they were
35 | easily able tell what the other was thinking. Now, they only have suspicions.
36 | Also, the previous editor was sacked, and a completely new editor installed
37 | in their place.)
38 |
39 | All four novels can be found, in Markdown format, in the `doc` subdirectory
40 | of this distribution.
41 |
42 | At 49K words (in 15 chapters of 33 paragraphs each,) _The Swallows_ is not
43 | quite long enough to qualify for NaNoGenMo. At 53K words,
44 | _The Swallows of Summer_ is. _Swallows and Sorrows_ is just barely over
45 | 50K words. _Dial S for Swallows_ is over 56K words.
46 |
47 | All novels were generated by the Python script `the_swallows.py` in the
48 | `script` directory, at different points in time. See the repository
49 | history to get the version of the script used for a particular novel.
50 |
51 | I invite the reader who is interested in how the script works to read the
52 | source code. It sometimes even contains comments.
53 |
--------------------------------------------------------------------------------
/src/swallows/story/world.py:
--------------------------------------------------------------------------------
1 | # Copyright (c)2013 Chris Pressey, Cat's Eye Technologies
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a
4 | # copy of this software and associated documentation files (the "Software"),
5 | # to deal in the Software without restriction, including without limitation
6 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | # and/or sell copies of the Software, and to permit persons to whom the
8 | # Software is furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | # DEALINGS IN THE SOFTWARE.
20 |
21 | import random
22 |
23 | from swallows.engine.objects import (
24 | Location, ProperLocation, Treasure, PluralTreasure,
25 | Container, ProperContainer,
26 | Item, Weapon, Horror
27 | )
28 | from swallows.story.characters import MaleCharacter, FemaleCharacter
29 |
30 | # TODO
31 |
32 | # World:
33 | # more reacting to the dead body:
34 | # - if they *agree*, take one of the courses of action
35 | # after agreement:
36 | # - calling the police (do they have a landline? it might be entertaining
37 | # if they share one mobile phone between the both of them)
38 | # - i'll have to introduce a new character... the detective. yow.
39 | # - trying to dispose of it... they try to drag it to... the garden?
40 | # i'll have to add a garden. and a shovel.
41 | # an unspeakable thing in the basement! (don't they have enough excitement
42 | # in their lives?)
43 | # bullets for the revolver
44 |
45 | ### world ###
46 |
47 | alice = FemaleCharacter('Alice')
48 | bob = MaleCharacter('Bob')
49 |
50 | kitchen = Location('kitchen')
51 | living_room = Location('living room')
52 | dining_room = Location('dining room')
53 | front_hall = Location('front hall')
54 | driveway = Location('driveway', noun="driveway")
55 | garage = Location('garage', noun="garage")
56 | path_by_the_shed = Location('path by the shed', noun="path")
57 | shed = Location('shed', noun="shed")
58 | upstairs_hall = Location('upstairs hall')
59 | study = Location('study')
60 | bathroom = Location('bathroom')
61 | bobs_bedroom = ProperLocation("<*> bedroom", owner=bob)
62 | alices_bedroom = ProperLocation("<*> bedroom", owner=alice)
63 |
64 | kitchen.set_exits(dining_room, front_hall)
65 | living_room.set_exits(dining_room, front_hall)
66 | dining_room.set_exits(living_room, kitchen)
67 | front_hall.set_exits(kitchen, living_room, driveway, upstairs_hall)
68 | driveway.set_exits(front_hall, garage, path_by_the_shed)
69 | garage.set_exits(driveway)
70 | path_by_the_shed.set_exits(driveway, shed)
71 | shed.set_exits(path_by_the_shed)
72 | upstairs_hall.set_exits(bobs_bedroom, alices_bedroom, front_hall, study, bathroom)
73 | bobs_bedroom.set_exits(upstairs_hall)
74 | alices_bedroom.set_exits(upstairs_hall)
75 | study.set_exits(upstairs_hall)
76 | bathroom.set_exits(upstairs_hall)
77 |
78 | house = (kitchen, living_room, dining_room, front_hall, driveway, garage,
79 | upstairs_hall, bobs_bedroom, alices_bedroom, study, bathroom,
80 | path_by_the_shed, shed)
81 |
82 | falcon = Treasure('golden falcon', location=dining_room)
83 | jewels = PluralTreasure('stolen jewels', location=garage)
84 |
85 | cupboards = Container('cupboards', location=kitchen)
86 | liquor_cabinet = Container('liquor cabinet', location=dining_room)
87 | mailbox = Container('mailbox', location=driveway)
88 |
89 | bobs_bed = ProperContainer("<*> bed", location=bobs_bedroom, owner=bob)
90 | alices_bed = ProperContainer("<*> bed", location=alices_bedroom, owner=alice)
91 |
92 | brandy = Item('bottle of brandy', location=liquor_cabinet)
93 | revolver = Weapon('revolver', location=random.choice([bobs_bed, alices_bed]))
94 | dead_body = Horror('dead body', location=bathroom)
95 |
96 | # when making alice and bob, we let them recognize certain important
97 | # objects in their world
98 | for c in (alice, bob):
99 | c.configure_objects(
100 | revolver=revolver,
101 | brandy=brandy,
102 | dead_body=dead_body,
103 | )
104 |
105 | ALL_ITEMS = (falcon, jewels, revolver, brandy)
106 |
--------------------------------------------------------------------------------
/src/swallows/engine/objects.py:
--------------------------------------------------------------------------------
1 | import random
2 | import sys
3 |
4 | from swallows.engine.events import Event
5 |
6 | ### TOPICS ###
7 |
8 | # a "topic" is just what a character has recently had addressed to
9 | # them. It could be anything, not just words, by another character
10 | # (for example, a gesture.)
11 |
12 | class Topic(object):
13 | def __init__(self, originator, subject=None):
14 | self.originator = originator
15 | self.subject = subject
16 |
17 |
18 | class GreetTopic(Topic):
19 | pass
20 |
21 |
22 | class SpeechTopic(Topic):
23 | pass
24 |
25 |
26 | class QuestionTopic(Topic):
27 | pass
28 |
29 |
30 | ### BELIEFS ###
31 |
32 | #
33 | # a belief is something an Animate believes. they come in a few types:
34 | #
35 | # - a belief that an object is somewhere
36 | # - because they saw it there (memory)
37 | # - because some other character told them it was there
38 | # - a belief that they should do something (a goal), which has subtypes:
39 | # - a belief that an object is desirable & they should try to acquire it
40 | # - a belief that something should be done about something (bland, general)
41 | # - a belief that another Animate believes something
42 | #
43 | # of course, any particular belief may turn out not to be true
44 | #
45 |
46 | # abstract base class
47 | class Belief(object):
48 | # constructor of all subclasses of this class should accept being
49 | # called with only one argument, as a convenience sort of thing
50 | # for BeliefSet.get and .remove, which don't really care about anything
51 | # about the Belief except for its class and its subject.
52 | # although, usually, you do want to pass more than one argument when
53 | # making a real Belief to pass to BeliefSet.add. (clear as mud, right?)
54 | def __init__(self, subject): # kind of silly for an ABC to have a
55 | assert isinstance(subject, Actor) # constructor, but it is to emphasize
56 | self.subject = subject # that all beliefs have a subject,
57 | # which is the thing we believe
58 | # something about
59 |
60 | def __str__(self):
61 | raise NotImplementedError
62 |
63 |
64 | class ItemLocation(Belief): # formerly "Memory"
65 | def __init__(self, subject, location=None, informant=None, concealer=None):
66 | assert isinstance(subject, Actor)
67 | assert isinstance(location, Actor) or location is None
68 | self.subject = subject # the thing we think is somewhere
69 | self.location = location # the place we think it is
70 | self.informant = informant # the actor who told us about it
71 | self.concealer = concealer # the actor who we think hid it there
72 |
73 | def __str__(self):
74 | s = "%s is in %s" % (
75 | self.subject.render(),
76 | self.location.render()
77 | )
78 | if self.concealer:
79 | s += " (hidden there by %s)" % self.concealer.render()
80 | if self.informant:
81 | s += " (%s told me so)" % self.informant.render()
82 | return s
83 |
84 |
85 | class Goal(Belief):
86 | def __init__(self, subject, phrase=None):
87 | assert isinstance(subject, Actor)
88 | self.subject = subject # the thing we would like to do something about
89 | self.phrase = phrase # human-readable description
90 |
91 | def __str__(self):
92 | return "I should %s %s" % (
93 | self.phrase,
94 | self.subject.render()
95 | )
96 |
97 |
98 | class Desire(Goal):
99 | def __init__(self, subject):
100 | assert isinstance(subject, Actor)
101 | self.subject = subject # the thing we would like to acquire
102 |
103 | def __str__(self):
104 | return "I want %s" % (
105 | self.subject.render()
106 | )
107 |
108 |
109 | # oh dear
110 | class BeliefsBelief(Belief):
111 | def __init__(self, subject, belief_set=None):
112 | assert isinstance(subject, Animate)
113 | self.subject = subject # the animate we think holds the belief
114 | if belief_set is None:
115 | belief_set = BeliefSet()
116 | assert isinstance(belief_set, BeliefSet)
117 | self.belief_set = belief_set # the beliefs we think they hold
118 |
119 | def __str__(self):
120 | return "%s believes { %s }" % (
121 | self.subject.render(),
122 | self.belief_set
123 | )
124 |
125 |
126 | class BeliefSet(object):
127 | """A BeliefSet works something like a Python set(), but has the
128 | following constraints:
129 |
130 | There can be only one of each type of Belief about a particular
131 | item in the set.
132 |
133 | So it's really kind of a map from Actors to maps from Belief
134 | subclasses to Beliefs.
135 |
136 | But it behooves us (or at least, me) to think of it as a set.
137 | (Besides, it might change.)
138 |
139 | """
140 | def __init__(self):
141 | self.belief_map = {}
142 |
143 | def add(self, belief):
144 | assert isinstance(belief, Belief)
145 | subject = belief.subject
146 | self.belief_map.setdefault(subject, {})[belief.__class__] = belief
147 |
148 | def remove(self, belief):
149 | # the particular belief passed to us doesn't really matter. we extract
150 | # the class and subject and return any existing belief we may have
151 | assert isinstance(belief, Belief)
152 | subject = belief.subject
153 | beliefs = self.belief_map.setdefault(subject, {})
154 | if belief.__class__ in beliefs:
155 | del beliefs[belief.__class__]
156 |
157 | def get(self, belief):
158 | # the particular belief passed to us doesn't really matter. we extract
159 | # the class and subject and return any existing belief we may have
160 | assert isinstance(belief, Belief)
161 | subject = belief.subject
162 | return self.belief_map.setdefault(subject, {}).get(
163 | belief.__class__, None
164 | )
165 |
166 | def subjects(self):
167 | for subject in self.belief_map:
168 | yield subject
169 |
170 | def beliefs_for(self, subject):
171 | beliefs = self.belief_map.setdefault(subject, {})
172 | for class_ in beliefs:
173 | yield beliefs[class_]
174 |
175 | def beliefs_of_class(self, class_):
176 | for subject in self.subjects():
177 | for belief in self.beliefs_for(subject):
178 | if belief.__class__ == class_:
179 | yield belief
180 |
181 | def __str__(self):
182 | l = []
183 | for subject in self.subjects():
184 | for belief in self.beliefs_for(subject):
185 | l.append(str(belief))
186 | return ', '.join(l)
187 |
188 |
189 | ### ACTORS (objects in the world) ###
190 |
191 | class Actor(object):
192 | def __init__(self, name, location=None, owner=None, collector=None):
193 | self.name = name
194 | self.collector = collector
195 | self.contents = set()
196 | self.enter = ""
197 | self.owner = owner
198 | self.location = None
199 | if location is not None:
200 | self.move_to(location)
201 |
202 | def notable(self):
203 | return self.treasure() or self.weapon() or self.animate() or self.horror()
204 |
205 | def treasure(self):
206 | return False
207 |
208 | def weapon(self):
209 | return False
210 |
211 | def horror(self):
212 | return False
213 |
214 | def takeable(self):
215 | return False
216 |
217 | def animate(self):
218 | return False
219 |
220 | def container(self):
221 | return False
222 |
223 | def article(self):
224 | return 'the'
225 |
226 | def posessive(self):
227 | return "its"
228 |
229 | def accusative(self):
230 | return "it"
231 |
232 | def pronoun(self):
233 | return "it"
234 |
235 | def was(self):
236 | return "was"
237 |
238 | def is_(self):
239 | return "is"
240 |
241 | def emit(self, *args, **kwargs):
242 | if self.collector:
243 | self.collector.collect(Event(*args, **kwargs))
244 |
245 | def move_to(self, location):
246 | if self.location:
247 | self.location.contents.remove(self)
248 | self.location = location
249 | self.location.contents.add(self)
250 |
251 | def render(self, event=None):
252 | """Return a string containing what we call this object, in the context
253 | of the given event (which may be None, to get a 'generic' description.)
254 |
255 | """
256 | name = self.name
257 | repl = None
258 | if self.owner is not None:
259 | repl = self.owner.render() + "'s"
260 | if event:
261 | if event.speaker is self.owner:
262 | repl = 'my'
263 | elif event.addressed_to is self.owner:
264 | repl = 'your'
265 | elif event.initiator() is self.owner:
266 | repl = event.initiator().posessive()
267 | if repl is not None:
268 | name = name.replace('<*>', repl)
269 | article = self.article()
270 | if not article:
271 | return name
272 | return '%s %s' % (article, name)
273 |
274 | def indefinite(self):
275 | article = 'a'
276 | if self.name.startswith(('a', 'e', 'i', 'o', 'u')):
277 | article = 'an'
278 | return '%s %s' % (article, self.name)
279 |
280 |
281 | ### some mixins for Actors ###
282 |
283 | class ProperMixin(object):
284 | def article(self):
285 | return ''
286 |
287 |
288 | class PluralMixin(object):
289 | def posessive(self):
290 | return "their"
291 |
292 | def accusative(self):
293 | return "them"
294 |
295 | def pronoun(self):
296 | return "they"
297 |
298 | def indefinite(self):
299 | article = 'some'
300 | return '%s %s' % (article, self.name)
301 |
302 | def was(self):
303 | return "were"
304 |
305 | def is_(self):
306 | return "are"
307 |
308 |
309 | class MasculineMixin(object):
310 | def posessive(self):
311 | return "his"
312 |
313 | def accusative(self):
314 | return "him"
315 |
316 | def pronoun(self):
317 | return "he"
318 |
319 |
320 | class FeminineMixin(object):
321 | def posessive(self):
322 | return "her"
323 |
324 | def accusative(self):
325 | return "her"
326 |
327 | def pronoun(self):
328 | return "she"
329 |
330 |
331 | ### ANIMATE OBJECTS ###
332 |
333 | class Animate(Actor):
334 | def __init__(self, name, location=None, owner=None, collector=None):
335 | Actor.__init__(
336 | self, name, location=location, owner=owner, collector=None
337 | )
338 | self.topic = None
339 | self.beliefs = BeliefSet()
340 |
341 | def animate(self):
342 | return True
343 |
344 | # for debugging
345 | def dump_beliefs(self):
346 | for subject in self.beliefs.subjects():
347 | for belief in self.beliefs.beliefs_for(subject):
348 | print ".oO{ %s }" % belief
349 |
350 | ###--- belief accessors/manipulators ---###
351 |
352 | # these are mostly just aliases for accessing the BeliefSet.
353 |
354 | def remember_location(self, thing, location, concealer=None):
355 | """Update this Animate's beliefs to include a belief that the
356 | given thing is located at the given location.
357 |
358 | Really just a readable alias for believe_location.
359 |
360 | """
361 | self.believe_location(thing, location, informant=None, concealer=concealer)
362 |
363 | def believe_location(self, thing, location, informant=None, concealer=None):
364 | """Update this Animate's beliefs to include a belief that the
365 | given thing is located at the given location. They may have
366 | been told this by someone.
367 |
368 | """
369 | self.beliefs.add(ItemLocation(
370 | thing, location, informant=informant, concealer=concealer
371 | ))
372 |
373 | def recall_location(self, thing):
374 | """Return an ItemLocation (belief) about this thing, or None."""
375 | return self.beliefs.get(ItemLocation(thing))
376 |
377 | def forget_location(self, thing):
378 | self.beliefs.remove(ItemLocation(thing))
379 |
380 | def desire(self, thing):
381 | self.beliefs.add(Desire(thing))
382 |
383 | def quench_desire(self, thing):
384 | # usually called when it has been acquired
385 | self.beliefs.remove(Desire(thing))
386 |
387 | def does_desire(self, thing):
388 | if thing.treasure():
389 | return True # omg YES
390 | if thing.weapon():
391 | return True # could come in handy. (TODO, sophisticate this?)
392 | return self.beliefs.get(Desire(thing)) is not None
393 |
394 | def believed_beliefs_of(self, other):
395 | """Returns a BeliefSet (not a Belief) that this Animate
396 | believes the other Animate holds.
397 |
398 | Typically you would manipulate this BeliefSet directly
399 | with add, remove, get, etc.
400 |
401 | """
402 | assert isinstance(other, Animate)
403 | # for extra fun, try reading the code of this method out loud!
404 | beliefs_belief = self.beliefs.get(BeliefsBelief(other))
405 | if beliefs_belief is None:
406 | beliefs_belief = BeliefsBelief(other, BeliefSet())
407 | self.beliefs.add(beliefs_belief)
408 | return beliefs_belief.belief_set
409 |
410 | ###--- topic stuff ---###
411 |
412 | def address(self, other, topic, phrase, participants=None):
413 | if participants is None:
414 | participants = [self, other]
415 | other.topic = topic
416 | self.emit(phrase, participants, speaker=self, addressed_to=other)
417 |
418 | def greet(self, other, phrase, participants=None):
419 | self.address(other, GreetTopic(self), phrase, participants)
420 |
421 | def speak_to(self, other, phrase, participants=None, subject=None):
422 | self.address(other, SpeechTopic(self, subject=subject), phrase, participants)
423 |
424 | def question(self, other, phrase, participants=None, subject=None):
425 | self.address(other, QuestionTopic(self, subject=subject), phrase, participants)
426 |
427 | ###--- generic actions ---###
428 |
429 | def place_in(self, location):
430 | """Like move_to but quieter. For setting up scenes, etc.
431 |
432 | """
433 | if self.location is not None:
434 | self.location.contents.remove(self)
435 | self.location = location
436 | self.location.contents.add(self)
437 | # this is needed so that the Editor knows where the character starts.
438 | # the Editor should (does?) strip out all instances of these that
439 | # aren't informative to the reader.
440 | self.emit("<1> in <2>", [self, self.location])
441 | # a side-effect of the following code is, if they start in a location
442 | # with a horror,they don't react to it. They probably should.
443 | for x in self.location.contents:
444 | if x == self:
445 | continue
446 | if x.notable():
447 | self.emit("<1> saw <2>", [self, x])
448 | self.remember_location(x, self.location)
449 |
450 | def move_to(self, location):
451 | assert(location != self.location)
452 | assert(location is not None)
453 | for x in self.location.contents:
454 | # otherwise we get "Bob saw Bob leave the room", eh?
455 | if x is self:
456 | continue
457 | if x.animate():
458 | x.emit("<1> saw <2> leave the %s" % x.location.noun(), [x, self])
459 | if self.location is not None:
460 | self.location.contents.remove(self)
461 | previous_location = self.location
462 | self.location = location
463 | assert self not in self.location.contents
464 | self.location.contents.add(self)
465 | self.emit("<1> went to <2>", [self, self.location],
466 | previous_location=previous_location)
467 |
468 | def point_at(self, other, item):
469 | # it would be nice if there was some way to
470 | # indicate the revolver as part of the Topic which will follow,
471 | # or otherwise indicate the context as "at gunpoint"
472 |
473 | assert self.location == other.location
474 | assert item.location == self
475 | self.emit("<1> pointed <3> at <2>",
476 | [self, other, item])
477 | for actor in self.location.contents:
478 | if actor.animate():
479 | actor.remember_location(item, self)
480 |
481 | def put_down(self, item):
482 | assert(item.location == self)
483 | self.emit("<1> put down <2>", [self, item])
484 | item.move_to(self.location)
485 | for actor in self.location.contents:
486 | if actor.animate():
487 | actor.remember_location(item, self.location)
488 |
489 | def pick_up(self, item):
490 | assert(item.location == self.location)
491 | self.emit("<1> picked up <2>", [self, item])
492 | item.move_to(self)
493 | for actor in self.location.contents:
494 | if actor.animate():
495 | actor.remember_location(item, self)
496 |
497 | def give_to(self, other, item):
498 | assert(item.location == self)
499 | assert(self.location == other.location)
500 | self.emit("<1> gave <3> to <2>", [self, other, item])
501 | item.move_to(other)
502 | for actor in self.location.contents:
503 | if actor.animate():
504 | actor.remember_location(item, other)
505 |
506 | def wander(self):
507 | self.move_to(
508 | self.location.exits[
509 | random.randint(0, len(self.location.exits)-1)
510 | ]
511 | )
512 |
513 | def live(self):
514 | """This gets called on each turn an animate moves.
515 |
516 | You need to implement this for particular animates.
517 |
518 | """
519 | raise NotImplementedError(
520 | 'Please implement %s.live()' % self.__class__.__name__
521 | )
522 |
523 |
524 | class Male(MasculineMixin, ProperMixin, Animate):
525 | pass
526 |
527 |
528 | class Female(FeminineMixin, ProperMixin, Animate):
529 | pass
530 |
531 |
532 | ### LOCATIONS ###
533 |
534 | class Location(Actor):
535 | def __init__(self, name, enter="went to", noun="room", owner=None):
536 | self.name = name
537 | self.enter = enter
538 | self.contents = set()
539 | self.exits = []
540 | self.noun_ = noun
541 | self.owner = owner
542 |
543 | def noun(self):
544 | return self.noun_
545 |
546 | def set_exits(self, *exits):
547 | for exit in exits:
548 | assert isinstance(exit, Location)
549 | self.exits = exits
550 |
551 |
552 | class ProperLocation(ProperMixin, Location):
553 | pass
554 |
555 |
556 | ### OTHER INANIMATE OBJECTS ###
557 |
558 | class Item(Actor):
559 | def takeable(self):
560 | return True
561 |
562 |
563 | class Weapon(Item):
564 | def weapon(self):
565 | return True
566 |
567 |
568 | class Container(Actor):
569 | def container(self):
570 | return True
571 |
572 |
573 | class ProperContainer(ProperMixin, Container):
574 | pass
575 |
576 |
577 | class Treasure(Item):
578 | def treasure(self):
579 | return True
580 |
581 |
582 | class PluralTreasure(PluralMixin, Treasure):
583 | pass
584 |
585 |
586 | class Horror(Actor):
587 | def horror(self):
588 | return True
589 |
--------------------------------------------------------------------------------
/src/swallows/engine/events.py:
--------------------------------------------------------------------------------
1 | import random
2 | import sys
3 |
4 | # TODO
5 |
6 | # Diction:
7 | # - use indef art when they have no memory of an item that they see
8 | # - dramatic irony would be really nice, but hard to pull off. Well, a certain
9 | # amount happens naturally now, with character pov. but more could be done
10 | # - "Chapter 3. _In which Bob hides the stolen jewels in the mailbox, etc_" --
11 | # i.e. chapter summaries -- that's a little too fancy to hope for, but with
12 | # a sufficiently smart Editor it could be done
13 |
14 | ### EVENTS ###
15 |
16 | class Event(object):
17 | def __init__(self, phrase, participants, excl=False,
18 | previous_location=None,
19 | speaker=None,
20 | addressed_to=None,
21 | exciting=False):
22 | """participants[0] is always the initiator, and we
23 | record the location that the event was initiated in.
24 |
25 | For now, we assume such an event can be:
26 | - observed by every actor at that location
27 | - affects only actors at that location
28 |
29 | In the future, we *may* have:
30 | - active and passive participants
31 | - active participants all must be present at the location
32 | - passive participants need not be
33 | (probably done by passing a number n: the first n
34 | participants are to be considered active)
35 |
36 | speaker and addressed_to apply to dialogue.
37 | If speaker == None, it means the narrator is speaking.
38 | If addressed_to == None, it means the reader is being spoken to.
39 |
40 | """
41 | self.phrase = phrase
42 | self.participants = participants
43 | self.location = participants[0].location
44 | self._previous_location = previous_location
45 | self.excl = excl
46 | self.speaker = speaker
47 | self.addressed_to = addressed_to
48 | self.exciting = exciting
49 |
50 | def rephrase(self, new_phrase):
51 | """Does not modify the event. Returns a new copy."""
52 | return Event(new_phrase, self.participants, excl=self.excl)
53 |
54 | def initiator(self):
55 | return self.participants[0]
56 |
57 | def previous_location(self):
58 | return self._previous_location
59 |
60 | def render(self):
61 | phrase = self.phrase
62 | i = 0
63 | for participant in self.participants:
64 | phrase = phrase.replace('<%d>' % (i + 1), participant.render(event=self))
65 | phrase = phrase.replace('' % (i + 1), participant.indefinite())
66 | phrase = phrase.replace('' % (i + 1), participant.posessive())
67 | phrase = phrase.replace('' % (i + 1), participant.accusative())
68 | phrase = phrase.replace('' % (i + 1), participant.pronoun())
69 | phrase = phrase.replace('' % (i + 1), participant.was())
70 | phrase = phrase.replace('' % (i + 1), participant.is_())
71 | i = i + 1
72 | return phrase
73 |
74 | def __str__(self):
75 | phrase = self.render()
76 | if self.excl:
77 | phrase = phrase + '!'
78 | else:
79 | phrase = phrase + '.'
80 | return phrase[0].upper() + phrase[1:]
81 |
82 |
83 | class AggregateEvent(Event):
84 | """Attempt at a way to combine multiple events into a single
85 | sentence. Each constituent event must have the same initiator.
86 |
87 | This is definitely not as nice as it could be.
88 |
89 | """
90 | def __init__(self, template, events, excl=False):
91 | self.template = template
92 | self.events = events
93 | self.excl = excl
94 | self.phrase = 'SEE SUBEVENTS PLZ'
95 | self._initiator = self.events[0].initiator()
96 | for event in self.events:
97 | assert event.initiator() == self._initiator
98 | self.location = self._initiator.location
99 |
100 | def rephrase(self, new_phrase):
101 | #raise NotImplementedError
102 | return self
103 |
104 | def initiator(self):
105 | return self._initiator
106 |
107 | def previous_location(self):
108 | return self.events[0].previous_location()
109 |
110 | def __str__(self):
111 | phrase = self.template % tuple([x.render() for x in self.events])
112 | if self.excl:
113 | phrase = phrase + '!'
114 | else:
115 | phrase = phrase + '.'
116 | return phrase[0].upper() + phrase[1:]
117 |
118 |
119 | class EventCollector(object):
120 | def __init__(self):
121 | self.events = []
122 |
123 | def collect(self, event):
124 | if self.events and str(event) == str(self.events[-1]):
125 | raise ValueError('Duplicate event: %s' % event)
126 | if event.phrase == '<1> went to <2>':
127 | assert event.previous_location() is not None
128 | assert event.previous_location() != event.location
129 | self.events.append(event)
130 |
131 |
132 | # not really needed, as emit() does nothing if there is no collector
133 | class Oblivion(EventCollector):
134 | def collect(self, event):
135 | pass
136 |
137 |
138 | oblivion = Oblivion()
139 |
140 |
141 | ### EDITOR AND PUBLISHER ###
142 |
143 | class Editor(object):
144 | """The Editor is remarkably similar to the _peephole optimizer_ in
145 | compiler construction. Instead of replacing sequences of instructions
146 | with more efficient but semantically equivalent sequences of
147 | instructions, it replaces sequences of sentences with more readable
148 | but semantically equivalent sequences of sentences.
149 |
150 | The Editor is also responsible for chopping up the sequence of
151 | sentences into "sensible" paragraphs. (This might be like a compiler
152 | code-rewriting pass that inserts NOPs to ensure instructions are on a
153 | word boundary, or some such.)
154 |
155 | The Editor is also responsible for picking which character to
156 | follow. (I don't think there's a compiler construction analogy for
157 | that.)
158 |
159 | Note that the event stream must start with " was in "
160 | as the first event for each character. Otherwise the Editor don't know
161 | who started where.
162 |
163 | Well, OK, it *used* to look a lot like a peephole optimizer. Soon, it
164 | will make multiple passes. It still looks a lot like the optimization
165 | phase of a compiler, though.
166 |
167 | """
168 |
169 | def __init__(self, collector, main_characters):
170 | self.events = list(reversed(collector.events))
171 | self.main_characters = main_characters
172 | self.pov_index = 0
173 | self.transformers = []
174 | # maps main characters to where they currently are (omnisciently)
175 | self.character_location = {}
176 | # maps main characters to where the reader last saw them
177 | self.last_seen_at = {}
178 | # maps characters to things that happened to them while not narrated
179 | self.exciting_developments = {}
180 |
181 | def add_transformer(self, transformer):
182 | self.transformers.append(transformer)
183 |
184 | def publish(self):
185 | paragraph_num = 1
186 | while len(self.events) > 0:
187 | pov_actor = self.main_characters[self.pov_index]
188 | paragraph_events = self.generate_paragraph_events(pov_actor)
189 | for transformer in self.transformers:
190 | if paragraph_events:
191 | paragraph_events = transformer.transform(
192 | self, paragraph_events, paragraph_num
193 | )
194 | self.publish_paragraph(paragraph_events)
195 | self.pov_index += 1
196 | if self.pov_index >= len(self.main_characters):
197 | self.pov_index = 0
198 | paragraph_num += 1
199 |
200 | def generate_paragraph_events(self, pov_actor):
201 | quota = random.randint(10, 25)
202 | paragraph_events = []
203 | while len(paragraph_events) < quota and len(self.events) > 0:
204 | event = self.events.pop()
205 |
206 | if not paragraph_events:
207 | # this is the first sentence of the paragraph
208 | # if the reader wasn't aware they were here, add an event
209 | if self.last_seen_at.get(pov_actor, None) != event.location:
210 | if not (('went to' in event.phrase) or
211 | ('made way to' in event.phrase) or
212 | (event.phrase == '<1> in <2>')):
213 | paragraph_events.append(Event('<1> in <2>', [pov_actor, event.location]))
214 | # if something exciting happened, tell the reader
215 | for (obj, loc) in self.exciting_developments.get(pov_actor, []):
216 | paragraph_events.append(Event('<1> had found <2> in <3>', [pov_actor, obj, loc]))
217 | self.exciting_developments[pov_actor] = []
218 |
219 | # update our idea of where the character is, even if these are
220 | # not events we will be dumping out
221 | self.character_location[event.initiator()] = event.location
222 |
223 | if event.location == self.character_location[pov_actor]:
224 | paragraph_events.append(event)
225 | # update the reader's idea of where the character is
226 | self.last_seen_at[event.initiator()] = event.location
227 | else:
228 | if event.exciting:
229 | self.exciting_developments.setdefault(event.initiator(), []).append(
230 | (event.participants[1], event.participants[2])
231 | )
232 |
233 | return paragraph_events
234 |
235 | def publish_paragraph(self, paragraph_events):
236 | for event in paragraph_events:
237 | sys.stdout.write(str(event) + " ")
238 | #sys.stdout.write("\n")
239 | print
240 | print
241 |
242 |
243 | class Transformer(object):
244 | pass
245 |
246 |
247 | class DeduplicateTransformer(Transformer):
248 | # check for verbatim repeated. this could be 'dangerous' if, say,
249 | # you have two characters, Bob Jones and Bob Smith, and both are
250 | # named 'Bob', and they are actually two different events... but...
251 | # for now that is an edge case.
252 | def transform(self, editor, incoming_events, paragraph_num):
253 | events = []
254 | for event in incoming_events:
255 | if events:
256 | if str(event) == str(events[-1]):
257 | events[-1].phrase = event.phrase + ', twice'
258 | elif str(event.rephrase(event.phrase + ', twice')) == str(events[-1]):
259 | events[-1].phrase = event.phrase + ', several times'
260 | elif str(event.rephrase(event.phrase + ', several times')) == str(events[-1]):
261 | pass
262 | else:
263 | events.append(event)
264 | else:
265 | events.append(event)
266 | return events
267 |
268 |
269 | class UsePronounsTransformer(Transformer):
270 | # replace repeated proper nouns with pronouns
271 | def transform(self, editor, incoming_events, paragraph_num):
272 | events = []
273 | for event in incoming_events:
274 | if events:
275 | if event.initiator() == events[-1].initiator():
276 | event.phrase = event.phrase.replace('<1>', '')
277 | events.append(event)
278 | else:
279 | events.append(event)
280 | return events
281 |
282 |
283 | class MadeTheirWayToTransformer(Transformer):
284 | def transform(self, editor, incoming_events, paragraph_num):
285 | events = []
286 | for event in incoming_events:
287 | if (events and
288 | event.initiator() == events[-1].initiator()):
289 | if (events[-1].phrase in ('<1> went to <2>',) and
290 | event.phrase == '<1> went to <2>'):
291 | assert event.location == event.participants[1]
292 | assert events[-1].previous_location() is not None
293 | assert events[-1].location == events[-1].participants[1]
294 | events[-1].phrase = '<1> made way to <2>'
295 | events[-1].participants[1] = event.participants[1]
296 | events[-1].location = event.participants[1]
297 | elif (events[-1].phrase in ('<1> made way to <2>',) and
298 | event.phrase == '<1> went to <2>'):
299 | assert event.location == event.participants[1]
300 | assert events[-1].previous_location() is not None
301 | assert events[-1].location == events[-1].participants[1]
302 | events[-1].phrase = '<1> made way to <2>'
303 | events[-1].participants[1] = event.participants[1]
304 | events[-1].location = event.participants[1]
305 | else:
306 | events.append(event)
307 | else:
308 | events.append(event)
309 | return events
310 |
311 |
312 | # well well well
313 | from swallows.engine.objects import Actor
314 | weather = Actor('the weather')
315 |
316 |
317 | class AddWeatherFrifferyTransformer(Transformer):
318 | def transform(self, editor, incoming_events, paragraph_num):
319 | events = []
320 | if paragraph_num == 1:
321 | choice = random.randint(0, 3)
322 | if choice == 0:
323 | events.append(Event("It was raining", [weather]))
324 | if choice == 1:
325 | events.append(Event("It was snowing", [weather]))
326 | if choice == 2:
327 | events.append(Event("The sun was shining", [weather]))
328 | if choice == 3:
329 | events.append(Event("The day was overcast and humid", [weather]))
330 | return events + incoming_events
331 |
332 |
333 | class AddParagraphStartFrifferyTransformer(Transformer):
334 | def transform(self, editor, incoming_events, paragraph_num):
335 | first_event = incoming_events[0]
336 | if paragraph_num == 1:
337 | return incoming_events
338 | if str(first_event).startswith("'"):
339 | return incoming_events
340 | if " had found " in str(first_event):
341 | return incoming_events
342 | if " was in " in str(first_event):
343 | return incoming_events
344 | choice = random.randint(0, 8)
345 | if choice == 0:
346 | first_event = first_event.rephrase(
347 | "Later on, " + first_event.phrase
348 | )
349 | if choice == 1:
350 | first_event = first_event.rephrase(
351 | "Suddenly, " + first_event.phrase
352 | )
353 | if choice == 2:
354 | first_event = first_event.rephrase(
355 | "After a moment's consideration, " + first_event.phrase
356 | )
357 | if choice == 3:
358 | first_event = first_event.rephrase(
359 | "Feeling anxious, " + first_event.phrase
360 | )
361 | return [first_event] + incoming_events[1:]
362 |
363 |
364 | class AggregateEventsTransformer(Transformer):
365 | # replace "Bob went to the kitchen. Bob saw the toaster"
366 | # with "Bob went to the kitchen, where he saw the toaster"
367 | def transform(self, editor, incoming_events, paragraph_num):
368 | events = []
369 | for event in incoming_events:
370 | if events:
371 | if ( event.initiator() == events[-1].initiator() and
372 | events[-1].phrase in ('<1> went to <2>',) and
373 | event.phrase in ('<1> saw <2>',) ):
374 | # this *might* be better if we only do it when <1>
375 | # is the pov character for this paragraph. but it
376 | # does work...
377 | event.phrase = event.phrase.replace('<1>', '')
378 | events[-1] = AggregateEvent(
379 | "%s, where %s", [events[-1], event],
380 | excl = event.excl)
381 | else:
382 | events.append(event)
383 | else:
384 | events.append(event)
385 | return events
386 |
387 |
388 | class DetectWanderingTransformer(Transformer):
389 | # not used yet
390 | # if they 'made their way' to their current location...
391 | def transform(self, editor, incoming_events, paragraph_num):
392 | events = []
393 | for event in incoming_events:
394 | if (event.phrase == '<1> made way to <2>' and
395 | event.location == event.previous_location()):
396 | event.phrase = '<1> wandered around for a bit, then came back to <2>'
397 | events.append(event)
398 | return events
399 |
400 |
401 | class Publisher(object):
402 | def __init__(self, characters=(), setting=(), friffery=False,
403 | debug=False, title='Untitled', chapters=18,
404 | events_per_chapter=810):
405 | self.characters = characters
406 | self.setting = setting
407 | self.friffery = friffery
408 | self.debug = debug
409 | self.title = title
410 | self.chapters = chapters
411 | self.events_per_chapter = events_per_chapter
412 |
413 | def publish_chapter(self, chapter_num):
414 | collector = EventCollector()
415 |
416 | for character in self.characters:
417 | character.collector = collector
418 | # don't continue a conversation from the previous chapter, please
419 | character.topic = None
420 | character.place_in(random.choice(self.setting))
421 |
422 | while len(collector.events) < self.events_per_chapter:
423 | for character in self.characters:
424 | character.live()
425 | #print len(collector.events) # , repr([str(e) for e in collector.events])
426 |
427 | if self.debug:
428 | for character in self.characters:
429 | print "%s'S EVENTS:" % character.name.upper()
430 | for event in collector.events:
431 | if event.participants[0] != character:
432 | continue
433 | print "%r in %s: %s" % (
434 | [p.render(event=event) for p in event.participants],
435 | event.location.render(),
436 | event.phrase
437 | )
438 | print
439 | for character in self.characters:
440 | print "%s'S STATE:" % character.name.upper()
441 | character.dump_beliefs()
442 | print
443 | print "- - - - -"
444 | print
445 |
446 | editor = Editor(collector, self.characters)
447 | editor.add_transformer(MadeTheirWayToTransformer())
448 | editor.add_transformer(DeduplicateTransformer())
449 | editor.add_transformer(AggregateEventsTransformer())
450 | editor.add_transformer(DetectWanderingTransformer())
451 | # this one should be last, so prior transformers don't
452 | # have to worry themselves about looking for pronouns
453 | editor.add_transformer(UsePronounsTransformer())
454 | # this should be a matter of configuring what transformers
455 | # to use, when you instantiate a Publisher
456 | if self.friffery:
457 | editor.add_transformer(AddWeatherFrifferyTransformer())
458 | editor.add_transformer(AddParagraphStartFrifferyTransformer())
459 | editor.publish()
460 |
461 | def publish(self):
462 | print self.title
463 | print "=" * len(self.title)
464 | print
465 |
466 | for chapter in range(1, self.chapters+1):
467 | print "Chapter %d." % chapter
468 | print "-----------"
469 | print
470 |
471 | self.publish_chapter(chapter)
472 |
--------------------------------------------------------------------------------
/src/swallows/story/characters.py:
--------------------------------------------------------------------------------
1 | # Copyright (c)2013 Chris Pressey, Cat's Eye Technologies
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a
4 | # copy of this software and associated documentation files (the "Software"),
5 | # to deal in the Software without restriction, including without limitation
6 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | # and/or sell copies of the Software, and to permit persons to whom the
8 | # Software is furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | # DEALINGS IN THE SOFTWARE.
20 |
21 | import random
22 | import sys
23 |
24 | from swallows.engine.objects import (
25 | Animate, ProperMixin, MasculineMixin, FeminineMixin,
26 | Topic,
27 | GreetTopic, SpeechTopic, QuestionTopic,
28 | Belief, ItemLocation, Goal,
29 | )
30 |
31 | # TODO
32 |
33 | # They can hide something, then see the other carrying it, then check that
34 | # it's still hidden, and be surprised that it's no longer ther.
35 | # 'Hello, Alice', said Bob. 'Hello, Bob', replied Alice. NEVER GETS OLD
36 | # they should always scream at seeing the dead body. the scream should
37 | # be heard throughout the house and yard.
38 | # ...they check that the brandy is still in the liquor cabinet. is this
39 | # really necessary?
40 | # certain things can't be taken, but can be dragged (like the body)
41 | # path-finder between any two rooms -- not too difficult, even if it
42 | # would be nicer in Prolog.
43 | # "it was so nice" -- actually *have* memories of locations, and feelings
44 | # (good/bad, 0 to 10 or something) about memories
45 | # anxiety memory = the one they're most recently panicked about
46 | # memory of whether the revolver was loaded last time they saw it
47 | # calling their bluff
48 | # making a run for it when at gunpoint (or trying to distract them,
49 | # slap the gun away, scramble for it, etc)
50 | # revolver might jam when they try to shoot it (maybe it should be a
51 | # pistol instead, as those can jam more easily)
52 | # dear me, someone might actually get shot. then what? another dead body?
53 |
54 |
55 | ### some Swallows-specific topics (sort of)
56 |
57 | class WhereQuestionTopic(Topic):
58 | pass
59 |
60 |
61 | class ThreatGiveMeTopic(Topic):
62 | pass
63 |
64 |
65 | class ThreatTellMeTopic(Topic):
66 | pass
67 |
68 |
69 | class ThreatAgreeTopic(Topic):
70 | pass
71 |
72 |
73 | ### some Swallows-specific beliefs
74 |
75 | class SuspicionOfHiding(Belief):
76 | """This character suspects some other character of hiding this thing."""
77 | def __str__(self):
78 | return "I think someone hid %s" % (
79 | self.subject.render()
80 | )
81 |
82 |
83 | ### Base character personalities for The Swallows
84 |
85 | class Character(Animate):
86 | def __init__(self, name, location=None, collector=None):
87 | """Constructor specific to characters. In it, we set up some
88 | Swallows-specific properties ('nerves').
89 |
90 | """
91 | Animate.__init__(self, name, location=location, collector=None)
92 | # this should really be *derived* from having a recent memory
93 | # of seeing a dead body in the bathroom. but for now,
94 | self.nerves = 'calm'
95 |
96 | def configure_objects(self, revolver=None, brandy=None, dead_body=None):
97 | """Here we set up some important items that this character needs
98 | to know about. This is maybe a form of dependency injection.
99 |
100 | """
101 | self.revolver = revolver
102 | self.brandy = brandy
103 | self.dead_body = dead_body
104 |
105 | def believe_location(self, thing, location, informant=None, concealer=None):
106 | # we override this method of Animate in order to also remove
107 | # our suspicion that the item has been hidden. 'cos we found it.
108 | Animate.believe_location(self, thing, location, informant=informant, concealer=concealer)
109 | self.beliefs.remove(SuspicionOfHiding(thing))
110 |
111 | def move_to(self, location):
112 | """Override some behaviour upon moving to a new location.
113 |
114 | """
115 | Animate.move_to(self, location)
116 | if random.randint(0, 10) == 0:
117 | self.emit("It was so nice being in <2> again",
118 | [self, self.location], excl=True)
119 |
120 | # okay, look around you.
121 | for x in self.location.contents:
122 | assert x.location == self.location
123 | if x == self:
124 | continue
125 | if x.horror():
126 | belief = self.recall_location(x)
127 | if belief:
128 | amount = random.choice(['shudder', 'wave'])
129 | emotion = random.choice(['fear', 'disgust', 'sickness', 'loathing'])
130 | self.emit("<1> felt a %s of %s as looked at <2>" % (amount, emotion), [self, x])
131 | self.remember_location(x, self.location)
132 | else:
133 | verb = random.choice(['screamed', 'yelped', 'went pale'])
134 | self.emit("<1> %s at the sight of " % verb, [self, x], excl=True)
135 | self.remember_location(x, self.location)
136 | self.nerves = 'shaken'
137 | elif x.animate():
138 | other = x
139 | self.emit("<1> saw <2>", [self, other])
140 | other.emit("<1> saw <2> walk into the %s" % self.location.noun(), [other, self])
141 | self.remember_location(x, self.location)
142 | self.greet(x, "'Hello, <2>,' said <1>")
143 | for y in other.contents:
144 | if y.treasure():
145 | self.emit(
146 | "<1> noticed <2> carrying ",
147 | [self, other, y])
148 | if self.revolver.location == self:
149 | self.point_at(other, self.revolver)
150 | self.address(other,
151 | ThreatGiveMeTopic(self, subject=y),
152 | "'Please give me <3>, <2>, or I shall shoot you,' said",
153 | [self, other, y])
154 | return
155 | # check if we suspect something of being hidden.
156 | suspicions = list(self.beliefs.beliefs_of_class(SuspicionOfHiding))
157 | # if we do... and we can do something about it...
158 | actionable_suspicions = []
159 | for suspicion in suspicions:
160 | if not suspicion.subject.treasure():
161 | continue
162 | if self.beliefs.get(ItemLocation(suspicion.subject)):
163 | continue
164 | actionable_suspicions.append(suspicion)
165 | if actionable_suspicions and self.revolver.location == self:
166 | suspicion = random.choice(actionable_suspicions)
167 | self.point_at(other, self.revolver)
168 | self.address(other,
169 | ThreatTellMeTopic(self, subject=suspicion.subject),
170 | "'Tell me where you have hidden <3>, <2>, or I shall shoot you,' said",
171 | [self, other, suspicion.subject])
172 | return
173 | elif x.notable():
174 | self.emit("<1> saw <2>", [self, x])
175 | self.remember_location(x, self.location)
176 |
177 | def live(self):
178 | """Override some behaviour for taking a turn in the story.
179 |
180 | """
181 | # first, if in a conversation, turn total attention to that
182 | if self.topic is not None:
183 | return self.converse(self.topic)
184 |
185 | # otherwise, if there are items here that you desire, you *must* pick
186 | # them up.
187 | for x in self.location.contents:
188 | if self.does_desire(x):
189 | self.pick_up(x)
190 | return
191 | people_about = False
192 |
193 | # otherwise, fixate on some valuable object (possibly the revolver)
194 | # that you are carrying:
195 | fixated_on = None
196 | for y in self.contents:
197 | if y.treasure():
198 | fixated_on = y
199 | break
200 | if not fixated_on and random.randint(0, 20) == 0 and self.revolver.location == self:
201 | fixated_on = self.revolver
202 |
203 | # check if you are alone
204 | for x in self.location.contents:
205 | if x.animate() and x is not self:
206 | people_about = True
207 |
208 | choice = random.randint(0, 25)
209 | if choice < 10 and not people_about:
210 | return self.hide_and_seek(fixated_on)
211 | if choice < 20:
212 | return self.wander()
213 | if choice == 20:
214 | self.emit("<1> yawned", [self])
215 | elif choice == 21:
216 | self.emit("<1> gazed thoughtfully into the distance", [self])
217 | elif choice == 22:
218 | self.emit("<1> thought heard something", [self])
219 | elif choice == 23:
220 | self.emit("<1> scratched head", [self])
221 | elif choice == 24:
222 | self.emit("<1> immediately had a feeling something was amiss", [self])
223 | else:
224 | return self.wander()
225 |
226 | #
227 | # The following are fairly plot-specific.
228 | #
229 |
230 | def hide_and_seek(self, fixated_on):
231 | # check for some place to hide the thing you're fixating on
232 | containers = []
233 | for container in self.location.contents:
234 | if container.container():
235 | # did I hide something here previously?
236 | beliefs_about_container = []
237 | for thing in self.beliefs.subjects():
238 | belief = self.recall_location(thing)
239 | if belief and belief.location == container:
240 | beliefs_about_container.append(belief)
241 | containers.append((container, beliefs_about_container))
242 | if not containers:
243 | # ? ... maybe this should be the responsibility of the caller
244 | return self.wander()
245 | # ok! we now have a list of containers, each of which has zero or
246 | # more beliefs of things being in it.
247 | if fixated_on:
248 | (container, beliefs) = random.choice(containers)
249 | self.emit("<1> hid <2> in <3>", [self, fixated_on, container])
250 | fixated_on.move_to(container)
251 | self.remember_location(fixated_on, container, concealer=self)
252 | return self.wander()
253 | else:
254 | # we're looking for treasure!
255 | # todo: it would maybe be better to prioritize this selection
256 | (container, beliefs) = random.choice(containers)
257 | # sometimes, we don't care what we think we know about something
258 | # (this lets us, for example, explore things in hopes of brandy)
259 | if beliefs and random.randint(0, 3) == 0:
260 | beliefs = None
261 | if beliefs:
262 | belief = random.choice(beliefs)
263 | thing = belief.subject
264 | picking_up = random.randint(0, 5) == 0
265 | if thing is self.revolver:
266 | picking_up = True
267 | if picking_up:
268 | if belief.concealer is self:
269 | self.emit("<1> retrieved <3> had hidden in <2>",
270 | [self, container, thing])
271 | else:
272 | self.emit("<1> retrieved <3> from <2>",
273 | [self, container, thing])
274 | # but!
275 | if thing.location != container:
276 | self.emit("But missing",
277 | [self, thing], excl=True
278 | )
279 | # forget ALLLLLLL about it, then. so realistic!
280 | self.forget_location(thing)
281 | else:
282 | thing.move_to(self)
283 | self.remember_location(thing, self)
284 | else:
285 | self.emit("<1> checked that <3> still in <2>",
286 | [self, container, thing])
287 | # but!
288 | if thing.location != container:
289 | self.emit("But missing",
290 | [self, thing], excl=True
291 | )
292 | self.forget_location(thing)
293 | self.beliefs.add(SuspicionOfHiding(thing))
294 | else: # no memories of this
295 | self.emit("<1> searched <2>", [self, container])
296 | desired_things = []
297 | for thing in container.contents:
298 | # remember what you saw whilst searching this container
299 | self.remember_location(thing, container)
300 | if self.does_desire(thing):
301 | desired_things.append(thing)
302 | if desired_things:
303 | thing = random.choice(desired_things)
304 | self.emit("<1> found <2> there, and took ",
305 | [self, thing, container], exciting=True)
306 | thing.move_to(self)
307 | self.remember_location(thing, self)
308 |
309 | def converse(self, topic):
310 | self.topic = None
311 | other = topic.originator
312 | if isinstance(topic, ThreatGiveMeTopic):
313 | found_object = None
314 | for x in self.contents:
315 | if x is topic.subject:
316 | found_object = x
317 | break
318 | if not found_object:
319 | self.speak_to(other,
320 | "'But I don't have <3>!' protested <1>",
321 | [self, other, topic.subject])
322 | else:
323 | self.speak_to(other,
324 | "'Please don't shoot!', <1> cried",
325 | [self, other, found_object])
326 | self.give_to(other, found_object)
327 | elif isinstance(topic, ThreatTellMeTopic):
328 | belief = self.recall_location(topic.subject)
329 | if not belief:
330 | self.speak_to(other,
331 | "'I have no memory of that, <2>,' <1> replied",
332 | [self, other, topic.subject])
333 | else:
334 | self.speak_to(other,
335 | "'Please don't shoot!', <1> cried, ' in <4>'",
336 | [self, other, topic.subject, belief.location])
337 | other.believe_location(topic.subject, belief.location,
338 | informant=self, concealer=self)
339 | elif isinstance(topic, ThreatAgreeTopic):
340 | self.speak_to(other,
341 | "'You make a persuasive case for remaining undecided, <2>,' said <1>",
342 | [self, other])
343 | self.beliefs.remove(Goal(topic.subject))
344 | # update other's BeliefsBelief about self to no longer
345 | # contain this Goal
346 | other.believed_beliefs_of(self).remove(Goal(topic.subject))
347 | elif isinstance(topic, GreetTopic):
348 | # emit, because making this a speak_to leads to too much silliness
349 | self.emit("'Hello, <2>,' replied <1>", [self, other])
350 | # this needs to be more general
351 | self_belief = self.recall_location(self.dead_body)
352 | if self_belief:
353 | self.discuss(other, self_belief)
354 | return
355 | # this need not be *all* the time
356 | for x in other.contents:
357 | if x.notable():
358 | self.remember_location(x, other)
359 | self.speak_to(other, "'I see you are carrying ,' said <1>", [self, other, x])
360 | return
361 | choice = random.randint(0, 3)
362 | if choice == 0:
363 | self.question(other, "'Lovely weather we're having, isn't it?' asked <1>")
364 | if choice == 1:
365 | self.speak_to(other, "'I was wondering where you were,' said <1>")
366 | elif isinstance(topic, QuestionTopic):
367 | if topic.subject is not None:
368 | choice = random.randint(0, 1)
369 | if choice == 0:
370 | self.speak_to(other, "'I know nothing about <3>, <2>,' explained <1>",
371 | [self, other, topic.subject])
372 | if choice == 1:
373 | self.speak_to(other, "'Perhaps, <2>,' replied <1>")
374 | else:
375 | self.speak_to(other, "'Perhaps, <2>,' replied <1>")
376 | elif isinstance(topic, WhereQuestionTopic):
377 | belief = self.recall_location(topic.subject)
378 | if not belief:
379 | self.speak_to(other,
380 | "'I don't know,' <1> answered simply",
381 | [self, other, topic.subject])
382 | elif belief.concealer == self:
383 | self.question(other,
384 | "'Why do you want to know where <3> is, <2>?'",
385 | [self, other, topic.subject])
386 | elif topic.subject.location == self:
387 | self.speak_to(other,
388 | "'I've got <3> right here, <2>'",
389 | [self, other, topic.subject])
390 | self.put_down(topic.subject)
391 | else:
392 | if topic.subject.location.animate():
393 | self.speak_to(other,
394 | "'I think <3> has <4>,', <1> recalled",
395 | [self, other, belief.location, topic.subject])
396 | else:
397 | self.speak_to(other,
398 | "'I believe it's in <3>, <2>,', <1> recalled",
399 | [self, other, belief.location])
400 | other.believe_location(
401 | topic.subject, belief.location, informant=self
402 | )
403 | elif isinstance(topic, SpeechTopic):
404 | choice = random.randint(0, 5)
405 | if choice == 0:
406 | self.emit("<1> nodded", [self])
407 | if choice == 1:
408 | self.emit("<1> remained silent", [self])
409 | if choice == 2:
410 | self.question(other, "'Do you really think so?' asked <1>")
411 | if choice == 3:
412 | self.speak_to(other, "'Yes, it's a shame really,' stated <1>")
413 | if choice == 4:
414 | self.speak_to(other, "'Oh, I know, I know,' said <1>")
415 | if choice == 5:
416 | # -- this is getting really annoying. disable for now. --
417 | # item = random.choice(ALL_ITEMS)
418 | # self.question(other, "'But what about <3>, <2>?' posed <1>",
419 | # [self, other, item], subject=item)
420 | self.speak_to(other, "'I see, <2>, I see,' said <1>")
421 |
422 | # this is its own method for indentation reasons
423 | def discuss(self, other, self_memory):
424 | assert self_memory
425 | assert isinstance(self_memory, Belief)
426 | # for now,
427 | # self_memory is an ItemLocation belief about something on our mind
428 | assert isinstance(self_memory, ItemLocation)
429 |
430 | # what do I believe the other believes about it?
431 | other_beliefs = self.believed_beliefs_of(other)
432 | other_memory = other_beliefs.get(self_memory)
433 |
434 | if not other_memory:
435 | self.question(other,
436 | "'Did you know there's in <4>?' asked <1>",
437 | [self, other, self_memory.subject, self_memory.location],
438 | subject=self_memory.subject)
439 | # well now they know what we think, anyway
440 | other.believed_beliefs_of(self).add(self_memory)
441 | return
442 | else:
443 | choice = random.randint(0, 2)
444 | if choice == 0:
445 | self.question(other, "'Do you think we should do something about <3>?' asked <1>",
446 | [self, other, self_memory.subject])
447 | other.believed_beliefs_of(self).add(self_memory)
448 | if choice == 1:
449 | self.speak_to(other, "'I think we should do something about <3>, <2>,' said <1>",
450 | [self, other, self_memory.subject])
451 | other.believed_beliefs_of(self).add(self_memory)
452 | if choice == 2:
453 | if self.nerves == 'calm':
454 | self.decide_what_to_do_about(other, self_memory.subject)
455 | else:
456 | if self.brandy.location == self:
457 | self.emit("<1> poured self a glass of brandy",
458 | [self, other, self_memory.subject])
459 | self.quench_desire(self.brandy)
460 | self.nerves = 'calm'
461 | self.put_down(self.brandy)
462 | elif self.recall_location(self.brandy):
463 | self.speak_to(other,
464 | "'I really must pour myself a drink,' moaned <1>",
465 | [self, other, self_memory.subject],
466 | subject=self.brandy)
467 | self.desire(self.brandy)
468 | if random.randint(0, 1) == 0:
469 | self.address(other, WhereQuestionTopic(self, subject=self.brandy),
470 | "'Where did you say <3> was?'",
471 | [self, other, self.brandy])
472 | else:
473 | self.address(other, WhereQuestionTopic(self, subject=self.brandy),
474 | "'Where is the brandy? I need a drink,' managed <1>",
475 | [self, other, self_memory.subject])
476 | self.desire(self.brandy)
477 |
478 | # this is its own method for indentation reasons
479 | def decide_what_to_do_about(self, other, thing):
480 | # this should probably be affected by whether this
481 | # character has, oh, i don't know, put the other at
482 | # gunpoint yet, or not, or something
483 | my_goal = self.beliefs.get(Goal(thing))
484 | if my_goal is None:
485 | if random.randint(0, 1) == 0:
486 | self.beliefs.add(Goal(thing, 'call the police about'))
487 | else:
488 | self.beliefs.add(Goal(thing, 'try to dispose of'))
489 | my_goal = self.beliefs.get(Goal(thing))
490 | assert my_goal is not None
491 |
492 | # here's where it gets a bit gnarly.
493 | # what do I believe the other believes?
494 | other_beliefs = self.believed_beliefs_of(other)
495 | # more specifically, what are their goals regarding the thing?
496 | other_goal = other_beliefs.get(Goal(thing))
497 |
498 | # they don't have one yet. tell them ours.
499 | if other_goal is None:
500 | self.speak_to(other,
501 | "'I really think we should %s <3>, <2>,' said <1>" % my_goal.phrase,
502 | [self, other, thing])
503 | # ok, they know what I think now:
504 | other.believed_beliefs_of(self).add(my_goal)
505 | elif other_goal.phrase == my_goal.phrase:
506 | # TODO make this an AgreementQuestion or smth
507 | self.question(other,
508 | ("'So we're agreed then, we should %s <3>?' asked <1>" %
509 | my_goal.phrase),
510 | [self, other, thing])
511 | # the other party might not've been aware that they agree
512 | other.believed_beliefs_of(self).add(my_goal)
513 | else: # WE DO NOT AGREE.
514 | if self.revolver.location == self:
515 | self.point_at(other, self.revolver)
516 | self.address(other,
517 | ThreatAgreeTopic(self, subject=thing),
518 | ("'I really feel *very* strongly that we should %s <3>, <2>,' said between clenched teeth" %
519 | my_goal.phrase),
520 | [self, other, thing])
521 | # just in case they weren't clued in yet
522 | other.believed_beliefs_of(self).add(my_goal)
523 | else:
524 | self.speak_to(other,
525 | ("'I don't think it would be a good idea to %s <3>, <2>,' said <1>" %
526 | my_goal.phrase),
527 | [self, other, thing])
528 | other.believed_beliefs_of(self).add(my_goal)
529 |
530 |
531 | class MaleCharacter(MasculineMixin, ProperMixin, Character):
532 | pass
533 |
534 |
535 | class FemaleCharacter(FeminineMixin, ProperMixin, Character):
536 | pass
537 |
--------------------------------------------------------------------------------