Merge lp:~divmod-dev/divmod.org/obtain-2824-6 into lp:divmod.org
- obtain-2824-6
- Merge into trunk
Proposed by
Glyph Lefkowitz
Status: | Merged |
---|---|
Merged at revision: | 2683 |
Proposed branch: | lp:~divmod-dev/divmod.org/obtain-2824-6 |
Merge into: | lp:divmod.org |
Diff against target: |
5589 lines (+3826/-806) 31 files modified
Epsilon/epsilon/remember.py (+39/-0) Epsilon/epsilon/test/test_remember.py (+90/-0) Imaginary/COMPATIBILITY.txt (+6/-0) Imaginary/ExampleGame/examplegame/furniture.py (+139/-0) Imaginary/ExampleGame/examplegame/glass.py (+72/-0) Imaginary/ExampleGame/examplegame/mice.py (+1/-1) Imaginary/ExampleGame/examplegame/squeaky.py (+42/-0) Imaginary/ExampleGame/examplegame/test/test_furniture.py (+107/-0) Imaginary/ExampleGame/examplegame/test/test_glass.py (+95/-0) Imaginary/ExampleGame/examplegame/test/test_squeaky.py (+51/-0) Imaginary/ExampleGame/examplegame/test/test_tether.py (+96/-0) Imaginary/ExampleGame/examplegame/tether.py (+121/-0) Imaginary/TODO.txt (+258/-0) Imaginary/imaginary/action.py (+261/-130) Imaginary/imaginary/creation.py (+3/-3) Imaginary/imaginary/events.py (+16/-11) Imaginary/imaginary/garments.py (+130/-17) Imaginary/imaginary/idea.py (+625/-0) Imaginary/imaginary/iimaginary.py (+313/-87) Imaginary/imaginary/language.py (+12/-0) Imaginary/imaginary/objects.py (+742/-248) Imaginary/imaginary/resources/motd (+1/-1) Imaginary/imaginary/test/commandutils.py (+11/-0) Imaginary/imaginary/test/test_actions.py (+43/-0) Imaginary/imaginary/test/test_container.py (+55/-0) Imaginary/imaginary/test/test_garments.py (+38/-20) Imaginary/imaginary/test/test_idea.py (+241/-0) Imaginary/imaginary/test/test_illumination.py (+159/-7) Imaginary/imaginary/test/test_objects.py (+43/-240) Imaginary/imaginary/test/test_player.py (+8/-19) Imaginary/imaginary/wiring/player.py (+8/-22) |
To merge this branch: | bzr merge lp:~divmod-dev/divmod.org/obtain-2824-6 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jean-Paul Calderone | Approve | ||
Review via email: mp+71631@code.launchpad.net |
Commit message
Description of the change
It makes obtain() better. this is basically what should be Imaginary trunk anyway; it changes a bunch of semantics, and improves a bunch of documentation.
To post a comment you must log in.
- 2679. By Jean-Paul Calderone
-
Put the _DisregardYourW
earingIt hack back in place. It is required for the tethering features in ExampleGame. This means worn clothing once again has a location of the wearer. - 2680. By Jean-Paul Calderone
-
There is no self
- 2681. By Jean-Paul Calderone
-
note about some uncovered code
- 2682. By Jean-Paul Calderone
-
Some minor doc fixes, and a note about Exit.link being confusing.
Revision history for this message
Glyph Lefkowitz (glyph) wrote : | # |
Once again, I apologize for the size of the branch. I'll try never to do that again :). Thanks for taking the time to attempt to comprehend this on multiple occasions, and thanks for landing it!
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'Epsilon/epsilon/remember.py' | |||
2 | --- Epsilon/epsilon/remember.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ Epsilon/epsilon/remember.py 2011-09-16 20:42:26 +0000 | |||
4 | @@ -0,0 +1,39 @@ | |||
5 | 1 | # -*- test-case-name: epsilon.test.test_remember -*- | ||
6 | 2 | |||
7 | 3 | """ | ||
8 | 4 | This module implements a utility for managing the lifecycle of attributes | ||
9 | 5 | related to a particular object. | ||
10 | 6 | """ | ||
11 | 7 | |||
12 | 8 | from epsilon.structlike import record | ||
13 | 9 | |||
14 | 10 | class remembered(record('creationFunction')): | ||
15 | 11 | """ | ||
16 | 12 | This descriptor decorator is applied to a function to create an attribute | ||
17 | 13 | which will be created on-demand, but remembered for the lifetime of the | ||
18 | 14 | instance to which it is attached. Subsequent accesses of the attribute | ||
19 | 15 | will return the remembered value. | ||
20 | 16 | |||
21 | 17 | @ivar creationFunction: the decorated function, to be called to create the | ||
22 | 18 | value. This should be a 1-argument callable, that takes only a 'self' | ||
23 | 19 | parameter, like a method. | ||
24 | 20 | """ | ||
25 | 21 | |||
26 | 22 | value = None | ||
27 | 23 | |||
28 | 24 | def __get__(self, oself, type): | ||
29 | 25 | """ | ||
30 | 26 | Retrieve the value if already cached, otherwise, call the | ||
31 | 27 | C{creationFunction} to create it. | ||
32 | 28 | """ | ||
33 | 29 | remembername = "_remembered_" + self.creationFunction.func_name | ||
34 | 30 | rememberedval = oself.__dict__.get(remembername, None) | ||
35 | 31 | if rememberedval is not None: | ||
36 | 32 | return rememberedval | ||
37 | 33 | rememberme = self.creationFunction(oself) | ||
38 | 34 | oself.__dict__[remembername] = rememberme | ||
39 | 35 | return rememberme | ||
40 | 36 | |||
41 | 37 | |||
42 | 38 | |||
43 | 39 | __all__ = ['remembered'] | ||
44 | 0 | 40 | ||
45 | === added file 'Epsilon/epsilon/test/test_remember.py' | |||
46 | --- Epsilon/epsilon/test/test_remember.py 1970-01-01 00:00:00 +0000 | |||
47 | +++ Epsilon/epsilon/test/test_remember.py 2011-09-16 20:42:26 +0000 | |||
48 | @@ -0,0 +1,90 @@ | |||
49 | 1 | |||
50 | 2 | from twisted.trial.unittest import TestCase | ||
51 | 3 | |||
52 | 4 | from epsilon.remember import remembered | ||
53 | 5 | from epsilon.structlike import record | ||
54 | 6 | |||
55 | 7 | class Rememberee(record("rememberer whichValue")): | ||
56 | 8 | """ | ||
57 | 9 | A sample value that holds on to its L{Rememberer}. | ||
58 | 10 | """ | ||
59 | 11 | |||
60 | 12 | |||
61 | 13 | class Rememberer(object): | ||
62 | 14 | """ | ||
63 | 15 | Sample application code which uses epsilon.remember. | ||
64 | 16 | |||
65 | 17 | @ivar invocations: The number of times that it is invoked. | ||
66 | 18 | """ | ||
67 | 19 | |||
68 | 20 | invocations = 0 | ||
69 | 21 | otherInvocations = 0 | ||
70 | 22 | |||
71 | 23 | @remembered | ||
72 | 24 | def value1(self): | ||
73 | 25 | """ | ||
74 | 26 | I remember a value. | ||
75 | 27 | """ | ||
76 | 28 | self.invocations += 1 | ||
77 | 29 | return Rememberee(self, 1) | ||
78 | 30 | |||
79 | 31 | |||
80 | 32 | @remembered | ||
81 | 33 | def value2(self): | ||
82 | 34 | """ | ||
83 | 35 | A separate value. | ||
84 | 36 | """ | ||
85 | 37 | self.otherInvocations += 1 | ||
86 | 38 | return Rememberee(self, 2) | ||
87 | 39 | |||
88 | 40 | |||
89 | 41 | class RememberedTests(TestCase): | ||
90 | 42 | """ | ||
91 | 43 | The "remembered" decorator allows you to lazily create an attribute and | ||
92 | 44 | remember it. | ||
93 | 45 | """ | ||
94 | 46 | |||
95 | 47 | def setUp(self): | ||
96 | 48 | """ | ||
97 | 49 | Create a L{Rememberer} for use with the tests. | ||
98 | 50 | """ | ||
99 | 51 | self.rememberer = Rememberer() | ||
100 | 52 | |||
101 | 53 | |||
102 | 54 | def test_selfArgument(self): | ||
103 | 55 | """ | ||
104 | 56 | The "self" argument to the decorated creation function will be the | ||
105 | 57 | instance the property is accessed upon. | ||
106 | 58 | """ | ||
107 | 59 | value = self.rememberer.value1 | ||
108 | 60 | self.assertIdentical(value.rememberer, self.rememberer) | ||
109 | 61 | |||
110 | 62 | |||
111 | 63 | def test_onlyOneInvocation(self): | ||
112 | 64 | """ | ||
113 | 65 | The callable wrapped by C{@remembered} will only be invoked once, | ||
114 | 66 | regardless of how many times the attribute is accessed. | ||
115 | 67 | """ | ||
116 | 68 | self.assertEquals(self.rememberer.invocations, 0) | ||
117 | 69 | firstTime = self.rememberer.value1 | ||
118 | 70 | self.assertEquals(self.rememberer.invocations, 1) | ||
119 | 71 | secondTime = self.rememberer.value1 | ||
120 | 72 | self.assertEquals(self.rememberer.invocations, 1) | ||
121 | 73 | self.assertIdentical(firstTime, secondTime) | ||
122 | 74 | |||
123 | 75 | |||
124 | 76 | def test_twoValues(self): | ||
125 | 77 | """ | ||
126 | 78 | If the L{@remembered} decorator is used more than once, each one will | ||
127 | 79 | be an attribute with its own identity. | ||
128 | 80 | """ | ||
129 | 81 | self.assertEquals(self.rememberer.invocations, 0) | ||
130 | 82 | self.assertEquals(self.rememberer.otherInvocations, 0) | ||
131 | 83 | firstValue1 = self.rememberer.value1 | ||
132 | 84 | self.assertEquals(self.rememberer.invocations, 1) | ||
133 | 85 | self.assertEquals(self.rememberer.otherInvocations, 0) | ||
134 | 86 | firstValue2 = self.rememberer.value2 | ||
135 | 87 | self.assertEquals(self.rememberer.otherInvocations, 1) | ||
136 | 88 | self.assertNotIdentical(firstValue1, firstValue2) | ||
137 | 89 | secondValue2 = self.rememberer.value2 | ||
138 | 90 | self.assertIdentical(firstValue2, secondValue2) | ||
139 | 0 | 91 | ||
140 | === added file 'Imaginary/COMPATIBILITY.txt' | |||
141 | --- Imaginary/COMPATIBILITY.txt 1970-01-01 00:00:00 +0000 | |||
142 | +++ Imaginary/COMPATIBILITY.txt 2011-09-16 20:42:26 +0000 | |||
143 | @@ -0,0 +1,6 @@ | |||
144 | 1 | Imaginary provides _no_ source-level compatibility from one release to the next. | ||
145 | 2 | |||
146 | 3 | Efforts will be made to provide database level compatibility (i.e. data from | ||
147 | 4 | one release can be loaded with the next). However, although we will try to | ||
148 | 5 | ensure that data will load, there is no guarantee that it will be meaningfully | ||
149 | 6 | upgraded yet. | ||
150 | 0 | 7 | ||
151 | === added file 'Imaginary/ExampleGame/examplegame/furniture.py' | |||
152 | --- Imaginary/ExampleGame/examplegame/furniture.py 1970-01-01 00:00:00 +0000 | |||
153 | +++ Imaginary/ExampleGame/examplegame/furniture.py 2011-09-16 20:42:26 +0000 | |||
154 | @@ -0,0 +1,139 @@ | |||
155 | 1 | # -*- test-case-name: examplegame.test.test_furniture -*- | ||
156 | 2 | |||
157 | 3 | """ | ||
158 | 4 | |||
159 | 5 | Furniture is the mass noun for the movable objects which may support the | ||
160 | 6 | human body (seating furniture and beds), provide storage, or hold objects | ||
161 | 7 | on horizontal surfaces above the ground. | ||
162 | 8 | |||
163 | 9 | -- Wikipedia, http://en.wikipedia.org/wiki/Furniture | ||
164 | 10 | |||
165 | 11 | L{imaginary.furniture} contains L{Action}s which allow players to interact with | ||
166 | 12 | household objects such as chairs and tables, and L{Enhancement}s which allow | ||
167 | 13 | L{Thing}s to behave as same. | ||
168 | 14 | |||
169 | 15 | This has the same implementation weakness as L{examplegame.tether}, in that it | ||
170 | 16 | needs to make some assumptions about who is moving what in its restrictions of | ||
171 | 17 | movement; it should be moved into imaginary proper when that can be properly | ||
172 | 18 | addressed. It's also a bit too draconian in terms of preventing the player | ||
173 | 19 | from moving for any reason just because they're seated. However, it's a | ||
174 | 20 | workable approximation for some uses, and thus useful as an example. | ||
175 | 21 | """ | ||
176 | 22 | |||
177 | 23 | from zope.interface import implements | ||
178 | 24 | |||
179 | 25 | from axiom.item import Item | ||
180 | 26 | from axiom.attributes import reference | ||
181 | 27 | |||
182 | 28 | from imaginary.iimaginary import ISittable, IContainer, IMovementRestriction | ||
183 | 29 | from imaginary.eimaginary import ActionFailure | ||
184 | 30 | from imaginary.events import ThatDoesntWork | ||
185 | 31 | from imaginary.language import Noun | ||
186 | 32 | from imaginary.action import Action, TargetAction | ||
187 | 33 | from imaginary.events import Success | ||
188 | 34 | from imaginary.enhancement import Enhancement | ||
189 | 35 | from imaginary.objects import Container | ||
190 | 36 | from imaginary.pyparsing import Literal, Optional, restOfLine | ||
191 | 37 | |||
192 | 38 | class Sit(TargetAction): | ||
193 | 39 | """ | ||
194 | 40 | An action allowing a player to sit down in a chair. | ||
195 | 41 | """ | ||
196 | 42 | expr = (Literal("sit") + Optional(Literal("on")) + | ||
197 | 43 | restOfLine.setResultsName("target")) | ||
198 | 44 | |||
199 | 45 | targetInterface = ISittable | ||
200 | 46 | |||
201 | 47 | def do(self, player, line, target): | ||
202 | 48 | """ | ||
203 | 49 | Do the action; sit down. | ||
204 | 50 | """ | ||
205 | 51 | target.seat(player) | ||
206 | 52 | |||
207 | 53 | actorMessage=["You sit in ", | ||
208 | 54 | Noun(target.thing).definiteNounPhrase(),"."] | ||
209 | 55 | otherMessage=[player.thing, " sits in ", | ||
210 | 56 | Noun(target.thing).definiteNounPhrase(),"."] | ||
211 | 57 | Success(actor=player.thing, location=player.thing.location, | ||
212 | 58 | actorMessage=actorMessage, | ||
213 | 59 | otherMessage=otherMessage).broadcast() | ||
214 | 60 | |||
215 | 61 | |||
216 | 62 | class Stand(Action): | ||
217 | 63 | """ | ||
218 | 64 | Stand up from a sitting position. | ||
219 | 65 | """ | ||
220 | 66 | expr = (Literal("stand") + Optional(Literal("up"))) | ||
221 | 67 | |||
222 | 68 | def do(self, player, line): | ||
223 | 69 | """ | ||
224 | 70 | Do the action; stand up. | ||
225 | 71 | """ | ||
226 | 72 | # XXX This is wrong. I should be issuing an obtain() query to find | ||
227 | 73 | # something that qualifies as "my location" or "the thing I'm already | ||
228 | 74 | # sitting in". | ||
229 | 75 | chair = ISittable(player.thing.location, None) | ||
230 | 76 | if chair is None: | ||
231 | 77 | raise ActionFailure(ThatDoesntWork( | ||
232 | 78 | actor=player.thing, | ||
233 | 79 | actorMessage=["You're already standing."])) | ||
234 | 80 | chair.unseat(player) | ||
235 | 81 | Success(actor=player.thing, location=player.thing.location, | ||
236 | 82 | actorMessage=["You stand up."], | ||
237 | 83 | otherMessage=[player.thing, " stands up."]).broadcast() | ||
238 | 84 | |||
239 | 85 | |||
240 | 86 | |||
241 | 87 | class Chair(Enhancement, Item): | ||
242 | 88 | """ | ||
243 | 89 | A chair is a thing you can sit in. | ||
244 | 90 | """ | ||
245 | 91 | |||
246 | 92 | implements(ISittable, IMovementRestriction) | ||
247 | 93 | |||
248 | 94 | powerupInterfaces = [ISittable] | ||
249 | 95 | |||
250 | 96 | thing = reference() | ||
251 | 97 | container = reference() | ||
252 | 98 | |||
253 | 99 | |||
254 | 100 | def movementImminent(self, movee, destination): | ||
255 | 101 | """ | ||
256 | 102 | A player tried to move while they were seated. Prevent them from doing | ||
257 | 103 | so, noting that they must stand first. | ||
258 | 104 | |||
259 | 105 | (Assume the player was trying to move themselves, although there's no | ||
260 | 106 | way to know currently.) | ||
261 | 107 | """ | ||
262 | 108 | raise ActionFailure(ThatDoesntWork( | ||
263 | 109 | actor=movee, | ||
264 | 110 | actorMessage=u"You can't do that while sitting down.")) | ||
265 | 111 | |||
266 | 112 | |||
267 | 113 | def applyEnhancement(self): | ||
268 | 114 | """ | ||
269 | 115 | Apply this enhancement to this L{Chair}'s thing, creating a | ||
270 | 116 | L{Container} to hold the seated player, if necessary. | ||
271 | 117 | """ | ||
272 | 118 | super(Chair, self).applyEnhancement() | ||
273 | 119 | container = IContainer(self.thing, None) | ||
274 | 120 | if container is None: | ||
275 | 121 | container = Container.createFor(self.thing, capacity=300) | ||
276 | 122 | self.container = container | ||
277 | 123 | |||
278 | 124 | |||
279 | 125 | def seat(self, player): | ||
280 | 126 | """ | ||
281 | 127 | The player sat down on this chair; place them into it and prevent them | ||
282 | 128 | from moving elsewhere until they stand up. | ||
283 | 129 | """ | ||
284 | 130 | player.thing.moveTo(self.container) | ||
285 | 131 | player.thing.powerUp(self, IMovementRestriction) | ||
286 | 132 | |||
287 | 133 | |||
288 | 134 | def unseat(self, player): | ||
289 | 135 | """ | ||
290 | 136 | The player stood up; remove them from this chair. | ||
291 | 137 | """ | ||
292 | 138 | player.thing.powerDown(self, IMovementRestriction) | ||
293 | 139 | player.thing.moveTo(self.container.thing.location) | ||
294 | 0 | 140 | ||
295 | === added file 'Imaginary/ExampleGame/examplegame/glass.py' | |||
296 | --- Imaginary/ExampleGame/examplegame/glass.py 1970-01-01 00:00:00 +0000 | |||
297 | +++ Imaginary/ExampleGame/examplegame/glass.py 2011-09-16 20:42:26 +0000 | |||
298 | @@ -0,0 +1,72 @@ | |||
299 | 1 | # -*- test-case-name: examplegame.test.test_glass -*- | ||
300 | 2 | """ | ||
301 | 3 | This example implements a transparent container that you can see, but not | ||
302 | 4 | reach, the contents of. | ||
303 | 5 | """ | ||
304 | 6 | from zope.interface import implements | ||
305 | 7 | |||
306 | 8 | from axiom.item import Item | ||
307 | 9 | from axiom.attributes import reference | ||
308 | 10 | |||
309 | 11 | from imaginary.iimaginary import ( | ||
310 | 12 | ILinkContributor, IWhyNot, IObstruction, IContainer) | ||
311 | 13 | from imaginary.enhancement import Enhancement | ||
312 | 14 | from imaginary.objects import ContainmentRelationship | ||
313 | 15 | from imaginary.idea import Link | ||
314 | 16 | |||
315 | 17 | class _CantReachThroughGlassBox(object): | ||
316 | 18 | """ | ||
317 | 19 | This object provides an explanation for why the user cannot access a target | ||
318 | 20 | that is inside a L{imaginary.objects.Thing} enhanced with a L{GlassBox}. | ||
319 | 21 | """ | ||
320 | 22 | implements(IWhyNot) | ||
321 | 23 | |||
322 | 24 | def tellMeWhyNot(self): | ||
323 | 25 | """ | ||
324 | 26 | Return a simple message explaining that the user can't reach through | ||
325 | 27 | the glass box. | ||
326 | 28 | """ | ||
327 | 29 | return "You can't reach through the glass box." | ||
328 | 30 | |||
329 | 31 | |||
330 | 32 | |||
331 | 33 | class _ObstructedByGlass(object): | ||
332 | 34 | """ | ||
333 | 35 | This is an annotation on a link between two objects which represents a | ||
334 | 36 | physical obstruction between them. It is used to annotate between a | ||
335 | 37 | L{GlassBox} and its contents, so you can see them without reaching them. | ||
336 | 38 | """ | ||
337 | 39 | implements(IObstruction) | ||
338 | 40 | |||
339 | 41 | def whyNot(self): | ||
340 | 42 | """ | ||
341 | 43 | @return: an object which explains why you can't reach through the glass | ||
342 | 44 | box. | ||
343 | 45 | """ | ||
344 | 46 | return _CantReachThroughGlassBox() | ||
345 | 47 | |||
346 | 48 | |||
347 | 49 | |||
348 | 50 | class GlassBox(Item, Enhancement): | ||
349 | 51 | """ | ||
350 | 52 | L{GlassBox} is an L{Enhancement} which modifies a container such that it is | ||
351 | 53 | contained. | ||
352 | 54 | """ | ||
353 | 55 | |||
354 | 56 | powerupInterfaces = (ILinkContributor,) | ||
355 | 57 | |||
356 | 58 | thing = reference() | ||
357 | 59 | |||
358 | 60 | def links(self): | ||
359 | 61 | """ | ||
360 | 62 | If the container attached to this L{GlassBox}'s C{thing} is closed, | ||
361 | 63 | yield its list of contents with each link annotated with | ||
362 | 64 | L{_ObstructedByGlass}, indicating that the object cannot be reached. | ||
363 | 65 | """ | ||
364 | 66 | container = IContainer(self.thing) | ||
365 | 67 | if container.closed: | ||
366 | 68 | for content in container.getContents(): | ||
367 | 69 | link = Link(self.thing.idea, content.idea) | ||
368 | 70 | link.annotate([_ObstructedByGlass(), | ||
369 | 71 | ContainmentRelationship(container)]) | ||
370 | 72 | yield link | ||
371 | 0 | 73 | ||
372 | === modified file 'Imaginary/ExampleGame/examplegame/mice.py' | |||
373 | --- Imaginary/ExampleGame/examplegame/mice.py 2009-06-29 04:03:17 +0000 | |||
374 | +++ Imaginary/ExampleGame/examplegame/mice.py 2011-09-16 20:42:26 +0000 | |||
375 | @@ -242,7 +242,7 @@ | |||
376 | 242 | character = random.choice(japanese.hiragana.keys()) | 242 | character = random.choice(japanese.hiragana.keys()) |
377 | 243 | self._currentChallenge = character | 243 | self._currentChallenge = character |
378 | 244 | actor = self._actor() | 244 | actor = self._actor() |
380 | 245 | action.Say().do(actor.thing, None, character) | 245 | action.Say().do(actor, None, character) |
381 | 246 | 246 | ||
382 | 247 | 247 | ||
383 | 248 | 248 | ||
384 | 249 | 249 | ||
385 | === added file 'Imaginary/ExampleGame/examplegame/squeaky.py' | |||
386 | --- Imaginary/ExampleGame/examplegame/squeaky.py 1970-01-01 00:00:00 +0000 | |||
387 | +++ Imaginary/ExampleGame/examplegame/squeaky.py 2011-09-16 20:42:26 +0000 | |||
388 | @@ -0,0 +1,42 @@ | |||
389 | 1 | # -*- test-case-name: examplegame.test.test_squeaky -*- | ||
390 | 2 | |||
391 | 3 | """ | ||
392 | 4 | This module implements an L{ILinkAnnotator} which causes an object to squeak | ||
393 | 5 | when it is moved. It should serve as a simple example for overriding what | ||
394 | 6 | happens when an action is executed (in this case, 'take' and 'drop'). | ||
395 | 7 | """ | ||
396 | 8 | |||
397 | 9 | from zope.interface import implements | ||
398 | 10 | |||
399 | 11 | from axiom.item import Item | ||
400 | 12 | from axiom.attributes import reference | ||
401 | 13 | |||
402 | 14 | from imaginary.iimaginary import IMovementRestriction, IConcept | ||
403 | 15 | from imaginary.events import Success | ||
404 | 16 | from imaginary.enhancement import Enhancement | ||
405 | 17 | from imaginary.objects import Thing | ||
406 | 18 | |||
407 | 19 | |||
408 | 20 | class Squeaker(Item, Enhancement): | ||
409 | 21 | """ | ||
410 | 22 | This is an L{Enhancement} which, when installed on a L{Thing}, causes that | ||
411 | 23 | L{Thing} to squeak when you pick it up. | ||
412 | 24 | """ | ||
413 | 25 | |||
414 | 26 | implements(IMovementRestriction) | ||
415 | 27 | |||
416 | 28 | powerupInterfaces = [IMovementRestriction] | ||
417 | 29 | |||
418 | 30 | thing = reference(allowNone=False, | ||
419 | 31 | whenDeleted=reference.CASCADE, | ||
420 | 32 | reftype=Thing) | ||
421 | 33 | |||
422 | 34 | |||
423 | 35 | def movementImminent(self, movee, destination): | ||
424 | 36 | """ | ||
425 | 37 | The object enhanced by this L{Squeaker} is about to move - emit a | ||
426 | 38 | L{Success} event which describes its squeak. | ||
427 | 39 | """ | ||
428 | 40 | Success(otherMessage=(IConcept(self.thing).capitalizeConcept(), | ||
429 | 41 | " emits a faint squeak."), | ||
430 | 42 | location=self.thing.location).broadcast() | ||
431 | 0 | 43 | ||
432 | === added file 'Imaginary/ExampleGame/examplegame/test/test_furniture.py' | |||
433 | --- Imaginary/ExampleGame/examplegame/test/test_furniture.py 1970-01-01 00:00:00 +0000 | |||
434 | +++ Imaginary/ExampleGame/examplegame/test/test_furniture.py 2011-09-16 20:42:26 +0000 | |||
435 | @@ -0,0 +1,107 @@ | |||
436 | 1 | |||
437 | 2 | """ | ||
438 | 3 | This module contains tests for the examplegame.furniture module. | ||
439 | 4 | """ | ||
440 | 5 | |||
441 | 6 | from twisted.trial.unittest import TestCase | ||
442 | 7 | |||
443 | 8 | from imaginary.test.commandutils import CommandTestCaseMixin, E | ||
444 | 9 | |||
445 | 10 | from imaginary.objects import Thing, Container, Exit | ||
446 | 11 | from examplegame.furniture import Chair | ||
447 | 12 | |||
448 | 13 | class SitAndStandTests(CommandTestCaseMixin, TestCase): | ||
449 | 14 | """ | ||
450 | 15 | Tests for the 'sit' and 'stand' actions. | ||
451 | 16 | """ | ||
452 | 17 | |||
453 | 18 | def setUp(self): | ||
454 | 19 | """ | ||
455 | 20 | Create a room, with a dude in it, and a chair he can sit in. | ||
456 | 21 | """ | ||
457 | 22 | CommandTestCaseMixin.setUp(self) | ||
458 | 23 | self.chairThing = Thing(store=self.store, name=u"chair") | ||
459 | 24 | self.chairThing.moveTo(self.location) | ||
460 | 25 | self.chair = Chair.createFor(self.chairThing) | ||
461 | 26 | |||
462 | 27 | |||
463 | 28 | def test_sitDown(self): | ||
464 | 29 | """ | ||
465 | 30 | Sitting in a chair should move your location to that chair. | ||
466 | 31 | """ | ||
467 | 32 | self.assertCommandOutput( | ||
468 | 33 | "sit chair", | ||
469 | 34 | ["You sit in the chair."], | ||
470 | 35 | ["Test Player sits in the chair."]) | ||
471 | 36 | self.assertEquals(self.player.location, self.chair.thing) | ||
472 | 37 | |||
473 | 38 | |||
474 | 39 | def test_standWhenStanding(self): | ||
475 | 40 | """ | ||
476 | 41 | You can't stand up - you're already standing up. | ||
477 | 42 | """ | ||
478 | 43 | self.assertCommandOutput( | ||
479 | 44 | "stand up", | ||
480 | 45 | ["You're already standing."]) | ||
481 | 46 | |||
482 | 47 | |||
483 | 48 | def test_standWhenSitting(self): | ||
484 | 49 | """ | ||
485 | 50 | If a player stands up when sitting in a chair, they should be seen to | ||
486 | 51 | stand up, and they should be placed back into the room where the chair | ||
487 | 52 | is located. | ||
488 | 53 | """ | ||
489 | 54 | self.test_sitDown() | ||
490 | 55 | self.assertCommandOutput( | ||
491 | 56 | "stand up", | ||
492 | 57 | ["You stand up."], | ||
493 | 58 | ["Test Player stands up."]) | ||
494 | 59 | self.assertEquals(self.player.location, self.location) | ||
495 | 60 | |||
496 | 61 | |||
497 | 62 | def test_takeWhenSitting(self): | ||
498 | 63 | """ | ||
499 | 64 | When a player is seated, they should still be able to take objects on | ||
500 | 65 | the floor around them. | ||
501 | 66 | """ | ||
502 | 67 | self.test_sitDown() | ||
503 | 68 | self.ball = Thing(store=self.store, name=u'ball') | ||
504 | 69 | self.ball.moveTo(self.location) | ||
505 | 70 | self.assertCommandOutput( | ||
506 | 71 | "take ball", | ||
507 | 72 | ["You take a ball."], | ||
508 | 73 | ["Test Player takes a ball."]) | ||
509 | 74 | |||
510 | 75 | |||
511 | 76 | def test_moveWhenSitting(self): | ||
512 | 77 | """ | ||
513 | 78 | A player who is sitting shouldn't be able to move without standing up | ||
514 | 79 | first. | ||
515 | 80 | """ | ||
516 | 81 | self.test_sitDown() | ||
517 | 82 | otherRoom = Thing(store=self.store, name=u'elsewhere') | ||
518 | 83 | Container.createFor(otherRoom, capacity=1000) | ||
519 | 84 | Exit.link(self.location, otherRoom, u'north') | ||
520 | 85 | self.assertCommandOutput( | ||
521 | 86 | "go north", | ||
522 | 87 | ["You can't do that while sitting down."]) | ||
523 | 88 | self.assertCommandOutput( | ||
524 | 89 | "go south", | ||
525 | 90 | ["You can't go that way."]) | ||
526 | 91 | |||
527 | 92 | |||
528 | 93 | def test_lookWhenSitting(self): | ||
529 | 94 | """ | ||
530 | 95 | Looking around when sitting should display the description of the room. | ||
531 | 96 | """ | ||
532 | 97 | self.test_sitDown() | ||
533 | 98 | self.assertCommandOutput( | ||
534 | 99 | "look", | ||
535 | 100 | # I'd like to add ', in the chair' to this test, but there's | ||
536 | 101 | # currently no way to modify the name of the object being looked | ||
537 | 102 | # at. | ||
538 | 103 | [E("[ Test Location ]"), | ||
539 | 104 | "Location for testing.", | ||
540 | 105 | "Observer Player and a chair"]) | ||
541 | 106 | |||
542 | 107 | |||
543 | 0 | 108 | ||
544 | === added file 'Imaginary/ExampleGame/examplegame/test/test_glass.py' | |||
545 | --- Imaginary/ExampleGame/examplegame/test/test_glass.py 1970-01-01 00:00:00 +0000 | |||
546 | +++ Imaginary/ExampleGame/examplegame/test/test_glass.py 2011-09-16 20:42:26 +0000 | |||
547 | @@ -0,0 +1,95 @@ | |||
548 | 1 | |||
549 | 2 | """ | ||
550 | 3 | Tests for L{examplegame.glass} | ||
551 | 4 | """ | ||
552 | 5 | |||
553 | 6 | from twisted.trial.unittest import TestCase | ||
554 | 7 | |||
555 | 8 | from imaginary.test.commandutils import CommandTestCaseMixin, E | ||
556 | 9 | |||
557 | 10 | from imaginary.objects import Thing, Container | ||
558 | 11 | |||
559 | 12 | from examplegame.glass import GlassBox | ||
560 | 13 | |||
561 | 14 | class GlassBoxTests(CommandTestCaseMixin, TestCase): | ||
562 | 15 | """ | ||
563 | 16 | Tests for L{GlassBox} | ||
564 | 17 | """ | ||
565 | 18 | |||
566 | 19 | def setUp(self): | ||
567 | 20 | """ | ||
568 | 21 | Create a room with a L{GlassBox} in it, which itself contains a ball. | ||
569 | 22 | """ | ||
570 | 23 | CommandTestCaseMixin.setUp(self) | ||
571 | 24 | self.box = Thing(store=self.store, name=u'box', | ||
572 | 25 | description=u'The system under test.') | ||
573 | 26 | self.ball = Thing(store=self.store, name=u'ball', | ||
574 | 27 | description=u'an interesting object') | ||
575 | 28 | self.container = Container.createFor(self.box) | ||
576 | 29 | GlassBox.createFor(self.box) | ||
577 | 30 | self.ball.moveTo(self.box) | ||
578 | 31 | self.box.moveTo(self.location) | ||
579 | 32 | self.container.closed = True | ||
580 | 33 | |||
581 | 34 | |||
582 | 35 | def test_lookThrough(self): | ||
583 | 36 | """ | ||
584 | 37 | You can see items within a glass box by looking at them directly. | ||
585 | 38 | """ | ||
586 | 39 | self.assertCommandOutput( | ||
587 | 40 | "look at ball", | ||
588 | 41 | [E("[ ball ]"), | ||
589 | 42 | "an interesting object"]) | ||
590 | 43 | |||
591 | 44 | |||
592 | 45 | def test_lookAt(self): | ||
593 | 46 | """ | ||
594 | 47 | You can see the contents within a glass box by looking at the box. | ||
595 | 48 | """ | ||
596 | 49 | self.assertCommandOutput( | ||
597 | 50 | "look at box", | ||
598 | 51 | [E("[ box ]"), | ||
599 | 52 | "The system under test.", | ||
600 | 53 | "a ball"]) | ||
601 | 54 | |||
602 | 55 | |||
603 | 56 | def test_take(self): | ||
604 | 57 | """ | ||
605 | 58 | You can't take items within a glass box. | ||
606 | 59 | """ | ||
607 | 60 | self.assertCommandOutput( | ||
608 | 61 | "get ball", | ||
609 | 62 | ["You can't reach through the glass box."]) | ||
610 | 63 | |||
611 | 64 | |||
612 | 65 | def test_openTake(self): | ||
613 | 66 | """ | ||
614 | 67 | Taking items from a glass box should work if it's open. | ||
615 | 68 | """ | ||
616 | 69 | self.container.closed = False | ||
617 | 70 | self.assertCommandOutput( | ||
618 | 71 | "get ball", | ||
619 | 72 | ["You take a ball."], | ||
620 | 73 | ["Test Player takes a ball."]) | ||
621 | 74 | |||
622 | 75 | |||
623 | 76 | def test_put(self): | ||
624 | 77 | """ | ||
625 | 78 | You can't put items into a glass box. | ||
626 | 79 | """ | ||
627 | 80 | self.container.closed = False | ||
628 | 81 | self.ball.moveTo(self.location) | ||
629 | 82 | self.container.closed = True | ||
630 | 83 | self.assertCommandOutput( | ||
631 | 84 | "put ball in box", | ||
632 | 85 | ["The box is closed."]) | ||
633 | 86 | |||
634 | 87 | |||
635 | 88 | def test_whyNot(self): | ||
636 | 89 | """ | ||
637 | 90 | A regression test; there was a bug where glass boxes would interfere | ||
638 | 91 | with normal target-acquisition error reporting. | ||
639 | 92 | """ | ||
640 | 93 | self.assertCommandOutput( | ||
641 | 94 | "get foobar", | ||
642 | 95 | ["Nothing like that around here."]) | ||
643 | 0 | 96 | ||
644 | === added file 'Imaginary/ExampleGame/examplegame/test/test_squeaky.py' | |||
645 | --- Imaginary/ExampleGame/examplegame/test/test_squeaky.py 1970-01-01 00:00:00 +0000 | |||
646 | +++ Imaginary/ExampleGame/examplegame/test/test_squeaky.py 2011-09-16 20:42:26 +0000 | |||
647 | @@ -0,0 +1,51 @@ | |||
648 | 1 | |||
649 | 2 | from twisted.trial.unittest import TestCase | ||
650 | 3 | |||
651 | 4 | from imaginary.test.commandutils import CommandTestCaseMixin | ||
652 | 5 | |||
653 | 6 | from imaginary.objects import Thing, Container | ||
654 | 7 | |||
655 | 8 | from examplegame.squeaky import Squeaker | ||
656 | 9 | |||
657 | 10 | class SqueakTest(CommandTestCaseMixin, TestCase): | ||
658 | 11 | """ | ||
659 | 12 | Squeak Test. | ||
660 | 13 | """ | ||
661 | 14 | |||
662 | 15 | def setUp(self): | ||
663 | 16 | """ | ||
664 | 17 | Set Up. | ||
665 | 18 | """ | ||
666 | 19 | CommandTestCaseMixin.setUp(self) | ||
667 | 20 | self.squeaker = Thing(store=self.store, name=u"squeaker") | ||
668 | 21 | self.squeaker.moveTo(self.location) | ||
669 | 22 | self.squeakification = Squeaker.createFor(self.squeaker) | ||
670 | 23 | |||
671 | 24 | |||
672 | 25 | def test_itSqueaks(self): | ||
673 | 26 | """ | ||
674 | 27 | Picking up a squeaky thing makes it emit a squeak. | ||
675 | 28 | """ | ||
676 | 29 | self.assertCommandOutput( | ||
677 | 30 | "take squeaker", | ||
678 | 31 | ["You take a squeaker.", | ||
679 | 32 | "A squeaker emits a faint squeak."], | ||
680 | 33 | ["Test Player takes a squeaker.", | ||
681 | 34 | "A squeaker emits a faint squeak."]) | ||
682 | 35 | |||
683 | 36 | |||
684 | 37 | def test_squeakyContainer(self): | ||
685 | 38 | """ | ||
686 | 39 | If a container is squeaky, that shouldn't interfere with its function | ||
687 | 40 | as a container. (i.e. let's make sure that links keep working even | ||
688 | 41 | though we're using an annotator here.) | ||
689 | 42 | """ | ||
690 | 43 | cont = Container.createFor(self.squeaker) | ||
691 | 44 | |||
692 | 45 | mcguffin = Thing(store=self.store, name=u"mcguffin") | ||
693 | 46 | mcguffin.moveTo(cont) | ||
694 | 47 | |||
695 | 48 | self.assertCommandOutput( | ||
696 | 49 | "take mcguffin from squeaker", | ||
697 | 50 | ["You take a mcguffin from the squeaker."], | ||
698 | 51 | ["Test Player takes a mcguffin from the squeaker."]) | ||
699 | 0 | 52 | ||
700 | === added file 'Imaginary/ExampleGame/examplegame/test/test_tether.py' | |||
701 | --- Imaginary/ExampleGame/examplegame/test/test_tether.py 1970-01-01 00:00:00 +0000 | |||
702 | +++ Imaginary/ExampleGame/examplegame/test/test_tether.py 2011-09-16 20:42:26 +0000 | |||
703 | @@ -0,0 +1,96 @@ | |||
704 | 1 | |||
705 | 2 | from twisted.trial.unittest import TestCase | ||
706 | 3 | |||
707 | 4 | from imaginary.test.commandutils import CommandTestCaseMixin, E | ||
708 | 5 | |||
709 | 6 | from imaginary.objects import Thing, Container, Exit | ||
710 | 7 | from imaginary.garments import Garment | ||
711 | 8 | |||
712 | 9 | from examplegame.furniture import Chair | ||
713 | 10 | from examplegame.tether import Tether | ||
714 | 11 | |||
715 | 12 | class TetherTest(CommandTestCaseMixin, TestCase): | ||
716 | 13 | """ | ||
717 | 14 | A test for tethering an item to its location, such that a player who picks | ||
718 | 15 | it up can't leave until they drop it. | ||
719 | 16 | """ | ||
720 | 17 | |||
721 | 18 | def setUp(self): | ||
722 | 19 | """ | ||
723 | 20 | Tether a ball to the room. | ||
724 | 21 | """ | ||
725 | 22 | CommandTestCaseMixin.setUp(self) | ||
726 | 23 | self.ball = Thing(store=self.store, name=u'ball') | ||
727 | 24 | self.ball.moveTo(self.location) | ||
728 | 25 | self.tether = Tether.createFor(self.ball, to=self.location) | ||
729 | 26 | self.otherPlace = Thing(store=self.store, name=u'elsewhere') | ||
730 | 27 | Container.createFor(self.otherPlace, capacity=1000) | ||
731 | 28 | Exit.link(self.location, self.otherPlace, u'north') | ||
732 | 29 | |||
733 | 30 | |||
734 | 31 | def test_takeAndLeave(self): | ||
735 | 32 | """ | ||
736 | 33 | You can't leave the room if you're holding the ball that's tied to it. | ||
737 | 34 | """ | ||
738 | 35 | self.assertCommandOutput( | ||
739 | 36 | "take ball", | ||
740 | 37 | ["You take a ball."], | ||
741 | 38 | ["Test Player takes a ball."]) | ||
742 | 39 | self.assertCommandOutput( | ||
743 | 40 | "go north", | ||
744 | 41 | ["You can't move, you're still holding a ball."], | ||
745 | 42 | ["Test Player struggles with a ball."]) | ||
746 | 43 | self.assertCommandOutput( | ||
747 | 44 | "drop ball", | ||
748 | 45 | ["You drop the ball."], | ||
749 | 46 | ["Test Player drops a ball."]) | ||
750 | 47 | self.assertCommandOutput( | ||
751 | 48 | "go north", | ||
752 | 49 | [E("[ elsewhere ]"), | ||
753 | 50 | E("( south )"), | ||
754 | 51 | ""], | ||
755 | 52 | ["Test Player leaves north."]) | ||
756 | 53 | |||
757 | 54 | |||
758 | 55 | def test_allTiedUp(self): | ||
759 | 56 | """ | ||
760 | 57 | If you're tied to a chair, you can't leave. | ||
761 | 58 | """ | ||
762 | 59 | chairThing = Thing(store=self.store, name=u'chair') | ||
763 | 60 | chairThing.moveTo(self.location) | ||
764 | 61 | chair = Chair.createFor(chairThing) | ||
765 | 62 | self.assertCommandOutput("sit chair", | ||
766 | 63 | ["You sit in the chair."], | ||
767 | 64 | ["Test Player sits in the chair."]) | ||
768 | 65 | Tether.createFor(self.player, to=chairThing) | ||
769 | 66 | self.assertCommandOutput( | ||
770 | 67 | "stand up", | ||
771 | 68 | ["You can't move, you're tied to a chair."], | ||
772 | 69 | ["Test Player struggles."]) | ||
773 | 70 | |||
774 | 71 | |||
775 | 72 | def test_tetheredClothing(self): | ||
776 | 73 | """ | ||
777 | 74 | Clothing that is tethered will also prevent movement if you wear it. | ||
778 | 75 | |||
779 | 76 | This isn't just simply a test for clothing; it's an example of | ||
780 | 77 | integrating with a foreign system which doesn't know about tethering, | ||
781 | 78 | but can move objects itself. | ||
782 | 79 | |||
783 | 80 | Tethering should I{not} have any custom logic related to clothing to | ||
784 | 81 | make this test pass; if it does get custom clothing code for some | ||
785 | 82 | reason, more tests should be added to deal with other systems that do | ||
786 | 83 | not take tethering into account (and vice versa). | ||
787 | 84 | """ | ||
788 | 85 | Garment.createFor(self.ball, garmentDescription=u"A lovely ball.", | ||
789 | 86 | garmentSlots=[u"head"]) | ||
790 | 87 | self.assertCommandOutput( | ||
791 | 88 | "wear ball", | ||
792 | 89 | ["You put on the ball."], | ||
793 | 90 | ["Test Player puts on a ball."]) | ||
794 | 91 | self.assertCommandOutput( | ||
795 | 92 | "go north", | ||
796 | 93 | ["You can't move, you're still holding a ball."], | ||
797 | 94 | ["Test Player struggles with a ball."]) | ||
798 | 95 | |||
799 | 96 | |||
800 | 0 | 97 | ||
801 | === added file 'Imaginary/ExampleGame/examplegame/tether.py' | |||
802 | --- Imaginary/ExampleGame/examplegame/tether.py 1970-01-01 00:00:00 +0000 | |||
803 | +++ Imaginary/ExampleGame/examplegame/tether.py 2011-09-16 20:42:26 +0000 | |||
804 | @@ -0,0 +1,121 @@ | |||
805 | 1 | # -*- test-case-name: examplegame.test.test_tether -*- | ||
806 | 2 | |||
807 | 3 | """ | ||
808 | 4 | A simplistic implementation of tethering, which demonstrates how to prevent | ||
809 | 5 | someone from moving around. | ||
810 | 6 | |||
811 | 7 | This implementation is somewhat limited, as it assumes that tethered objects | ||
812 | 8 | can only be located in players' inventories and on the ground. It also makes | ||
813 | 9 | several assumptions about who is actually doing the moving in moveTo; in order | ||
814 | 10 | to be really correct, the implementation of movement needs to relay more | ||
815 | 11 | information about what is moving and how. | ||
816 | 12 | """ | ||
817 | 13 | |||
818 | 14 | from zope.interface import implements | ||
819 | 15 | |||
820 | 16 | from axiom.item import Item | ||
821 | 17 | from axiom.attributes import reference | ||
822 | 18 | |||
823 | 19 | from imaginary.iimaginary import IMovementRestriction, IActor | ||
824 | 20 | from imaginary.eimaginary import ActionFailure | ||
825 | 21 | from imaginary.events import ThatDoesntWork | ||
826 | 22 | from imaginary.enhancement import Enhancement | ||
827 | 23 | from imaginary.objects import Thing | ||
828 | 24 | |||
829 | 25 | |||
830 | 26 | class Tether(Item, Enhancement): | ||
831 | 27 | """ | ||
832 | 28 | I am a force that binds two objects together. | ||
833 | 29 | |||
834 | 30 | Right now this force isn't symmetric; the idea is that the thing that we | ||
835 | 31 | are tethered 'to' is immovable for some other reason. This is why we're in | ||
836 | 32 | the example rather than a real robust piece of game-library functionality | ||
837 | 33 | in imaginary proper. | ||
838 | 34 | |||
839 | 35 | The C{thing} that we are installed on is prevented from moving more than a | ||
840 | 36 | certain distance away from the thing it is tethered C{to}. | ||
841 | 37 | |||
842 | 38 | This is accomplished by preventing movement of the object's container; | ||
843 | 39 | i.e. if you pick up a ball that is tied to the ground, you can't move until | ||
844 | 40 | you drop it. | ||
845 | 41 | """ | ||
846 | 42 | |||
847 | 43 | thing = reference(reftype=Thing, | ||
848 | 44 | whenDeleted=reference.CASCADE, | ||
849 | 45 | allowNone=False) | ||
850 | 46 | |||
851 | 47 | # XXX 'thing' and 'to' should be treated more consistently, or at least the | ||
852 | 48 | # differences between them explained officially. | ||
853 | 49 | to = reference(reftype=Thing, | ||
854 | 50 | whenDeleted=reference.CASCADE, | ||
855 | 51 | allowNone=False) | ||
856 | 52 | |||
857 | 53 | implements(IMovementRestriction) | ||
858 | 54 | |||
859 | 55 | powerupInterfaces = [IMovementRestriction] | ||
860 | 56 | |||
861 | 57 | def movementImminent(self, movee, destination): | ||
862 | 58 | """ | ||
863 | 59 | The object which is tethered is trying to move somewhere. If it has an | ||
864 | 60 | IActor, assume that it's a player trying to move on its own, and emit | ||
865 | 61 | an appropriate message. | ||
866 | 62 | |||
867 | 63 | Otherwise, assume that it is moving *to* an actor, and install a | ||
868 | 64 | L{MovementBlocker} on that actor. | ||
869 | 65 | """ | ||
870 | 66 | # There isn't enough information provided to moveTo just yet; we need | ||
871 | 67 | # to know who is doing the moving. In the meanwhile, if you have an | ||
872 | 68 | # actor, we'll assume you're a player. | ||
873 | 69 | if IActor(movee, None) is not None: | ||
874 | 70 | raise ActionFailure( | ||
875 | 71 | ThatDoesntWork( | ||
876 | 72 | actor=self.thing, | ||
877 | 73 | actorMessage=[u"You can't move, you're tied to ", | ||
878 | 74 | self.to, | ||
879 | 75 | "."], | ||
880 | 76 | otherMessage=[self.thing, u' struggles.'])) | ||
881 | 77 | MovementBlocker.destroyFor(self.thing.location) | ||
882 | 78 | if self.to != destination: | ||
883 | 79 | MovementBlocker.createFor(destination, tether=self) | ||
884 | 80 | |||
885 | 81 | return False | ||
886 | 82 | |||
887 | 83 | |||
888 | 84 | class MovementBlocker(Item, Enhancement): | ||
889 | 85 | """ | ||
890 | 86 | A L{MovementBlocker} is an L{Enhancement} which prevents the movement of a | ||
891 | 87 | player holding a tethered object. | ||
892 | 88 | """ | ||
893 | 89 | implements(IMovementRestriction) | ||
894 | 90 | |||
895 | 91 | powerupInterfaces = [IMovementRestriction] | ||
896 | 92 | |||
897 | 93 | thing = reference( | ||
898 | 94 | doc=""" | ||
899 | 95 | The L{Thing} whose movement is blocked. | ||
900 | 96 | """, reftype=Thing, allowNone=False, | ||
901 | 97 | whenDeleted=reference.CASCADE) | ||
902 | 98 | |||
903 | 99 | tether = reference( | ||
904 | 100 | doc=""" | ||
905 | 101 | The L{Tether} ultimely responsible for blocking movement. | ||
906 | 102 | """, | ||
907 | 103 | reftype=Tether, allowNone=False, | ||
908 | 104 | whenDeleted=reference.CASCADE) | ||
909 | 105 | |||
910 | 106 | |||
911 | 107 | def movementImminent(self, movee, destination): | ||
912 | 108 | """ | ||
913 | 109 | The player this blocker is installed on is trying to move. Assume that | ||
914 | 110 | they are trying to move themselves (via a 'go' action) and prevent it | ||
915 | 111 | by raising an L{ActionFailure} with an appropriate error message for | ||
916 | 112 | the player. | ||
917 | 113 | """ | ||
918 | 114 | raise ActionFailure( | ||
919 | 115 | ThatDoesntWork( | ||
920 | 116 | actor=self.thing, | ||
921 | 117 | actorMessage= | ||
922 | 118 | [u"You can't move, you're still holding ", | ||
923 | 119 | self.tether.thing,u'.'], | ||
924 | 120 | otherMessage= | ||
925 | 121 | [self.thing, u' struggles with ', self.tether.thing,u'.'])) | ||
926 | 0 | 122 | ||
927 | === added file 'Imaginary/TODO.txt' | |||
928 | --- Imaginary/TODO.txt 1970-01-01 00:00:00 +0000 | |||
929 | +++ Imaginary/TODO.txt 2011-09-16 20:42:26 +0000 | |||
930 | @@ -0,0 +1,258 @@ | |||
931 | 1 | |||
932 | 2 | (This list of tasks is for the #2824 branch, it shouldn't be merged to trunk.) | ||
933 | 3 | |||
934 | 4 | self-review: | ||
935 | 5 | |||
936 | 6 | (Since this is already a large branch, I am going to clean up the obvious | ||
937 | 7 | stuff before putting it into review for someone else) | ||
938 | 8 | |||
939 | 9 | * test coverage: | ||
940 | 10 | |||
941 | 11 | * there need to be direct tests for imaginary.idea. This is obviously | ||
942 | 12 | the biggest issue. | ||
943 | 13 | |||
944 | 14 | * lifecycle testing (testing that if these are not identical on | ||
945 | 15 | subsequent method calls, bad stuff happens): | ||
946 | 16 | * Thing.idea | ||
947 | 17 | * Exit.exitIdea | ||
948 | 18 | * Containment._exitIdea | ||
949 | 19 | * Containment._entranceIdea | ||
950 | 20 | |||
951 | 21 | * direct tests for IContainmentRelationship and | ||
952 | 22 | ContainmentRelationship. | ||
953 | 23 | |||
954 | 24 | ------------------------------------------------------------------------------- | ||
955 | 25 | |||
956 | 26 | I think everything below this line is probably for a separate branch, but I | ||
957 | 27 | need to clean it up and file some additional tickets before deleting it. | ||
958 | 28 | |||
959 | 29 | ------------------------------------------------------------------------------- | ||
960 | 30 | |||
961 | 31 | General high-level structural issues: | ||
962 | 32 | |||
963 | 33 | * the "sensory" system could address multiple issues. It would be | ||
964 | 34 | worthwhile to have a rudiment of it, if only to remove duplication | ||
965 | 35 | between "findProviders" and "search" and the thing that computes the list | ||
966 | 36 | for ExpressSurroundings, so we can have a consistent way to construct | ||
967 | 37 | that thing. | ||
968 | 38 | |||
969 | 39 | * movement restrictions want to raise ActionFailure for pretty error | ||
970 | 40 | handling, but don't know who the actor is. This should be dealt with in | ||
971 | 41 | more than one way: | ||
972 | 42 | |||
973 | 43 | * There should be an error-handling path which allows actions to fail | ||
974 | 44 | with feedback only to the actor. "You can't do that because..." | ||
975 | 45 | |||
976 | 46 | * moveTo should receive more information, specifically the actor who | ||
977 | 47 | initiated the movement. There should probably be a TON of extra | ||
978 | 48 | information, like the number of joules used in support of the | ||
979 | 49 | movement etc. | ||
980 | 50 | |||
981 | 51 | * moveTo should not be raising ActionFailure directly. There should be | ||
982 | 52 | a conventional exception type to raise specifically for saying "not | ||
983 | 53 | movable", and its callers should catch it. | ||
984 | 54 | |||
985 | 55 | * Navigators and retrievers need to be reconciled. Specifically, CanSee | ||
986 | 56 | and Visibility need to be smashed into the same object somehow. | ||
987 | 57 | |||
988 | 58 | Some use-cases that should be implemented / tested: | ||
989 | 59 | |||
990 | 60 | * container travel: | ||
991 | 61 | |||
992 | 62 | * I should *not* be able to get out of a closed container that I'm in. | ||
993 | 63 | |||
994 | 64 | * Bugfix, sort of: If I'm inside a closed container, I should be able | ||
995 | 65 | to see and access the stuff around me. | ||
996 | 66 | |||
997 | 67 | * containment fixes | ||
998 | 68 | |||
999 | 69 | * the obtain() call used to pass to ExpressSurroundings is wrong; it's | ||
1000 | 70 | asking "what can you, the location, see from here"; whereas it should | ||
1001 | 71 | be asking "what can you, the player, see in this location". If it | ||
1002 | 72 | were to be really smart / correct, it would be passing an initial | ||
1003 | 73 | path to obtain(), since we know the relationship between these two | ||
1004 | 74 | things. Dumb implementation of that could simply re-evaluate the | ||
1005 | 75 | path up to that point and ignore all the links in it to validate that | ||
1006 | 76 | it's a valid path. | ||
1007 | 77 | |||
1008 | 78 | * ranged actions | ||
1009 | 79 | |||
1010 | 80 | * 'get coolant rod with claw' | ||
1011 | 81 | |||
1012 | 82 | * 'shoot target' | ||
1013 | 83 | |||
1014 | 84 | * A shooting range. There are two rooms: one with targets in it, | ||
1015 | 85 | one with the player and their gun. | ||
1016 | 86 | |||
1017 | 87 | * 'look' and 'shoot' should work, although ideally 'go' should | ||
1018 | 88 | not: it should say something like "that's live fire over | ||
1019 | 89 | there!" | ||
1020 | 90 | |||
1021 | 91 | * the gun should be implicitly located, since it's the only valid | ||
1022 | 92 | tool. | ||
1023 | 93 | |||
1024 | 94 | * I should not be able to see *or* reach objects that are around corners. | ||
1025 | 95 | |||
1026 | 96 | * I should be able to exit a darkened room, perhaps with some modifiers. | ||
1027 | 97 | Some effects that some games might want, should these be default?: | ||
1028 | 98 | |||
1029 | 99 | * Stumbling around in the dark will occasionally send you in a random | ||
1030 | 100 | direction. | ||
1031 | 101 | |||
1032 | 102 | * You can always exit in the direction of an exit where the target of | ||
1033 | 103 | the exit is itself lit. | ||
1034 | 104 | |||
1035 | 105 | * Definitely some games will want this, some not: You are likely to be | ||
1036 | 106 | eaten by a lurking grue. | ||
1037 | 107 | |||
1038 | 108 | * I shouldn't be able to see an event that transpires in a dark room. | ||
1039 | 109 | |||
1040 | 110 | * I should be able to pick up a shirt in a dark room if I have | ||
1041 | 111 | something that lets only me see in the dark (night-vision goggles?) | ||
1042 | 112 | but others should not be able to see that. | ||
1043 | 113 | |||
1044 | 114 | Some restructuring: | ||
1045 | 115 | |||
1046 | 116 | * What paramters should findProviders and search take? We're starting with | ||
1047 | 117 | 'distance' and 'interface'. Let's enumerate some verbs: | ||
1048 | 118 | |||
1049 | 119 | * take: something you can physically reach without walking | ||
1050 | 120 | |||
1051 | 121 | * drop: something you are holding | ||
1052 | 122 | |||
1053 | 123 | * wear: something you are holding | ||
1054 | 124 | |||
1055 | 125 | * sit: something in the room, something you are *NOT* holding | ||
1056 | 126 | |||
1057 | 127 | * stand: something *which is your location*. (something which is | ||
1058 | 128 | holding you?) | ||
1059 | 129 | |||
1060 | 130 | * unwear/remove: something you are *wearing*? something you're holding | ||
1061 | 131 | would be good enough. | ||
1062 | 132 | |||
1063 | 133 | * look: something you can see | ||
1064 | 134 | |||
1065 | 135 | * we need a 'near look' and a 'far look'. When the user types | ||
1066 | 136 | 'look' they only want to see items in their immediate vicinity, | ||
1067 | 137 | but 'look around' or 'look north' or whatever should still allow | ||
1068 | 138 | them to see things that are close enough. | ||
1069 | 139 | |||
1070 | 140 | * shoot: something you can see? If there's something in a glass box, | ||
1071 | 141 | you should be able to shoot it. | ||
1072 | 142 | |||
1073 | 143 | * eat: something you're holding | ||
1074 | 144 | |||
1075 | 145 | defaults: | ||
1076 | 146 | |||
1077 | 147 | * actor -> "you" (still not sure what this means) | ||
1078 | 148 | |||
1079 | 149 | a thought: the self-link of the Idea starting an obtain() | ||
1080 | 150 | search should apply annotationsForIncoming, but it should not | ||
1081 | 151 | apply annotationsForOutgoing. Then the actor idea can always | ||
1082 | 152 | apply an annotationsForOutgoing that says "this isn't you any | ||
1083 | 153 | more"? then you can have a (retriever? sense?) | ||
1084 | 154 | |||
1085 | 155 | * target -> something you're holding | ||
1086 | 156 | |||
1087 | 157 | * tool -> something you're holding | ||
1088 | 158 | |||
1089 | 159 | None of these verbs really know anything about distance, except | ||
1090 | 160 | possibly "shoot" - which really cares about the distance to the target | ||
1091 | 161 | *in the hit-probability calculation*; i.e. it wants to know about the | ||
1092 | 162 | path during the execution of the action, not during the location of the | ||
1093 | 163 | target. | ||
1094 | 164 | |||
1095 | 165 | * Rather than an ad-hoc construction of a Navigator and Retriever in | ||
1096 | 166 | findProviders() and search(), there should be a (pluggable, eventually: | ||
1097 | 167 | this is how one would implement night-vision goggles) way to get objects | ||
1098 | 168 | representing "sensory" inputs. (although "reachability" is a bit of a | ||
1099 | 169 | stretch for a sense, it does make sense as 'touch'.) Practically | ||
1100 | 170 | speaking, that means the logic in the "CanSee" retriever ought to be in | ||
1101 | 171 | Visibility somehow. Options for implementing this: | ||
1102 | 172 | |||
1103 | 173 | * smash the Retriever and the Navigator into one object, and compose | ||
1104 | 174 | them. Then build each one (Visibility, Reachability, etc) by | ||
1105 | 175 | wrapping around the other. Named goes around the outside in | ||
1106 | 176 | search(). | ||
1107 | 177 | |||
1108 | 178 | * add a convenient API for getCandelas and friends to use. getCandelas | ||
1109 | 179 | needs to not take lighting into account. If we have an reification of | ||
1110 | 180 | 'sensory' objects, then we can construct a custom one for this query. | ||
1111 | 181 | |||
1112 | 182 | * Make an "_Idealized" (or something) base class which implements an Item | ||
1113 | 183 | with an 'idea' attribute so that we can manage it on L{Exit}, L{Thing} | ||
1114 | 184 | and perhaps something else. | ||
1115 | 185 | |||
1116 | 186 | * The special-cased just-to-self path in Idea.obtain sucks, because it's | ||
1117 | 187 | inconsistent. There's no 'shouldKeepGoing' call for the link that can | ||
1118 | 188 | prevent it from yielding. If we move the responsibility for doing | ||
1119 | 189 | lighting back into the navigator (where, really, it belongs: Visibility | ||
1120 | 190 | is supposed to be a navigator, right?) this means the navigator can't | ||
1121 | 191 | prevent you from accessing an actor aspect of yourself. | ||
1122 | 192 | |||
1123 | 193 | Other cases which will use this same system: | ||
1124 | 194 | |||
1125 | 195 | * Restraints. Let's say there's a chair that you sit in which | ||
1126 | 196 | restrains you. It needs a proxy which can prevent you from | ||
1127 | 197 | reaching your actor interface. This is much like darkness, | ||
1128 | 198 | except it's going to want to *not* restrict visual events or | ||
1129 | 199 | 'examine'. | ||
1130 | 200 | |||
1131 | 201 | * Suppression / Anti-Magic Field. Restraints and darkness both | ||
1132 | 202 | prevent a blanket "everything" with a few possible exceptions; | ||
1133 | 203 | you might also want an effect which suspends only specific | ||
1134 | 204 | actions / interfaces. | ||
1135 | 205 | |||
1136 | 206 | * Blindfold. | ||
1137 | 207 | |||
1138 | 208 | * The two systems that all of these seem to touch are 'vision' and | ||
1139 | 209 | 'reachability'. So we need link types that say "you can't see beyond | ||
1140 | 210 | this point" and "you can't reach beyond this point". That's | ||
1141 | 211 | "Visibility" and "Proximity", as implemented already, except | ||
1142 | 212 | Visibility can't say "don't keep going" for darkness. It has to have | ||
1143 | 213 | a way to say "you can't see stuff that is immediately on the other | ||
1144 | 214 | side of this link, but if you go through another link that *is* | ||
1145 | 215 | visible, then you can see stuff". i.e. the case where you can't see | ||
1146 | 216 | yourself, or any of your inventory, but you *can* see another room. | ||
1147 | 217 | |||
1148 | 218 | * (test for) additional L{ILinkContributor} powerups being added, removed | ||
1149 | 219 | (i.e. making sure that the 'idea' in question is the same). | ||
1150 | 220 | |||
1151 | 221 | ---- | ||
1152 | 222 | |||
1153 | 223 | Rules for lighting: | ||
1154 | 224 | |||
1155 | 225 | If a room is dark, all objects in that room should have the darkness rule | ||
1156 | 226 | applied to them, regardless (?) of how they are discovered. | ||
1157 | 227 | |||
1158 | 228 | Right now this is enforced entirely by the CanSee retriever and the | ||
1159 | 229 | _PossiblyDark link annotation. | ||
1160 | 230 | |||
1161 | 231 | However, this is overkill. The link annotation is not used at any point during | ||
1162 | 232 | traversal. Only the final annotation in any given path is used, and even that | ||
1163 | 233 | annotation is discarded if there is an effective "null annotation"; a link to a | ||
1164 | 234 | location with no lighting. The way this is detected is (I think) suspect, | ||
1165 | 235 | because it doesn't use the path (path.of can't detect links which ). | ||
1166 | 236 | |||
1167 | 237 | So we could implement the same rules by not annotating anything, and applying | ||
1168 | 238 | proxies only at the final link, inspecting its location, rather than applying | ||
1169 | 239 | an elaborate series of proxies as we go. | ||
1170 | 240 | |||
1171 | 241 | Problems with the current implementation of lighting: | ||
1172 | 242 | |||
1173 | 243 | * applyLighting needs to know the interface that's being queried for so | ||
1174 | 244 | that it can return a _DarkLocationProxy. There's no good way to | ||
1175 | 245 | determine this right now, because the way we know what interface is being | ||
1176 | 246 | asked for has to do with the Retriever, which (in theory?) could be | ||
1177 | 247 | something arbitrary. We could make 'interface' a required attribute of | ||
1178 | 248 | the navigator. That seems a bit weird, since one could (theoretically) | ||
1179 | 249 | want to be able to retrieve things by arbitrary sets of rules, but maybe | ||
1180 | 250 | that's not a useful use-case? | ||
1181 | 251 | |||
1182 | 252 | * Limitations: these can be deferred for a different branch since I think | ||
1183 | 253 | they're mostly just a SMOP, but worth thinking about: | ||
1184 | 254 | |||
1185 | 255 | * the proxy you get for a darkened object ought to be pluggable, so | ||
1186 | 256 | that descriptions can change depending on light level. This could be | ||
1187 | 257 | a useful dramatic tool. | ||
1188 | 258 | |||
1189 | 0 | 259 | ||
1190 | === modified file 'Imaginary/imaginary/action.py' | |||
1191 | --- Imaginary/imaginary/action.py 2009-06-29 04:03:17 +0000 | |||
1192 | +++ Imaginary/imaginary/action.py 2011-09-16 20:42:26 +0000 | |||
1193 | @@ -15,6 +15,8 @@ | |||
1194 | 15 | from imaginary import (iimaginary, eimaginary, iterutils, events, | 15 | from imaginary import (iimaginary, eimaginary, iterutils, events, |
1195 | 16 | objects, text as T, language, pyparsing) | 16 | objects, text as T, language, pyparsing) |
1196 | 17 | from imaginary.world import ImaginaryWorld | 17 | from imaginary.world import ImaginaryWorld |
1197 | 18 | from imaginary.idea import ( | ||
1198 | 19 | CanSee, Proximity, ProviderOf, Named, Traversability) | ||
1199 | 18 | 20 | ||
1200 | 19 | ## Hacks because pyparsing doesn't have fantastic unicode support | 21 | ## Hacks because pyparsing doesn't have fantastic unicode support |
1201 | 20 | _quoteRemovingQuotedString = pyparsing.quotedString.copy() | 22 | _quoteRemovingQuotedString = pyparsing.quotedString.copy() |
1202 | @@ -44,9 +46,12 @@ | |||
1203 | 44 | 46 | ||
1204 | 45 | 47 | ||
1205 | 46 | def parse(self, player, line): | 48 | def parse(self, player, line): |
1207 | 47 | for cls in self.actions: | 49 | """ |
1208 | 50 | Parse an action. | ||
1209 | 51 | """ | ||
1210 | 52 | for eachActionType in self.actions: | ||
1211 | 48 | try: | 53 | try: |
1213 | 49 | match = cls.match(player, line) | 54 | match = eachActionType.match(player, line) |
1214 | 50 | except pyparsing.ParseException: | 55 | except pyparsing.ParseException: |
1215 | 51 | pass | 56 | pass |
1216 | 52 | else: | 57 | else: |
1217 | @@ -56,85 +61,163 @@ | |||
1218 | 56 | if isinstance(v, pyparsing.ParseResults): | 61 | if isinstance(v, pyparsing.ParseResults): |
1219 | 57 | match[k] = v[0] | 62 | match[k] = v[0] |
1220 | 58 | 63 | ||
1222 | 59 | return cls().runEventTransaction(player, line, match) | 64 | return eachActionType().runEventTransaction(player, line, match) |
1223 | 60 | return defer.fail(eimaginary.NoSuchCommand(line)) | 65 | return defer.fail(eimaginary.NoSuchCommand(line)) |
1224 | 61 | 66 | ||
1225 | 62 | 67 | ||
1226 | 63 | 68 | ||
1227 | 64 | class Action(object): | 69 | class Action(object): |
1228 | 70 | """ | ||
1229 | 71 | An L{Action} represents an intention of a player to do something. | ||
1230 | 72 | """ | ||
1231 | 65 | __metaclass__ = _ActionType | 73 | __metaclass__ = _ActionType |
1232 | 66 | infrastructure = True | 74 | infrastructure = True |
1233 | 67 | 75 | ||
1234 | 76 | actorInterface = iimaginary.IActor | ||
1235 | 68 | 77 | ||
1236 | 69 | def runEventTransaction(self, player, line, match): | 78 | def runEventTransaction(self, player, line, match): |
1237 | 70 | """ | 79 | """ |
1242 | 71 | Take a player, line, and dictionary of parse results and execute the | 80 | Take a player, input, and dictionary of parse results, resolve those |
1243 | 72 | actual Action implementation. | 81 | parse results into implementations of appropriate interfaces in the |
1244 | 73 | 82 | game world, and execute the actual Action implementation (contained in | |
1245 | 74 | @param player: A provider of C{self.actorInterface} | 83 | the 'do' method) in an event transaction. |
1246 | 84 | |||
1247 | 85 | This is the top level of action invocation. | ||
1248 | 86 | |||
1249 | 87 | @param player: A L{Thing} representing the actor's body. | ||
1250 | 88 | |||
1251 | 75 | @param line: A unicode string containing the original input | 89 | @param line: A unicode string containing the original input |
1252 | 90 | |||
1253 | 76 | @param match: A dictionary containing some parse results to pass | 91 | @param match: A dictionary containing some parse results to pass |
1261 | 77 | through to the C{run} method. | 92 | through to this L{Action}'s C{do} method as keyword arguments. |
1262 | 78 | 93 | ||
1263 | 79 | """ | 94 | @raise eimaginary.AmbiguousArgument: if multiple valid targets are |
1264 | 80 | events.runEventTransaction( | 95 | found for an argument. |
1265 | 81 | player.store, self.run, player, line, **match) | 96 | """ |
1266 | 82 | 97 | def thunk(): | |
1267 | 83 | 98 | begin = time.time() | |
1268 | 99 | try: | ||
1269 | 100 | actor = self.actorInterface(player) | ||
1270 | 101 | for (k, v) in match.items(): | ||
1271 | 102 | try: | ||
1272 | 103 | objs = self.resolve(player, k, v) | ||
1273 | 104 | except NotImplementedError: | ||
1274 | 105 | pass | ||
1275 | 106 | else: | ||
1276 | 107 | if len(objs) == 1: | ||
1277 | 108 | match[k] = objs[0] | ||
1278 | 109 | elif len(objs) == 0: | ||
1279 | 110 | self.cantFind(player, actor, k, v) | ||
1280 | 111 | else: | ||
1281 | 112 | raise eimaginary.AmbiguousArgument(self, k, v, objs) | ||
1282 | 113 | return self.do(actor, line, **match) | ||
1283 | 114 | finally: | ||
1284 | 115 | end = time.time() | ||
1285 | 116 | log.msg(interface=iaxiom.IStatEvent, | ||
1286 | 117 | stat_actionDuration=end - begin, | ||
1287 | 118 | stat_actionExecuted=1) | ||
1288 | 119 | events.runEventTransaction(player.store, thunk) | ||
1289 | 120 | |||
1290 | 121 | |||
1291 | 122 | def cantFind(self, player, actor, slot, name): | ||
1292 | 123 | """ | ||
1293 | 124 | This hook is invoked when a target cannot be found. | ||
1294 | 125 | |||
1295 | 126 | This will delegate to a method like C{self.cantFind_<slot>(actor, | ||
1296 | 127 | name)} if one exists, to determine the error message to show to the | ||
1297 | 128 | actor. It will then raise L{eimaginary.ActionFailure} to stop | ||
1298 | 129 | processing of this action. | ||
1299 | 130 | |||
1300 | 131 | @param player: The L{Thing} doing the searching. | ||
1301 | 132 | |||
1302 | 133 | @type player: L{IThing} | ||
1303 | 134 | |||
1304 | 135 | @param actor: The L{IActor} doing the searching. | ||
1305 | 136 | |||
1306 | 137 | @type actor: L{IActor} | ||
1307 | 138 | |||
1308 | 139 | @param slot: The slot in question. | ||
1309 | 140 | |||
1310 | 141 | @type slot: C{str} | ||
1311 | 142 | |||
1312 | 143 | @param name: The name of the object being searched for. | ||
1313 | 144 | |||
1314 | 145 | @type name: C{unicode} | ||
1315 | 146 | |||
1316 | 147 | @raise eimaginary.ActionFailure: always. | ||
1317 | 148 | """ | ||
1318 | 149 | func = getattr(self, "cantFind_"+slot, None) | ||
1319 | 150 | if func: | ||
1320 | 151 | msg = func(actor, name) | ||
1321 | 152 | else: | ||
1322 | 153 | msg = "Who's that?" | ||
1323 | 154 | raise eimaginary.ActionFailure( | ||
1324 | 155 | events.ThatDoesntWork( | ||
1325 | 156 | actorMessage=msg, | ||
1326 | 157 | actor=player)) | ||
1327 | 158 | |||
1328 | 159 | |||
1329 | 160 | @classmethod | ||
1330 | 84 | def match(cls, player, line): | 161 | def match(cls, player, line): |
1331 | 162 | """ | ||
1332 | 163 | @return: a list of 2-tuples of all the results of parsing the given | ||
1333 | 164 | C{line} using this L{Action} type's pyparsing C{expr} attribute, or | ||
1334 | 165 | None if the expression does not match the given line. | ||
1335 | 166 | |||
1336 | 167 | @param line: a line of user input to be interpreted as an action. | ||
1337 | 168 | |||
1338 | 169 | @see: L{imaginary.pyparsing} | ||
1339 | 170 | """ | ||
1340 | 85 | return cls.expr.parseString(line) | 171 | return cls.expr.parseString(line) |
1369 | 86 | match = classmethod(match) | 172 | |
1370 | 87 | 173 | ||
1371 | 88 | 174 | def do(self, player, line, **slots): | |
1372 | 89 | def run(self, player, line, **kw): | 175 | """ |
1373 | 90 | begin = time.time() | 176 | Subclasses override this method to actually perform the action. |
1374 | 91 | try: | 177 | |
1375 | 92 | return self._reallyRun(player, line, kw) | 178 | This method is performed in an event transaction, by 'run'. |
1376 | 93 | finally: | 179 | |
1377 | 94 | end = time.time() | 180 | NB: The suggested implementation strategy for a 'do' method is to do |
1378 | 95 | log.msg( | 181 | action-specific setup but then delegate the bulk of the actual logic to |
1379 | 96 | interface=iaxiom.IStatEvent, | 182 | a method on a target/tool interface. The 'do' method's job is to |
1380 | 97 | stat_actionDuration=end - begin, | 183 | select the appropriate methods to invoke. |
1381 | 98 | stat_actionExecuted=1, | 184 | |
1382 | 99 | ) | 185 | @param player: a provider of this L{Action}'s C{actorInterface}. |
1383 | 100 | 186 | ||
1384 | 101 | 187 | @param line: the input string that created this action. | |
1385 | 102 | def _reallyRun(self, player, line, kw): | 188 | |
1386 | 103 | for (k, v) in kw.items(): | 189 | @param slots: The results of calling C{self.resolve} on each parsing |
1387 | 104 | try: | 190 | result (described by a setResultsName in C{self.expr}). |
1388 | 105 | objs = self.resolve(player, k, v) | 191 | """ |
1389 | 106 | except NotImplementedError: | 192 | raise NotImplementedError("'do' method not implemented") |
1362 | 107 | pass | ||
1363 | 108 | else: | ||
1364 | 109 | if len(objs) != 1: | ||
1365 | 110 | raise eimaginary.AmbiguousArgument(self, k, v, objs) | ||
1366 | 111 | else: | ||
1367 | 112 | kw[k] = objs[0] | ||
1368 | 113 | return self.do(player, line, **kw) | ||
1390 | 114 | 193 | ||
1391 | 115 | 194 | ||
1392 | 116 | def resolve(self, player, name, value): | 195 | def resolve(self, player, name, value): |
1414 | 117 | raise NotImplementedError("Don't know how to resolve %r (%r)" % (name, value)) | 196 | """ |
1415 | 118 | 197 | Resolve a given parsed value to a valid action parameter by calling a | |
1416 | 119 | 198 | 'resolve_<name>' method on this L{Action} with the given C{player} and | |
1417 | 120 | 199 | C{value}. | |
1418 | 121 | class NoTargetAction(Action): | 200 | |
1419 | 122 | """ | 201 | @param player: the L{Thing} attempting to perform this action. |
1420 | 123 | @cvar actorInterface: Interface that the actor must provide. | 202 | |
1421 | 124 | """ | 203 | @type player: L{Thing} |
1422 | 125 | infrastructure = True | 204 | |
1423 | 126 | 205 | @param name: the name of the slot being filled. For example, 'target'. | |
1424 | 127 | actorInterface = iimaginary.IActor | 206 | |
1425 | 128 | 207 | @type name: L{str} | |
1426 | 129 | def match(cls, player, line): | 208 | |
1427 | 130 | actor = cls.actorInterface(player, None) | 209 | @param value: a string representing the value that was parsed. For |
1428 | 131 | if actor is not None: | 210 | example, if the user typed 'get fish', this would be 'fish'. |
1429 | 132 | return super(NoTargetAction, cls).match(player, line) | 211 | |
1430 | 133 | return None | 212 | @return: a value which will be passed as the 'name' parameter to this |
1431 | 134 | match = classmethod(match) | 213 | L{Action}'s C{do} method. |
1432 | 135 | 214 | """ | |
1433 | 136 | def run(self, player, line, **kw): | 215 | resolver = getattr(self, 'resolve_%s' % (name,), None) |
1434 | 137 | return super(NoTargetAction, self).run(self.actorInterface(player), line, **kw) | 216 | if resolver is None: |
1435 | 217 | raise NotImplementedError( | ||
1436 | 218 | "Don't know how to resolve %r (%r)" % (name, value)) | ||
1437 | 219 | return resolver(player, value) | ||
1438 | 220 | |||
1439 | 138 | 221 | ||
1440 | 139 | 222 | ||
1441 | 140 | def targetString(name): | 223 | def targetString(name): |
1442 | @@ -144,7 +227,7 @@ | |||
1443 | 144 | 227 | ||
1444 | 145 | 228 | ||
1445 | 146 | 229 | ||
1447 | 147 | class TargetAction(NoTargetAction): | 230 | class TargetAction(Action): |
1448 | 148 | """ | 231 | """ |
1449 | 149 | Subclass L{TargetAction} to implement an action that acts on a target, like | 232 | Subclass L{TargetAction} to implement an action that acts on a target, like |
1450 | 150 | 'take foo' or 'eat foo' where 'foo' is the target. | 233 | 'take foo' or 'eat foo' where 'foo' is the target. |
1451 | @@ -160,10 +243,9 @@ | |||
1452 | 160 | def targetRadius(self, player): | 243 | def targetRadius(self, player): |
1453 | 161 | return 2 | 244 | return 2 |
1454 | 162 | 245 | ||
1459 | 163 | def resolve(self, player, k, v): | 246 | def resolve_target(self, player, targetName): |
1460 | 164 | if k == "target": | 247 | return _getIt(player, targetName, |
1461 | 165 | return list(player.thing.search(self.targetRadius(player), self.targetInterface, v)) | 248 | self.targetInterface, self.targetRadius(player)) |
1458 | 166 | return super(TargetAction, self).resolve(player, k, v) | ||
1462 | 167 | 249 | ||
1463 | 168 | 250 | ||
1464 | 169 | 251 | ||
1465 | @@ -183,21 +265,29 @@ | |||
1466 | 183 | def toolRadius(self, player): | 265 | def toolRadius(self, player): |
1467 | 184 | return 2 | 266 | return 2 |
1468 | 185 | 267 | ||
1478 | 186 | def resolve(self, player, k, v): | 268 | def resolve_tool(self, player, toolName): |
1479 | 187 | if k == "tool": | 269 | return _getIt(player, toolName, |
1480 | 188 | return list(player.thing.search( | 270 | self.toolInterface, self.toolRadius(player)) |
1481 | 189 | self.toolRadius(player), self.toolInterface, v)) | 271 | |
1482 | 190 | return super(ToolAction, self).resolve(player, k, v) | 272 | |
1483 | 191 | 273 | ||
1484 | 192 | 274 | def _getIt(player, thingName, iface, radius): | |
1485 | 193 | 275 | return list(player.search(radius, iface, thingName)) | |
1486 | 194 | class LookAround(NoTargetAction): | 276 | |
1487 | 277 | |||
1488 | 278 | |||
1489 | 279 | class LookAround(Action): | ||
1490 | 195 | actionName = "look" | 280 | actionName = "look" |
1491 | 196 | expr = pyparsing.Literal("look") + pyparsing.StringEnd() | 281 | expr = pyparsing.Literal("look") + pyparsing.StringEnd() |
1492 | 197 | 282 | ||
1493 | 198 | def do(self, player, line): | 283 | def do(self, player, line): |
1494 | 284 | ultimateLocation = player.thing.location | ||
1495 | 285 | while ultimateLocation.location is not None: | ||
1496 | 286 | ultimateLocation = ultimateLocation.location | ||
1497 | 199 | for visible in player.thing.findProviders(iimaginary.IVisible, 1): | 287 | for visible in player.thing.findProviders(iimaginary.IVisible, 1): |
1499 | 200 | if player.thing.location is visible.thing: | 288 | # XXX what if my location is furniture? I want to see '( Foo, |
1500 | 289 | # sitting in the Bar )', not '( Bar )'. | ||
1501 | 290 | if visible.isViewOf(ultimateLocation): | ||
1502 | 201 | concept = visible.visualize() | 291 | concept = visible.visualize() |
1503 | 202 | break | 292 | break |
1504 | 203 | else: | 293 | else: |
1505 | @@ -217,7 +307,35 @@ | |||
1506 | 217 | 307 | ||
1507 | 218 | targetInterface = iimaginary.IVisible | 308 | targetInterface = iimaginary.IVisible |
1508 | 219 | 309 | ||
1510 | 220 | def targetNotAvailable(self, player, exc): | 310 | def resolve_target(self, player, targetName): |
1511 | 311 | """ | ||
1512 | 312 | Resolve the target to look at by looking for a named, visible object in | ||
1513 | 313 | a proximity of 3 meters from the player. | ||
1514 | 314 | |||
1515 | 315 | @param player: The player doing the looking. | ||
1516 | 316 | |||
1517 | 317 | @type player: L{IThing} | ||
1518 | 318 | |||
1519 | 319 | @param targetName: The name of the object we are looking for. | ||
1520 | 320 | |||
1521 | 321 | @type targetName: C{unicode} | ||
1522 | 322 | |||
1523 | 323 | @return: A list of visible objects. | ||
1524 | 324 | |||
1525 | 325 | @rtype: C{list} of L{IVisible} | ||
1526 | 326 | |||
1527 | 327 | @raise eimaginary.ActionFailure: with an appropriate message if the | ||
1528 | 328 | target cannot be resolved for an identifiable reason. See | ||
1529 | 329 | L{imaginary.objects.Thing.obtainOrReportWhyNot} for a description | ||
1530 | 330 | of how such reasons may be identified. | ||
1531 | 331 | """ | ||
1532 | 332 | return player.obtainOrReportWhyNot( | ||
1533 | 333 | Proximity(3.0, Named(targetName, | ||
1534 | 334 | CanSee(ProviderOf(iimaginary.IVisible)), | ||
1535 | 335 | player))) | ||
1536 | 336 | |||
1537 | 337 | |||
1538 | 338 | def cantFind_target(self, player, name): | ||
1539 | 221 | return "You don't see that." | 339 | return "You don't see that." |
1540 | 222 | 340 | ||
1541 | 223 | def targetRadius(self, player): | 341 | def targetRadius(self, player): |
1542 | @@ -238,7 +356,7 @@ | |||
1543 | 238 | 356 | ||
1544 | 239 | 357 | ||
1545 | 240 | 358 | ||
1547 | 241 | class Illuminate(NoTargetAction): | 359 | class Illuminate(Action): |
1548 | 242 | """ | 360 | """ |
1549 | 243 | Change the ambient light level at the location of the actor. Since this is | 361 | Change the ambient light level at the location of the actor. Since this is |
1550 | 244 | an administrative action that directly manipulates the environment, the | 362 | an administrative action that directly manipulates the environment, the |
1551 | @@ -488,7 +606,7 @@ | |||
1552 | 488 | 606 | ||
1553 | 489 | 607 | ||
1554 | 490 | 608 | ||
1556 | 491 | class Equipment(NoTargetAction): | 609 | class Equipment(Action): |
1557 | 492 | expr = pyparsing.Literal("equipment") | 610 | expr = pyparsing.Literal("equipment") |
1558 | 493 | 611 | ||
1559 | 494 | actorInterface = iimaginary.IClothingWearer | 612 | actorInterface = iimaginary.IClothingWearer |
1560 | @@ -528,9 +646,9 @@ | |||
1561 | 528 | pyparsing.White() + | 646 | pyparsing.White() + |
1562 | 529 | targetString("tool")) | 647 | targetString("tool")) |
1563 | 530 | 648 | ||
1565 | 531 | def targetNotAvailable(self, player, exc): | 649 | def cantFind_target(self, player, targetName): |
1566 | 532 | return "Nothing like that around here." | 650 | return "Nothing like that around here." |
1568 | 533 | toolNotAvailable = targetNotAvailable | 651 | cantFind_tool = cantFind_target |
1569 | 534 | 652 | ||
1570 | 535 | def do(self, player, line, target, tool): | 653 | def do(self, player, line, target, tool): |
1571 | 536 | # XXX Make sure target is in tool | 654 | # XXX Make sure target is in tool |
1572 | @@ -547,7 +665,7 @@ | |||
1573 | 547 | toolInterface = iimaginary.IThing | 665 | toolInterface = iimaginary.IThing |
1574 | 548 | targetInterface = iimaginary.IContainer | 666 | targetInterface = iimaginary.IContainer |
1575 | 549 | 667 | ||
1577 | 550 | def targetNotAvailable(self, player, exc): | 668 | def cantFind_target(self, player, targetName): |
1578 | 551 | return "That doesn't work." | 669 | return "That doesn't work." |
1579 | 552 | 670 | ||
1580 | 553 | expr = (pyparsing.Literal("put") + | 671 | expr = (pyparsing.Literal("put") + |
1581 | @@ -609,7 +727,7 @@ | |||
1582 | 609 | pyparsing.White() + | 727 | pyparsing.White() + |
1583 | 610 | targetString("target")) | 728 | targetString("target")) |
1584 | 611 | 729 | ||
1586 | 612 | def targetNotAvailable(self, player, exc): | 730 | def cantFind_target(self, player, targetName): |
1587 | 613 | return u"Nothing like that around here." | 731 | return u"Nothing like that around here." |
1588 | 614 | 732 | ||
1589 | 615 | def targetRadius(self, player): | 733 | def targetRadius(self, player): |
1590 | @@ -641,7 +759,7 @@ | |||
1591 | 641 | pyparsing.White() + | 759 | pyparsing.White() + |
1592 | 642 | targetString("target")) | 760 | targetString("target")) |
1593 | 643 | 761 | ||
1595 | 644 | def targetNotAvailable(self, player, exc): | 762 | def cantFind_target(self, player, targetName): |
1596 | 645 | return "Nothing like that around here." | 763 | return "Nothing like that around here." |
1597 | 646 | 764 | ||
1598 | 647 | def targetRadius(self, player): | 765 | def targetRadius(self, player): |
1599 | @@ -678,7 +796,7 @@ | |||
1600 | 678 | 796 | ||
1601 | 679 | 797 | ||
1602 | 680 | 798 | ||
1604 | 681 | class Dig(NoTargetAction): | 799 | class Dig(Action): |
1605 | 682 | expr = (pyparsing.Literal("dig") + | 800 | expr = (pyparsing.Literal("dig") + |
1606 | 683 | pyparsing.White() + | 801 | pyparsing.White() + |
1607 | 684 | DIRECTION_LITERAL + | 802 | DIRECTION_LITERAL + |
1608 | @@ -707,7 +825,7 @@ | |||
1609 | 707 | 825 | ||
1610 | 708 | 826 | ||
1611 | 709 | 827 | ||
1613 | 710 | class Bury(NoTargetAction): | 828 | class Bury(Action): |
1614 | 711 | expr = (pyparsing.Literal("bury") + | 829 | expr = (pyparsing.Literal("bury") + |
1615 | 712 | pyparsing.White() + | 830 | pyparsing.White() + |
1616 | 713 | DIRECTION_LITERAL) | 831 | DIRECTION_LITERAL) |
1617 | @@ -738,47 +856,59 @@ | |||
1618 | 738 | 856 | ||
1619 | 739 | 857 | ||
1620 | 740 | 858 | ||
1624 | 741 | class Go(NoTargetAction): | 859 | class Go(Action): |
1625 | 742 | expr = (pyparsing.Optional(pyparsing.Literal("go") + pyparsing.White()) + | 860 | expr = ( |
1626 | 743 | DIRECTION_LITERAL) | 861 | DIRECTION_LITERAL | |
1627 | 862 | (pyparsing.Literal("go") + pyparsing.White() + | ||
1628 | 863 | targetString("direction")) | | ||
1629 | 864 | (pyparsing.Literal("enter") + pyparsing.White() + | ||
1630 | 865 | targetString("direction")) | | ||
1631 | 866 | (pyparsing.Literal("exit") + pyparsing.White() + | ||
1632 | 867 | targetString("direction"))) | ||
1633 | 868 | |||
1634 | 869 | actorInterface = iimaginary.IThing | ||
1635 | 870 | |||
1636 | 871 | def resolve_direction(self, player, directionName): | ||
1637 | 872 | """ | ||
1638 | 873 | Identify a direction by having the player search for L{IExit} | ||
1639 | 874 | providers that they can see and reach. | ||
1640 | 875 | """ | ||
1641 | 876 | return player.obtainOrReportWhyNot( | ||
1642 | 877 | Proximity( | ||
1643 | 878 | 3.0, | ||
1644 | 879 | Traversability( | ||
1645 | 880 | Named(directionName, | ||
1646 | 881 | CanSee(ProviderOf(iimaginary.IExit)), player)))) | ||
1647 | 882 | |||
1648 | 883 | |||
1649 | 884 | def cantFind_direction(self, actor, directionName): | ||
1650 | 885 | """ | ||
1651 | 886 | Explain to the user that they can't go in a direction that they can't | ||
1652 | 887 | locate. | ||
1653 | 888 | """ | ||
1654 | 889 | return u"You can't go that way." | ||
1655 | 890 | |||
1656 | 744 | 891 | ||
1657 | 745 | def do(self, player, line, direction): | 892 | def do(self, player, line, direction): |
1667 | 746 | try: | 893 | location = player.location |
1659 | 747 | exit = iimaginary.IContainer(player.thing.location).getExitNamed(direction) | ||
1660 | 748 | except KeyError: | ||
1661 | 749 | raise eimaginary.ActionFailure(events.ThatDoesntWork( | ||
1662 | 750 | actor=player.thing, | ||
1663 | 751 | actorMessage=u"You can't go that way.")) | ||
1664 | 752 | |||
1665 | 753 | dest = exit.toLocation | ||
1666 | 754 | location = player.thing.location | ||
1668 | 755 | 894 | ||
1669 | 756 | evt = events.Success( | 895 | evt = events.Success( |
1670 | 757 | location=location, | 896 | location=location, |
1673 | 758 | actor=player.thing, | 897 | actor=player, |
1674 | 759 | otherMessage=(player.thing, " leaves ", direction, ".")) | 898 | otherMessage=(player, " leaves ", direction.name, ".")) |
1675 | 760 | evt.broadcast() | 899 | evt.broadcast() |
1676 | 761 | 900 | ||
1677 | 762 | if exit.sibling is not None: | ||
1678 | 763 | arriveDirection = exit.sibling.name | ||
1679 | 764 | else: | ||
1680 | 765 | arriveDirection = object.OPPOSITE_DIRECTIONS[exit.name] | ||
1681 | 766 | |||
1682 | 767 | try: | 901 | try: |
1689 | 768 | player.thing.moveTo( | 902 | direction.traverse(player) |
1684 | 769 | dest, | ||
1685 | 770 | arrivalEventFactory=lambda player: events.MovementArrivalEvent( | ||
1686 | 771 | thing=player, | ||
1687 | 772 | origin=None, | ||
1688 | 773 | direction=arriveDirection)) | ||
1690 | 774 | except eimaginary.DoesntFit: | 903 | except eimaginary.DoesntFit: |
1691 | 775 | raise eimaginary.ActionFailure(events.ThatDoesntWork( | 904 | raise eimaginary.ActionFailure(events.ThatDoesntWork( |
1694 | 776 | actor=player.thing, | 905 | actor=player, |
1695 | 777 | actorMessage=language.ExpressString(u"There's no room for you there."))) | 906 | actorMessage=language.ExpressString( |
1696 | 907 | u"There's no room for you there."))) | ||
1697 | 778 | 908 | ||
1701 | 779 | # XXX A convention for programmatically invoked actions? | 909 | # This is subtly incorrect: see http://divmod.org/trac/ticket/2917 |
1702 | 780 | # None as the line? | 910 | lookAroundActor = iimaginary.IActor(player) |
1703 | 781 | LookAround().do(player, "look") | 911 | LookAround().do(lookAroundActor, "look") |
1704 | 782 | 912 | ||
1705 | 783 | 913 | ||
1706 | 784 | 914 | ||
1707 | @@ -789,9 +919,11 @@ | |||
1708 | 789 | 919 | ||
1709 | 790 | targetInterface = iimaginary.IActor | 920 | targetInterface = iimaginary.IActor |
1710 | 791 | 921 | ||
1714 | 792 | def targetNotAvailable(self, player, exc): | 922 | def cantFind_target(self, player, targetName): |
1715 | 793 | for thing in player.search(self.targetRadius(player), iimaginary.IThing, exc.partValue): | 923 | for thing in player.thing.search(self.targetRadius(player), |
1716 | 794 | return (language.Noun(thing).nounPhrase().plaintext(player), " cannot be restored.") | 924 | iimaginary.IThing, targetName): |
1717 | 925 | return (language.Noun(thing).nounPhrase().plaintext(player), | ||
1718 | 926 | " cannot be restored.") | ||
1719 | 795 | return "Who's that?" | 927 | return "Who's that?" |
1720 | 796 | 928 | ||
1721 | 797 | def targetRadius(self, player): | 929 | def targetRadius(self, player): |
1722 | @@ -831,7 +963,6 @@ | |||
1723 | 831 | return 3 | 963 | return 3 |
1724 | 832 | 964 | ||
1725 | 833 | def do(self, player, line, target): | 965 | def do(self, player, line, target): |
1726 | 834 | toBroadcast = [] | ||
1727 | 835 | if target is player: | 966 | if target is player: |
1728 | 836 | raise eimaginary.ActionFailure( | 967 | raise eimaginary.ActionFailure( |
1729 | 837 | events.ThatDoesntMakeSense(u"Hit yourself? Stupid.", | 968 | events.ThatDoesntMakeSense(u"Hit yourself? Stupid.", |
1730 | @@ -866,7 +997,7 @@ | |||
1731 | 866 | 997 | ||
1732 | 867 | 998 | ||
1733 | 868 | 999 | ||
1735 | 869 | class Say(NoTargetAction): | 1000 | class Say(Action): |
1736 | 870 | expr = (((pyparsing.Literal("say") + pyparsing.White()) ^ | 1001 | expr = (((pyparsing.Literal("say") + pyparsing.White()) ^ |
1737 | 871 | pyparsing.Literal("'")) + | 1002 | pyparsing.Literal("'")) + |
1738 | 872 | pyparsing.restOfLine.setResultsName("text")) | 1003 | pyparsing.restOfLine.setResultsName("text")) |
1739 | @@ -877,7 +1008,7 @@ | |||
1740 | 877 | 1008 | ||
1741 | 878 | 1009 | ||
1742 | 879 | 1010 | ||
1744 | 880 | class Emote(NoTargetAction): | 1011 | class Emote(Action): |
1745 | 881 | expr = (((pyparsing.Literal("emote") + pyparsing.White()) ^ | 1012 | expr = (((pyparsing.Literal("emote") + pyparsing.White()) ^ |
1746 | 882 | pyparsing.Literal(":")) + | 1013 | pyparsing.Literal(":")) + |
1747 | 883 | pyparsing.restOfLine.setResultsName("text")) | 1014 | pyparsing.restOfLine.setResultsName("text")) |
1748 | @@ -890,7 +1021,7 @@ | |||
1749 | 890 | 1021 | ||
1750 | 891 | 1022 | ||
1751 | 892 | 1023 | ||
1753 | 893 | class Actions(NoTargetAction): | 1024 | class Actions(Action): |
1754 | 894 | expr = pyparsing.Literal("actions") | 1025 | expr = pyparsing.Literal("actions") |
1755 | 895 | 1026 | ||
1756 | 896 | def do(self, player, line): | 1027 | def do(self, player, line): |
1757 | @@ -903,7 +1034,7 @@ | |||
1758 | 903 | 1034 | ||
1759 | 904 | 1035 | ||
1760 | 905 | 1036 | ||
1762 | 906 | class Search(NoTargetAction): | 1037 | class Search(Action): |
1763 | 907 | expr = (pyparsing.Literal("search") + | 1038 | expr = (pyparsing.Literal("search") + |
1764 | 908 | targetString("name")) | 1039 | targetString("name")) |
1765 | 909 | 1040 | ||
1766 | @@ -920,7 +1051,7 @@ | |||
1767 | 920 | 1051 | ||
1768 | 921 | 1052 | ||
1769 | 922 | 1053 | ||
1771 | 923 | class Score(NoTargetAction): | 1054 | class Score(Action): |
1772 | 924 | expr = pyparsing.Literal("score") | 1055 | expr = pyparsing.Literal("score") |
1773 | 925 | 1056 | ||
1774 | 926 | scoreFormat = ( | 1057 | scoreFormat = ( |
1775 | @@ -951,7 +1082,7 @@ | |||
1776 | 951 | 1082 | ||
1777 | 952 | 1083 | ||
1778 | 953 | 1084 | ||
1780 | 954 | class Who(NoTargetAction): | 1085 | class Who(Action): |
1781 | 955 | expr = pyparsing.Literal("who") | 1086 | expr = pyparsing.Literal("who") |
1782 | 956 | 1087 | ||
1783 | 957 | def do(self, player, line): | 1088 | def do(self, player, line): |
1784 | @@ -1003,7 +1134,7 @@ | |||
1785 | 1003 | 1134 | ||
1786 | 1004 | 1135 | ||
1787 | 1005 | 1136 | ||
1789 | 1006 | class Inventory(NoTargetAction): | 1137 | class Inventory(Action): |
1790 | 1007 | expr = pyparsing.Literal("inventory") | 1138 | expr = pyparsing.Literal("inventory") |
1791 | 1008 | 1139 | ||
1792 | 1009 | def do(self, player, line): | 1140 | def do(self, player, line): |
1793 | @@ -1113,7 +1244,7 @@ | |||
1794 | 1113 | 1244 | ||
1795 | 1114 | 1245 | ||
1796 | 1115 | 1246 | ||
1798 | 1116 | class Help(NoTargetAction): | 1247 | class Help(Action): |
1799 | 1117 | """ | 1248 | """ |
1800 | 1118 | A command for looking up help files. | 1249 | A command for looking up help files. |
1801 | 1119 | 1250 | ||
1802 | 1120 | 1251 | ||
1803 | === modified file 'Imaginary/imaginary/creation.py' | |||
1804 | --- Imaginary/imaginary/creation.py 2009-06-29 04:03:17 +0000 | |||
1805 | +++ Imaginary/imaginary/creation.py 2011-09-16 20:42:26 +0000 | |||
1806 | @@ -16,7 +16,7 @@ | |||
1807 | 16 | from imaginary.iimaginary import IThingType | 16 | from imaginary.iimaginary import IThingType |
1808 | 17 | from imaginary.eimaginary import ActionFailure, DoesntFit | 17 | from imaginary.eimaginary import ActionFailure, DoesntFit |
1809 | 18 | 18 | ||
1811 | 19 | from imaginary.action import NoTargetAction, insufficientSpace | 19 | from imaginary.action import Action, insufficientSpace |
1812 | 20 | from imaginary.action import targetString | 20 | from imaginary.action import targetString |
1813 | 21 | 21 | ||
1814 | 22 | from imaginary.pyparsing import Literal, White, Optional, restOfLine | 22 | from imaginary.pyparsing import Literal, White, Optional, restOfLine |
1815 | @@ -109,7 +109,7 @@ | |||
1816 | 109 | otherMessage=language.Sentence([player, " creates ", phrase, "."])) | 109 | otherMessage=language.Sentence([player, " creates ", phrase, "."])) |
1817 | 110 | 110 | ||
1818 | 111 | 111 | ||
1820 | 112 | class Create(NoTargetAction): | 112 | class Create(Action): |
1821 | 113 | """ | 113 | """ |
1822 | 114 | An action which can create items by looking at the L{IThingType} plugin | 114 | An action which can create items by looking at the L{IThingType} plugin |
1823 | 115 | registry. | 115 | registry. |
1824 | @@ -163,7 +163,7 @@ | |||
1825 | 163 | 163 | ||
1826 | 164 | 164 | ||
1827 | 165 | 165 | ||
1829 | 166 | class ListThingTypes(NoTargetAction): | 166 | class ListThingTypes(Action): |
1830 | 167 | """ | 167 | """ |
1831 | 168 | An action which tells the invoker what thing types exist to be created with | 168 | An action which tells the invoker what thing types exist to be created with |
1832 | 169 | the L{Create} command. | 169 | the L{Create} command. |
1833 | 170 | 170 | ||
1834 | === modified file 'Imaginary/imaginary/events.py' | |||
1835 | --- Imaginary/imaginary/events.py 2009-01-14 05:21:23 +0000 | |||
1836 | +++ Imaginary/imaginary/events.py 2011-09-16 20:42:26 +0000 | |||
1837 | @@ -1,10 +1,11 @@ | |||
1839 | 1 | # -*- test-case-name: imaginary.test -*- | 1 | # -*- test-case-name: imaginary.test.test_actions.TargetActionTests.test_resolveTargetCaseInsensitively -*- |
1840 | 2 | 2 | ||
1841 | 3 | from zope.interface import implements | 3 | from zope.interface import implements |
1842 | 4 | 4 | ||
1843 | 5 | from twisted.python import context | 5 | from twisted.python import context |
1844 | 6 | 6 | ||
1845 | 7 | from imaginary import iimaginary, language, eimaginary | 7 | from imaginary import iimaginary, language, eimaginary |
1846 | 8 | from imaginary.idea import Proximity, ProviderOf | ||
1847 | 8 | 9 | ||
1848 | 9 | 10 | ||
1849 | 10 | class Event(language.BaseExpress): | 11 | class Event(language.BaseExpress): |
1850 | @@ -35,8 +36,13 @@ | |||
1851 | 35 | 36 | ||
1852 | 36 | 37 | ||
1853 | 37 | def conceptFor(self, observer): | 38 | def conceptFor(self, observer): |
1856 | 38 | # This can't be a dict because then the ordering when actor is target | 39 | """ |
1857 | 39 | # or target is tool or etc is non-deterministic. | 40 | Retrieve the appropriate L{IConcept} provider for a given observer. If |
1858 | 41 | the observer is this L{Event}'s C{actor}, it will return the | ||
1859 | 42 | C{actorMessage} for this event, and so on for the tool and the target. | ||
1860 | 43 | If it doesn't match a L{Thing} known to this event, it will return | ||
1861 | 44 | C{otherMessage}. | ||
1862 | 45 | """ | ||
1863 | 40 | if observer is self.actor: | 46 | if observer is self.actor: |
1864 | 41 | msg = self.actorMessage | 47 | msg = self.actorMessage |
1865 | 42 | elif observer is self.target: | 48 | elif observer is self.target: |
1866 | @@ -65,13 +71,12 @@ | |||
1867 | 65 | L{Event}'s location when this method, L{Event.reify}, was called. | 71 | L{Event}'s location when this method, L{Event.reify}, was called. |
1868 | 66 | """ | 72 | """ |
1869 | 67 | L = [] | 73 | L = [] |
1877 | 68 | for ob in iimaginary.IContainer(self.location).getContents(): | 74 | for observer in (self.location.idea.obtain( |
1878 | 69 | observer = iimaginary.IEventObserver(ob, None) | 75 | Proximity(0.5, ProviderOf(iimaginary.IEventObserver)))): |
1879 | 70 | if observer: | 76 | sender = observer.prepare(self) |
1880 | 71 | sender = observer.prepare(self) | 77 | if not callable(sender): |
1881 | 72 | if not callable(sender): | 78 | raise TypeError("Senders must be callable", sender) |
1882 | 73 | raise TypeError("Senders must be callable", sender) | 79 | L.append(sender) |
1876 | 74 | L.append(sender) | ||
1883 | 75 | return lambda: map(apply, L) | 80 | return lambda: map(apply, L) |
1884 | 76 | 81 | ||
1885 | 77 | 82 | ||
1886 | @@ -163,7 +168,7 @@ | |||
1887 | 163 | raise | 168 | raise |
1888 | 164 | try: | 169 | try: |
1889 | 165 | result = store.transact(runHelper) | 170 | result = store.transact(runHelper) |
1891 | 166 | except eimaginary.ActionFailure, e: | 171 | except eimaginary.ActionFailure: |
1892 | 167 | broadcaster.broadcastRevertEvents() | 172 | broadcaster.broadcastRevertEvents() |
1893 | 168 | return None | 173 | return None |
1894 | 169 | else: | 174 | else: |
1895 | 170 | 175 | ||
1896 | === modified file 'Imaginary/imaginary/garments.py' | |||
1897 | --- Imaginary/imaginary/garments.py 2009-06-29 04:03:17 +0000 | |||
1898 | +++ Imaginary/imaginary/garments.py 2011-09-16 20:42:26 +0000 | |||
1899 | @@ -11,6 +11,9 @@ | |||
1900 | 11 | from axiom import item, attributes | 11 | from axiom import item, attributes |
1901 | 12 | 12 | ||
1902 | 13 | from imaginary import iimaginary, language, objects | 13 | from imaginary import iimaginary, language, objects |
1903 | 14 | from imaginary.eimaginary import ActionFailure | ||
1904 | 15 | from imaginary.events import ThatDoesntWork | ||
1905 | 16 | from imaginary.idea import Link | ||
1906 | 14 | from imaginary.creation import createCreator | 17 | from imaginary.creation import createCreator |
1907 | 15 | from imaginary.enhancement import Enhancement | 18 | from imaginary.enhancement import Enhancement |
1908 | 16 | 19 | ||
1909 | @@ -80,9 +83,17 @@ | |||
1910 | 80 | 83 | ||
1911 | 81 | 84 | ||
1912 | 82 | class Garment(item.Item, Enhancement): | 85 | class Garment(item.Item, Enhancement): |
1913 | 86 | """ | ||
1914 | 87 | An enhancement for a L{Thing} representing its utility as an article of | ||
1915 | 88 | clothing. | ||
1916 | 89 | """ | ||
1917 | 83 | implements(iimaginary.IClothing, | 90 | implements(iimaginary.IClothing, |
1920 | 84 | iimaginary.IDescriptionContributor) | 91 | iimaginary.IDescriptionContributor, |
1921 | 85 | powerupInterfaces = (iimaginary.IClothing, iimaginary.IDescriptionContributor) | 92 | iimaginary.IMovementRestriction) |
1922 | 93 | |||
1923 | 94 | powerupInterfaces = (iimaginary.IClothing, | ||
1924 | 95 | iimaginary.IDescriptionContributor, | ||
1925 | 96 | iimaginary.IMovementRestriction) | ||
1926 | 86 | 97 | ||
1927 | 87 | thing = attributes.reference() | 98 | thing = attributes.reference() |
1928 | 88 | 99 | ||
1929 | @@ -113,6 +124,43 @@ | |||
1930 | 113 | return self.garmentDescription | 124 | return self.garmentDescription |
1931 | 114 | 125 | ||
1932 | 115 | 126 | ||
1933 | 127 | def nowWornBy(self, wearer): | ||
1934 | 128 | """ | ||
1935 | 129 | This garment is now worn by the given wearer. As this garment is now | ||
1936 | 130 | on top, set its C{wearLevel} to be higher than any other L{Garment} | ||
1937 | 131 | related to the new C{wearer}. | ||
1938 | 132 | """ | ||
1939 | 133 | self.wearer = wearer | ||
1940 | 134 | self.wearLevel = wearer.store.query( | ||
1941 | 135 | Garment, | ||
1942 | 136 | Garment.wearer == wearer).getColumn("wearLevel").max(default=0) + 1 | ||
1943 | 137 | |||
1944 | 138 | |||
1945 | 139 | def noLongerWorn(self): | ||
1946 | 140 | """ | ||
1947 | 141 | This garment is no longer being worn by anyone. | ||
1948 | 142 | """ | ||
1949 | 143 | self.wearer = None | ||
1950 | 144 | self.wearLevel = None | ||
1951 | 145 | |||
1952 | 146 | |||
1953 | 147 | def movementImminent(self, movee, destination): | ||
1954 | 148 | """ | ||
1955 | 149 | Something is trying to move. Don't allow it if I'm currently worn. | ||
1956 | 150 | """ | ||
1957 | 151 | if self.wearer is not None and movee is self.thing: | ||
1958 | 152 | raise ActionFailure( | ||
1959 | 153 | ThatDoesntWork( | ||
1960 | 154 | # XXX I don't actually know who is performing the action | ||
1961 | 155 | # :-(. | ||
1962 | 156 | actor=self.wearer.thing, | ||
1963 | 157 | actorMessage=[ | ||
1964 | 158 | "You can't move ", | ||
1965 | 159 | language.Noun(self.thing).definiteNounPhrase(), | ||
1966 | 160 | " without removing it first."])) | ||
1967 | 161 | |||
1968 | 162 | |||
1969 | 163 | |||
1970 | 116 | def _orderTopClothingByGlobalSlotList(tempClothes): | 164 | def _orderTopClothingByGlobalSlotList(tempClothes): |
1971 | 117 | """ | 165 | """ |
1972 | 118 | This function orders a dict as returned by getGarmentDict in the order that | 166 | This function orders a dict as returned by getGarmentDict in the order that |
1973 | @@ -154,9 +202,15 @@ | |||
1974 | 154 | person or mannequin. | 202 | person or mannequin. |
1975 | 155 | """ | 203 | """ |
1976 | 156 | 204 | ||
1980 | 157 | implements(iimaginary.IClothingWearer, iimaginary.IDescriptionContributor) | 205 | _interfaces = (iimaginary.IClothingWearer, |
1981 | 158 | powerupInterfaces = (iimaginary.IClothingWearer, iimaginary.IDescriptionContributor, | 206 | iimaginary.IDescriptionContributor, |
1982 | 159 | iimaginary.ILinkContributor) | 207 | iimaginary.ILinkContributor, |
1983 | 208 | iimaginary.ILinkAnnotator, | ||
1984 | 209 | ) | ||
1985 | 210 | |||
1986 | 211 | implements(*_interfaces) | ||
1987 | 212 | |||
1988 | 213 | powerupInterfaces = _interfaces | ||
1989 | 160 | 214 | ||
1990 | 161 | 215 | ||
1991 | 162 | thing = attributes.reference() | 216 | thing = attributes.reference() |
1992 | @@ -172,27 +226,52 @@ | |||
1993 | 172 | 226 | ||
1994 | 173 | 227 | ||
1995 | 174 | def putOn(self, newGarment): | 228 | def putOn(self, newGarment): |
1996 | 229 | """ | ||
1997 | 230 | Wear a new L{Garment} on this L{Wearer}, first moving it to this | ||
1998 | 231 | L{Wearer}'s C{thing} if it is not already there. | ||
1999 | 232 | |||
2000 | 233 | @param newGarment: the article of clothing to wear. | ||
2001 | 234 | |||
2002 | 235 | @type newGarment: L{Garment} | ||
2003 | 236 | |||
2004 | 237 | @raise TooBulky: if the bulk of any of the slots occupied by | ||
2005 | 238 | C{newGarment} is greater than the bulk of any other clothing | ||
2006 | 239 | already in that slot. (For example, if you tried to wear a T-shirt | ||
2007 | 240 | over a heavy coat.) | ||
2008 | 241 | """ | ||
2009 | 175 | c = self.getGarmentDict() | 242 | c = self.getGarmentDict() |
2010 | 176 | for garmentSlot in newGarment.garmentSlots: | 243 | for garmentSlot in newGarment.garmentSlots: |
2011 | 177 | if garmentSlot in c: | 244 | if garmentSlot in c: |
2012 | 178 | # We don't want to be able to wear T-shirts over heavy coats; | ||
2013 | 179 | # therefore, heavy coats have a high "bulk" | ||
2014 | 180 | currentTopOfSlot = c[garmentSlot][-1] | 245 | currentTopOfSlot = c[garmentSlot][-1] |
2015 | 181 | if currentTopOfSlot.bulk >= newGarment.bulk: | 246 | if currentTopOfSlot.bulk >= newGarment.bulk: |
2016 | 182 | raise TooBulky(currentTopOfSlot, newGarment) | 247 | raise TooBulky(currentTopOfSlot, newGarment) |
2017 | 183 | 248 | ||
2021 | 184 | newGarment.thing.moveTo(None) | 249 | newGarment.thing.moveTo(self.thing) |
2022 | 185 | newGarment.wearer = self | 250 | newGarment.nowWornBy(self) |
2020 | 186 | newGarment.wearLevel = self.store.query(Garment, Garment.wearer == self).getColumn("wearLevel").max(default=0) + 1 | ||
2023 | 187 | 251 | ||
2024 | 188 | 252 | ||
2025 | 189 | def takeOff(self, garment): | 253 | def takeOff(self, garment): |
2026 | 254 | """ | ||
2027 | 255 | Remove a garment which this player is wearing. | ||
2028 | 256 | |||
2029 | 257 | (Note: no error checking is currently performed to see if this garment | ||
2030 | 258 | is actually already worn by this L{Wearer}.) | ||
2031 | 259 | |||
2032 | 260 | @param garment: the article of clothing to remove. | ||
2033 | 261 | |||
2034 | 262 | @type garment: L{Garment} | ||
2035 | 263 | |||
2036 | 264 | @raise InaccessibleGarment: if the garment is obscured by any other | ||
2037 | 265 | clothing, and is therefore not in the top slot for any of the slots | ||
2038 | 266 | it occupies. For example, if you put on an undershirt, then a | ||
2039 | 267 | turtleneck, you can't remove the undershirt without removing the | ||
2040 | 268 | turtleneck first. | ||
2041 | 269 | """ | ||
2042 | 190 | gdict = self.getGarmentDict() | 270 | gdict = self.getGarmentDict() |
2043 | 191 | for slot in garment.garmentSlots: | 271 | for slot in garment.garmentSlots: |
2044 | 192 | if gdict[slot][-1] is not garment: | 272 | if gdict[slot][-1] is not garment: |
2045 | 193 | raise InaccessibleGarment(self, garment, gdict[slot][-1]) | 273 | raise InaccessibleGarment(self, garment, gdict[slot][-1]) |
2048 | 194 | garment.thing.moveTo(garment.wearer.thing) | 274 | garment.noLongerWorn() |
2047 | 195 | garment.wearer = garment.wearLevel = None | ||
2049 | 196 | 275 | ||
2050 | 197 | 276 | ||
2051 | 198 | # IDescriptionContributor | 277 | # IDescriptionContributor |
2052 | @@ -205,11 +284,45 @@ | |||
2053 | 205 | 284 | ||
2054 | 206 | # ILinkContributor | 285 | # ILinkContributor |
2055 | 207 | def links(self): | 286 | def links(self): |
2061 | 208 | d = {} | 287 | for garmentThing in self.store.query(objects.Thing, |
2062 | 209 | for t in self.store.query(objects.Thing, attributes.AND(Garment.thing == objects.Thing.storeID, | 288 | attributes.AND( |
2063 | 210 | Garment.wearer == self)): | 289 | Garment.thing == objects.Thing.storeID, |
2064 | 211 | d.setdefault(t.name, []).append(t) | 290 | Garment.wearer == self)): |
2065 | 212 | return d | 291 | yield Link(self.thing.idea, garmentThing.idea) |
2066 | 292 | |||
2067 | 293 | |||
2068 | 294 | # ILinkAnnotator | ||
2069 | 295 | def annotationsFor(self, link, idea): | ||
2070 | 296 | """ | ||
2071 | 297 | Tell the containment system to disregard containment relationships for | ||
2072 | 298 | which I will generate a link. | ||
2073 | 299 | """ | ||
2074 | 300 | if list(link.of(iimaginary.IContainmentRelationship)): | ||
2075 | 301 | if link.source.delegate is self.thing: | ||
2076 | 302 | clothing = iimaginary.IClothing(link.target.delegate, None) | ||
2077 | 303 | if clothing is not None: | ||
2078 | 304 | if clothing.wearer is self: | ||
2079 | 305 | yield _DisregardYourWearingIt() | ||
2080 | 306 | |||
2081 | 307 | |||
2082 | 308 | |||
2083 | 309 | class _DisregardYourWearingIt(object): | ||
2084 | 310 | """ | ||
2085 | 311 | This is an annotation, produced by L{Wearer} for containment relationships | ||
2086 | 312 | between people (who are containers) and the clothing that they're wearing. | ||
2087 | 313 | A hopefully temporary workaround for the fact that clothing is rendered in | ||
2088 | 314 | its own way and therefor shouldn't show up in the list of a person's | ||
2089 | 315 | contents. | ||
2090 | 316 | """ | ||
2091 | 317 | implements(iimaginary.IElectromagneticMedium) | ||
2092 | 318 | |||
2093 | 319 | def isOpaque(self): | ||
2094 | 320 | """ | ||
2095 | 321 | I am opaque, so that clothing will show up only once (in your "wearing" | ||
2096 | 322 | list, rather than there and in your "contained" list), and obscured | ||
2097 | 323 | clothing won't show up at all. | ||
2098 | 324 | """ | ||
2099 | 325 | return True | ||
2100 | 213 | 326 | ||
2101 | 214 | 327 | ||
2102 | 215 | 328 | ||
2103 | 216 | 329 | ||
2104 | === added file 'Imaginary/imaginary/idea.py' | |||
2105 | --- Imaginary/imaginary/idea.py 1970-01-01 00:00:00 +0000 | |||
2106 | +++ Imaginary/imaginary/idea.py 2011-09-16 20:42:26 +0000 | |||
2107 | @@ -0,0 +1,625 @@ | |||
2108 | 1 | # -*- test-case-name: imaginary -*- | ||
2109 | 2 | |||
2110 | 3 | """ | ||
2111 | 4 | This module implements a highly abstract graph-traversal system for actions and | ||
2112 | 5 | events to locate the objects which can respond to them. The top-level | ||
2113 | 6 | entry-point to this system is L{Idea.obtain}. | ||
2114 | 7 | |||
2115 | 8 | It also implements several basic retrievers related to visibility and physical | ||
2116 | 9 | reachability. | ||
2117 | 10 | """ | ||
2118 | 11 | |||
2119 | 12 | from zope.interface import implements | ||
2120 | 13 | from epsilon.structlike import record | ||
2121 | 14 | |||
2122 | 15 | from imaginary.iimaginary import ( | ||
2123 | 16 | INameable, ILitLink, IThing, IObstruction, IElectromagneticMedium, | ||
2124 | 17 | IDistance, IRetriever, IExit) | ||
2125 | 18 | |||
2126 | 19 | |||
2127 | 20 | |||
2128 | 21 | class Link(record("source target")): | ||
2129 | 22 | """ | ||
2130 | 23 | A L{Link} is a connection between two L{Idea}s in a L{Path}. | ||
2131 | 24 | |||
2132 | 25 | @ivar source: the idea that this L{Link} originated from. | ||
2133 | 26 | @type source: L{Idea} | ||
2134 | 27 | |||
2135 | 28 | @ivar target: the idea that this L{Link} refers to. | ||
2136 | 29 | @type target: L{Idea} | ||
2137 | 30 | """ | ||
2138 | 31 | |||
2139 | 32 | def __init__(self, *a, **k): | ||
2140 | 33 | super(Link, self).__init__(*a, **k) | ||
2141 | 34 | self.annotations = [] | ||
2142 | 35 | |||
2143 | 36 | |||
2144 | 37 | def annotate(self, annotations): | ||
2145 | 38 | """ | ||
2146 | 39 | Annotate this link with a list of annotations. | ||
2147 | 40 | """ | ||
2148 | 41 | self.annotations.extend(annotations) | ||
2149 | 42 | |||
2150 | 43 | |||
2151 | 44 | def of(self, interface): | ||
2152 | 45 | """ | ||
2153 | 46 | Yield all annotations on this link which provide the given interface. | ||
2154 | 47 | """ | ||
2155 | 48 | for annotation in self.annotations: | ||
2156 | 49 | provider = interface(annotation, None) | ||
2157 | 50 | if provider is not None: | ||
2158 | 51 | yield provider | ||
2159 | 52 | |||
2160 | 53 | |||
2161 | 54 | |||
2162 | 55 | class Path(record('links')): | ||
2163 | 56 | """ | ||
2164 | 57 | A list of L{Link}s. | ||
2165 | 58 | """ | ||
2166 | 59 | |||
2167 | 60 | def of(self, interface): | ||
2168 | 61 | """ | ||
2169 | 62 | @return: an iterator of providers of interfaces, adapted from each link | ||
2170 | 63 | in this path. | ||
2171 | 64 | """ | ||
2172 | 65 | for link in self.links: | ||
2173 | 66 | for annotation in link.of(interface): | ||
2174 | 67 | yield annotation | ||
2175 | 68 | |||
2176 | 69 | |||
2177 | 70 | def eachTargetAs(self, interface): | ||
2178 | 71 | """ | ||
2179 | 72 | @return: an iterable of all non-None results of each L{Link.targetAs} | ||
2180 | 73 | method in this L{Path}'s C{links} attribute. | ||
2181 | 74 | """ | ||
2182 | 75 | for link in self.links: | ||
2183 | 76 | provider = interface(link.target.delegate, None) | ||
2184 | 77 | if provider is not None: | ||
2185 | 78 | yield provider | ||
2186 | 79 | |||
2187 | 80 | |||
2188 | 81 | def targetAs(self, interface): | ||
2189 | 82 | """ | ||
2190 | 83 | Retrieve the target of the last link of this path, its final | ||
2191 | 84 | destination, as a given interface. | ||
2192 | 85 | |||
2193 | 86 | @param interface: the interface to retrieve. | ||
2194 | 87 | @type interface: L{zope.interface.interfaces.IInterface} | ||
2195 | 88 | |||
2196 | 89 | @return: the last link's target, adapted to the given interface, or | ||
2197 | 90 | C{None} if no appropriate adapter or component exists. | ||
2198 | 91 | @rtype: C{interface} or C{NoneType} | ||
2199 | 92 | """ | ||
2200 | 93 | return interface(self.links[-1].target.delegate, None) | ||
2201 | 94 | |||
2202 | 95 | |||
2203 | 96 | def isCyclic(self): | ||
2204 | 97 | """ | ||
2205 | 98 | Determine if this path is cyclic, to avoid descending down infinite | ||
2206 | 99 | loops. | ||
2207 | 100 | |||
2208 | 101 | @return: a boolean indicating whether this L{Path} is cyclic or not, | ||
2209 | 102 | i.e. whether the L{Idea} its last link points at is the source of | ||
2210 | 103 | any of its links. | ||
2211 | 104 | """ | ||
2212 | 105 | if len(self.links) < 2: | ||
2213 | 106 | return False | ||
2214 | 107 | return (self.links[-1].target in (x.source for x in self.links)) | ||
2215 | 108 | |||
2216 | 109 | |||
2217 | 110 | def to(self, link): | ||
2218 | 111 | """ | ||
2219 | 112 | Create a new path, extending this one by one new link. | ||
2220 | 113 | """ | ||
2221 | 114 | return Path(self.links + [link]) | ||
2222 | 115 | |||
2223 | 116 | |||
2224 | 117 | def __repr__(self): | ||
2225 | 118 | """ | ||
2226 | 119 | @return: an expanded pretty-printed representation of this Path, | ||
2227 | 120 | suitable for debugging. | ||
2228 | 121 | """ | ||
2229 | 122 | s = 'Path(' | ||
2230 | 123 | for link in self.links: | ||
2231 | 124 | dlgt = link.target.delegate | ||
2232 | 125 | src = link.source.delegate | ||
2233 | 126 | s += "\n\t" | ||
2234 | 127 | s += repr(getattr(src, 'name', src)) | ||
2235 | 128 | s += " => " | ||
2236 | 129 | s += repr(getattr(dlgt, 'name', dlgt)) | ||
2237 | 130 | s += " " | ||
2238 | 131 | s += repr(link.annotations) | ||
2239 | 132 | s += ')' | ||
2240 | 133 | return s | ||
2241 | 134 | |||
2242 | 135 | |||
2243 | 136 | |||
2244 | 137 | class Idea(record("delegate linkers annotators")): | ||
2245 | 138 | """ | ||
2246 | 139 | Consider a person's activities with the world around them as having two | ||
2247 | 140 | layers. One is a physical layer, out in the world, composed of matter and | ||
2248 | 141 | energy. The other is a cognitive layer, internal to the person, composed | ||
2249 | 142 | of ideas about that matter and energy. | ||
2250 | 143 | |||
2251 | 144 | For example, when a person wants to sit in a wooden chair, they must first | ||
2252 | 145 | visually locate the arrangement of wood in question, make the determination | ||
2253 | 146 | of that it is a "chair" based on its properties, and then perform the | ||
2254 | 147 | appropriate actions to sit upon it. | ||
2255 | 148 | |||
2256 | 149 | However, a person may also interact with symbolic abstractions rather than | ||
2257 | 150 | physical objects. They may read a word, or point at a window on a computer | ||
2258 | 151 | screen. An L{Idea} is a representation of the common unit that can be | ||
2259 | 152 | referred to in this way. | ||
2260 | 153 | |||
2261 | 154 | Both physical and cognitive layers are present in Imaginary. The cognitive | ||
2262 | 155 | layer is modeled by L{imaginary.idea}. The physical layer is modeled by a | ||
2263 | 156 | rudimentary point-of-interest simulation in L{imaginary.objects}. An | ||
2264 | 157 | L{imaginary.thing.Thing} is a physical object; an L{Idea} is a node in a | ||
2265 | 158 | non-physical graph, related by links that are annotated to describe the | ||
2266 | 159 | nature of the relationship between it and other L{Idea}s. | ||
2267 | 160 | |||
2268 | 161 | L{Idea} is the most abstract unit of simulation. It does not have any | ||
2269 | 162 | behavior or simulation semantics of its own; it merely ties together | ||
2270 | 163 | different related systems. | ||
2271 | 164 | |||
2272 | 165 | An L{Idea} is composed of a C{delegate}, which is an object that implements | ||
2273 | 166 | simulation-defined interfaces; a list of L{ILinkContributor}s, which | ||
2274 | 167 | produce L{Link}s to other L{Idea}s, an a set of C{ILinkAnnotator}s, which | ||
2275 | 168 | apply annotations (which themselves implement simulation-defined | ||
2276 | 169 | link-annotation interfaces) to those links. | ||
2277 | 170 | |||
2278 | 171 | Each L{imaginary.thing.Thing} has a corresponding L{Idea} to represent it | ||
2279 | 172 | in the simulation. The physical simulation defines only a few types of | ||
2280 | 173 | links: objects have links to their containers, containers have links to | ||
2281 | 174 | their contents, rooms have links to their exits, exits have links to their | ||
2282 | 175 | destinations. Any L{imaginary.thing.Thing} can have a powerup applied to | ||
2283 | 176 | it which adds to the list of linkers or annotators for its L{Idea}, | ||
2284 | 177 | however, which allows users to create arbitrary objects. | ||
2285 | 178 | |||
2286 | 179 | For example, the target of the "look" action must implement | ||
2287 | 180 | L{imaginary.iimaginary.IVisible}, but need not be a | ||
2288 | 181 | L{iimaginary.objects.Thing}. A simulation might want to provide a piece of | ||
2289 | 182 | graffiti that you could look at, but would not be a physical object, in the | ||
2290 | 183 | sense that you couldn't pick it up, weigh it, push it, etc. Such an object | ||
2291 | 184 | could be implemented as a powerup for both | ||
2292 | 185 | L{imaginary.iimaginary.IDescriptionContributor}, which would impart some | ||
2293 | 186 | short flavor text to the room, and L{imaginary.iimaginary.IVisible}, which | ||
2294 | 187 | would be an acceptable target of 'look'. The | ||
2295 | 188 | L{imaginary.iimaginary.IVisible} implementation could even be an in-memory | ||
2296 | 189 | object, not stored in the database at all; and there could be different | ||
2297 | 190 | implementations for different observers, depending on their level of | ||
2298 | 191 | knowledge about the in-world graffiti. | ||
2299 | 192 | |||
2300 | 193 | @ivar delegate: this object is the object which may be adaptable to a set | ||
2301 | 194 | of interfaces. This L{Idea} delegates all adaptation to its delegate. | ||
2302 | 195 | In many cases (when referring to a physical object), this will be an | ||
2303 | 196 | L{imaginary.thing.Thing}, but not necessarily. | ||
2304 | 197 | |||
2305 | 198 | @ivar linkers: a L{list} of L{ILinkContributor}s which are used to gather | ||
2306 | 199 | L{Link}s from this L{Idea} during L{Idea.obtain} traversal. | ||
2307 | 200 | |||
2308 | 201 | @ivar annotators: a L{list} of L{ILinkAnnotator}s which are used to annotate | ||
2309 | 202 | L{Link}s gathered from this L{Idea} via the C{linkers} list. | ||
2310 | 203 | """ | ||
2311 | 204 | |||
2312 | 205 | def __init__(self, delegate): | ||
2313 | 206 | super(Idea, self).__init__(delegate, [], []) | ||
2314 | 207 | |||
2315 | 208 | |||
2316 | 209 | def _allLinks(self): | ||
2317 | 210 | """ | ||
2318 | 211 | Return an iterator of all L{Links} away from this idea. | ||
2319 | 212 | """ | ||
2320 | 213 | for linker in self.linkers: | ||
2321 | 214 | for link in linker.links(): | ||
2322 | 215 | yield link | ||
2323 | 216 | |||
2324 | 217 | |||
2325 | 218 | def _applyAnnotators(self, linkiter): | ||
2326 | 219 | """ | ||
2327 | 220 | Apply my list of annotators to each link in the given iterable. | ||
2328 | 221 | """ | ||
2329 | 222 | for link in linkiter: | ||
2330 | 223 | self._annotateOneLink(link) | ||
2331 | 224 | yield link | ||
2332 | 225 | |||
2333 | 226 | |||
2334 | 227 | def _annotateOneLink(self, link): | ||
2335 | 228 | """ | ||
2336 | 229 | Apply all L{ILinkAnnotator}s in this L{Idea}'s C{annotators} list. | ||
2337 | 230 | """ | ||
2338 | 231 | allAnnotations = [] | ||
2339 | 232 | for annotator in self.annotators: | ||
2340 | 233 | # XXX important to test: annotators shouldn't mutate the links. | ||
2341 | 234 | # The annotators show up in a non-deterministic order, so in order | ||
2342 | 235 | # to facilitate a consistent view of the link in annotationsFor(), | ||
2343 | 236 | # all annotations are applied at the end. | ||
2344 | 237 | allAnnotations.extend(annotator.annotationsFor(link, self)) | ||
2345 | 238 | link.annotate(allAnnotations) | ||
2346 | 239 | |||
2347 | 240 | |||
2348 | 241 | def obtain(self, retriever): | ||
2349 | 242 | """ | ||
2350 | 243 | Traverse the graph of L{Idea}s, starting with C{self}, looking for | ||
2351 | 244 | objects which the given L{IRetriever} can retrieve. | ||
2352 | 245 | |||
2353 | 246 | The graph will be traversed by looking at all the links generated by | ||
2354 | 247 | this L{Idea}'s C{linkers}, only continuing down those links for which | ||
2355 | 248 | the given L{IRetriever}'s C{shouldKeepGoing} returns L{True}. | ||
2356 | 249 | |||
2357 | 250 | @param retriever: an object which will be passed each L{Path} in turn, | ||
2358 | 251 | discovered during traversal of the L{Idea} graph. If any | ||
2359 | 252 | invocation of L{IRetriever.retrieve} on this parameter should | ||
2360 | 253 | succeed, that will be yielded as a result from this method. | ||
2361 | 254 | @type retriever: L{IRetriever} | ||
2362 | 255 | |||
2363 | 256 | @return: a generator which yields the results of C{retriever.retrieve} | ||
2364 | 257 | which are not L{None}. | ||
2365 | 258 | """ | ||
2366 | 259 | return ObtainResult(self, retriever) | ||
2367 | 260 | |||
2368 | 261 | |||
2369 | 262 | def _doObtain(self, retriever, path, reasonsWhyNot): | ||
2370 | 263 | """ | ||
2371 | 264 | A generator that implements the logic for obtain() | ||
2372 | 265 | """ | ||
2373 | 266 | if path is None: | ||
2374 | 267 | # Special case: we only get a self->self link if we are the | ||
2375 | 268 | # beginning _and_ the end. | ||
2376 | 269 | path = Path([]) | ||
2377 | 270 | selfLink = Link(self, self) | ||
2378 | 271 | self._annotateOneLink(selfLink) | ||
2379 | 272 | finalPath = path.to(selfLink) | ||
2380 | 273 | else: | ||
2381 | 274 | finalPath = Path(path.links[:]) | ||
2382 | 275 | self._annotateOneLink(finalPath.links[-1]) | ||
2383 | 276 | |||
2384 | 277 | result = retriever.retrieve(finalPath) | ||
2385 | 278 | objections = set(retriever.objectionsTo(finalPath, result)) | ||
2386 | 279 | reasonsWhyNot |= objections | ||
2387 | 280 | if result is not None: | ||
2388 | 281 | if not objections: | ||
2389 | 282 | yield result | ||
2390 | 283 | |||
2391 | 284 | for link in self._applyAnnotators(self._allLinks()): | ||
2392 | 285 | subpath = path.to(link) | ||
2393 | 286 | if subpath.isCyclic(): | ||
2394 | 287 | continue | ||
2395 | 288 | if retriever.shouldKeepGoing(subpath): | ||
2396 | 289 | for obtained in link.target._doObtain(retriever, subpath, reasonsWhyNot): | ||
2397 | 290 | yield obtained | ||
2398 | 291 | |||
2399 | 292 | |||
2400 | 293 | |||
2401 | 294 | class ObtainResult(record("idea retriever")): | ||
2402 | 295 | """ | ||
2403 | 296 | The result of L{Idea.obtain}, this provides an iterable of results. | ||
2404 | 297 | |||
2405 | 298 | @ivar reasonsWhyNot: If this iterator has already been exhausted, this will | ||
2406 | 299 | be a C{set} of L{IWhyNot} objects explaining possible reasons why there | ||
2407 | 300 | were no results. For example, if the room where the player attempted | ||
2408 | 301 | to obtain targets is dark, this may contain an L{IWhyNot} provider. | ||
2409 | 302 | However, until this iterator has been exhausted, it will be C{None}. | ||
2410 | 303 | @type reasonsWhyNot: C{set} of L{IWhyNot}, or C{NoneType} | ||
2411 | 304 | |||
2412 | 305 | @ivar idea: the L{Idea} that L{Idea.obtain} was invoked on. | ||
2413 | 306 | @type idea: L{Idea} | ||
2414 | 307 | |||
2415 | 308 | @ivar retriever: The L{IRetriever} that L{Idea.obtain} was invoked with. | ||
2416 | 309 | @type retriever: L{IRetriever} | ||
2417 | 310 | """ | ||
2418 | 311 | |||
2419 | 312 | reasonsWhyNot = None | ||
2420 | 313 | |||
2421 | 314 | def __iter__(self): | ||
2422 | 315 | """ | ||
2423 | 316 | A generator which yields each result of the query, then sets | ||
2424 | 317 | C{reasonsWhyNot}. | ||
2425 | 318 | """ | ||
2426 | 319 | reasonsWhyNot = set() | ||
2427 | 320 | for result in self.idea._doObtain(self.retriever, None, reasonsWhyNot): | ||
2428 | 321 | yield result | ||
2429 | 322 | self.reasonsWhyNot = reasonsWhyNot | ||
2430 | 323 | |||
2431 | 324 | |||
2432 | 325 | |||
2433 | 326 | class DelegatingRetriever(object): | ||
2434 | 327 | """ | ||
2435 | 328 | A delegating retriever, so that retrievers can be easily composed. | ||
2436 | 329 | |||
2437 | 330 | See the various methods marked for overriding. | ||
2438 | 331 | |||
2439 | 332 | @ivar retriever: A retriever to delegate most operations to. | ||
2440 | 333 | @type retriever: L{IRetriever} | ||
2441 | 334 | """ | ||
2442 | 335 | |||
2443 | 336 | implements(IRetriever) | ||
2444 | 337 | |||
2445 | 338 | def __init__(self, retriever): | ||
2446 | 339 | """ | ||
2447 | 340 | Create a delegator with a retriever to delegate to. | ||
2448 | 341 | """ | ||
2449 | 342 | self.retriever = retriever | ||
2450 | 343 | |||
2451 | 344 | |||
2452 | 345 | def moreObjectionsTo(self, path, result): | ||
2453 | 346 | """ | ||
2454 | 347 | Override in subclasses to yield objections to add to this | ||
2455 | 348 | L{DelegatingRetriever}'s C{retriever}'s C{objectionsTo}. | ||
2456 | 349 | |||
2457 | 350 | By default, offer no additional objections. | ||
2458 | 351 | """ | ||
2459 | 352 | return [] | ||
2460 | 353 | |||
2461 | 354 | |||
2462 | 355 | def objectionsTo(self, path, result): | ||
2463 | 356 | """ | ||
2464 | 357 | Concatenate C{self.moreObjectionsTo} with C{self.moreObjectionsTo}. | ||
2465 | 358 | """ | ||
2466 | 359 | for objection in self.retriever.objectionsTo(path, result): | ||
2467 | 360 | yield objection | ||
2468 | 361 | for objection in self.moreObjectionsTo(path, result): | ||
2469 | 362 | yield objection | ||
2470 | 363 | |||
2471 | 364 | |||
2472 | 365 | def shouldStillKeepGoing(self, path): | ||
2473 | 366 | """ | ||
2474 | 367 | Override in subclasses to halt traversal via a C{False} return value for | ||
2475 | 368 | C{shouldKeepGoing} if this L{DelegatingRetriever}'s C{retriever}'s | ||
2476 | 369 | C{shouldKeepGoing} returns C{True}. | ||
2477 | 370 | |||
2478 | 371 | By default, return C{True} to keep going. | ||
2479 | 372 | """ | ||
2480 | 373 | return True | ||
2481 | 374 | |||
2482 | 375 | |||
2483 | 376 | def shouldKeepGoing(self, path): | ||
2484 | 377 | """ | ||
2485 | 378 | If this L{DelegatingRetriever}'s C{retriever}'s C{shouldKeepGoing} | ||
2486 | 379 | returns C{False} for the given path, return C{False} and stop | ||
2487 | 380 | traversing. Otherwise, delegate to C{shouldStillKeepGoing}. | ||
2488 | 381 | """ | ||
2489 | 382 | return (self.retriever.shouldKeepGoing(path) and | ||
2490 | 383 | self.shouldStillKeepGoing(path)) | ||
2491 | 384 | |||
2492 | 385 | |||
2493 | 386 | def resultRetrieved(self, path, retrievedResult): | ||
2494 | 387 | """ | ||
2495 | 388 | A result was retrieved. Post-process it if desired. | ||
2496 | 389 | |||
2497 | 390 | Override this in subclasses to modify (non-None) results returned from | ||
2498 | 391 | this L{DelegatingRetriever}'s C{retriever}'s C{retrieve} method. | ||
2499 | 392 | |||
2500 | 393 | By default, simply return the result retrieved. | ||
2501 | 394 | """ | ||
2502 | 395 | return retrievedResult | ||
2503 | 396 | |||
2504 | 397 | |||
2505 | 398 | def retrieve(self, path): | ||
2506 | 399 | """ | ||
2507 | 400 | Delegate to this L{DelegatingRetriever}'s C{retriever}'s C{retrieve} | ||
2508 | 401 | method, then post-process it with C{resultRetrieved}. | ||
2509 | 402 | """ | ||
2510 | 403 | subResult = self.retriever.retrieve(path) | ||
2511 | 404 | if subResult is None: | ||
2512 | 405 | return None | ||
2513 | 406 | return self.resultRetrieved(path, subResult) | ||
2514 | 407 | |||
2515 | 408 | |||
2516 | 409 | |||
2517 | 410 | class Proximity(DelegatingRetriever): | ||
2518 | 411 | """ | ||
2519 | 412 | L{Proximity} is a retriever which will continue traversing any path which | ||
2520 | 413 | is shorter than its proscribed distance, but not any longer. | ||
2521 | 414 | |||
2522 | 415 | @ivar distance: the distance, in meters, to query for. | ||
2523 | 416 | |||
2524 | 417 | @type distance: L{float} | ||
2525 | 418 | """ | ||
2526 | 419 | |||
2527 | 420 | def __init__(self, distance, retriever): | ||
2528 | 421 | DelegatingRetriever.__init__(self, retriever) | ||
2529 | 422 | self.distance = distance | ||
2530 | 423 | |||
2531 | 424 | |||
2532 | 425 | def shouldStillKeepGoing(self, path): | ||
2533 | 426 | """ | ||
2534 | 427 | Implement L{IRetriever.shouldKeepGoing} to stop for paths whose sum of | ||
2535 | 428 | L{IDistance} annotations is greater than L{Proximity.distance}. | ||
2536 | 429 | """ | ||
2537 | 430 | dist = sum(vector.distance for vector in path.of(IDistance)) | ||
2538 | 431 | ok = (self.distance >= dist) | ||
2539 | 432 | return ok | ||
2540 | 433 | |||
2541 | 434 | |||
2542 | 435 | |||
2543 | 436 | class Reachable(DelegatingRetriever): | ||
2544 | 437 | """ | ||
2545 | 438 | L{Reachable} is a navivator which will object to any path with an | ||
2546 | 439 | L{IObstruction} annotation on it. | ||
2547 | 440 | """ | ||
2548 | 441 | |||
2549 | 442 | def moreObjectionsTo(self, path, result): | ||
2550 | 443 | """ | ||
2551 | 444 | Yield an objection from each L{IObstruction.whyNot} method annotating | ||
2552 | 445 | the given path. | ||
2553 | 446 | """ | ||
2554 | 447 | if result is not None: | ||
2555 | 448 | for obstruction in path.of(IObstruction): | ||
2556 | 449 | yield obstruction.whyNot() | ||
2557 | 450 | |||
2558 | 451 | |||
2559 | 452 | |||
2560 | 453 | class Traversability(DelegatingRetriever): | ||
2561 | 454 | """ | ||
2562 | 455 | A path is only traversible if it terminates in *one* exit. Once you've | ||
2563 | 456 | gotten to an exit, you have to stop, because the player needs to go through | ||
2564 | 457 | that exit to get to the next one. | ||
2565 | 458 | """ | ||
2566 | 459 | |||
2567 | 460 | def shouldStillKeepGoing(self, path): | ||
2568 | 461 | """ | ||
2569 | 462 | Stop at the first exit that you find. | ||
2570 | 463 | """ | ||
2571 | 464 | for index, target in enumerate(path.eachTargetAs(IExit)): | ||
2572 | 465 | if index > 0: | ||
2573 | 466 | return False | ||
2574 | 467 | return True | ||
2575 | 468 | |||
2576 | 469 | |||
2577 | 470 | |||
2578 | 471 | class Vector(record('distance direction')): | ||
2579 | 472 | """ | ||
2580 | 473 | A L{Vector} is a link annotation which remembers a distance and a | ||
2581 | 474 | direction; for example, a link through a 'north' exit between rooms will | ||
2582 | 475 | have a direction of 'north' and a distance specified by that | ||
2583 | 476 | L{imaginary.objects.Exit} (defaulting to 1 meter). | ||
2584 | 477 | """ | ||
2585 | 478 | |||
2586 | 479 | implements(IDistance) | ||
2587 | 480 | |||
2588 | 481 | |||
2589 | 482 | |||
2590 | 483 | class ProviderOf(record("interface")): | ||
2591 | 484 | """ | ||
2592 | 485 | L{ProviderOf} is a retriever which will retrieve the facet which provides | ||
2593 | 486 | its C{interface}, if any exists at the terminus of the path. | ||
2594 | 487 | |||
2595 | 488 | @ivar interface: The interface which defines the type of values returned by | ||
2596 | 489 | the C{retrieve} method. | ||
2597 | 490 | @type interface: L{zope.interface.interfaces.IInterface} | ||
2598 | 491 | """ | ||
2599 | 492 | |||
2600 | 493 | implements(IRetriever) | ||
2601 | 494 | |||
2602 | 495 | def retrieve(self, path): | ||
2603 | 496 | """ | ||
2604 | 497 | Retrieve the target of the path, as it provides the interface specified | ||
2605 | 498 | by this L{ProviderOf}. | ||
2606 | 499 | |||
2607 | 500 | @return: the target of the path, adapted to this retriever's interface, | ||
2608 | 501 | as defined by L{Path.targetAs}. | ||
2609 | 502 | |||
2610 | 503 | @rtype: L{ProviderOf.interface} | ||
2611 | 504 | """ | ||
2612 | 505 | return path.targetAs(self.interface) | ||
2613 | 506 | |||
2614 | 507 | |||
2615 | 508 | def objectionsTo(self, path, result): | ||
2616 | 509 | """ | ||
2617 | 510 | Implement L{IRetriever.objectionsTo} to yield no objections. | ||
2618 | 511 | """ | ||
2619 | 512 | return [] | ||
2620 | 513 | |||
2621 | 514 | |||
2622 | 515 | def shouldKeepGoing(self, path): | ||
2623 | 516 | """ | ||
2624 | 517 | Implement L{IRetriever.shouldKeepGoing} to always return C{True}. | ||
2625 | 518 | """ | ||
2626 | 519 | return True | ||
2627 | 520 | |||
2628 | 521 | |||
2629 | 522 | |||
2630 | 523 | class AlsoKnownAs(record('name')): | ||
2631 | 524 | """ | ||
2632 | 525 | L{AlsoKnownAs} is an annotation that indicates that the link it annotates | ||
2633 | 526 | is known as a particular name. | ||
2634 | 527 | |||
2635 | 528 | @ivar name: The name that this L{AlsoKnownAs}'s link's target is also known | ||
2636 | 529 | as. | ||
2637 | 530 | @type name: C{unicode} | ||
2638 | 531 | """ | ||
2639 | 532 | |||
2640 | 533 | implements(INameable) | ||
2641 | 534 | |||
2642 | 535 | def knownTo(self, observer, name): | ||
2643 | 536 | """ | ||
2644 | 537 | An L{AlsoKnownAs} is known to all observers as its C{name} attribute. | ||
2645 | 538 | """ | ||
2646 | 539 | return (self.name == name) | ||
2647 | 540 | |||
2648 | 541 | |||
2649 | 542 | |||
2650 | 543 | class Named(DelegatingRetriever): | ||
2651 | 544 | """ | ||
2652 | 545 | A retriever which wraps another retriever, but yields only results known to | ||
2653 | 546 | a particular observer by a particular name. | ||
2654 | 547 | |||
2655 | 548 | @ivar name: the name to search for. | ||
2656 | 549 | |||
2657 | 550 | @ivar observer: the observer who should identify the target by the name | ||
2658 | 551 | this L{Named} is searching for. | ||
2659 | 552 | @type observer: L{Thing} | ||
2660 | 553 | """ | ||
2661 | 554 | |||
2662 | 555 | def __init__(self, name, retriever, observer): | ||
2663 | 556 | DelegatingRetriever.__init__(self, retriever) | ||
2664 | 557 | self.name = name | ||
2665 | 558 | self.observer = observer | ||
2666 | 559 | |||
2667 | 560 | |||
2668 | 561 | def resultRetrieved(self, path, subResult): | ||
2669 | 562 | """ | ||
2670 | 563 | Invoke C{retrieve} on the L{IRetriever} which we wrap, but only return | ||
2671 | 564 | it if the L{INameable} target of the given path is known as this | ||
2672 | 565 | L{Named}'s C{name}. | ||
2673 | 566 | """ | ||
2674 | 567 | named = path.targetAs(INameable) | ||
2675 | 568 | allAliases = list(path.links[-1].of(INameable)) | ||
2676 | 569 | if named is not None: | ||
2677 | 570 | allAliases += [named] | ||
2678 | 571 | for alias in allAliases: | ||
2679 | 572 | if alias.knownTo(self.observer, self.name): | ||
2680 | 573 | return subResult | ||
2681 | 574 | return None | ||
2682 | 575 | |||
2683 | 576 | |||
2684 | 577 | |||
2685 | 578 | class CanSee(DelegatingRetriever): | ||
2686 | 579 | """ | ||
2687 | 580 | Wrap a L{ProviderOf}, yielding the results that it would yield, but | ||
2688 | 581 | applying lighting to the ultimate target based on the last L{IThing} the | ||
2689 | 582 | path. | ||
2690 | 583 | |||
2691 | 584 | @ivar retriever: The lowest-level retriever being wrapped. | ||
2692 | 585 | |||
2693 | 586 | @type retriever: L{ProviderOf} (Note: it might be a good idea to add an | ||
2694 | 587 | 'interface' attribute to L{IRetriever} so this no longer depends on a | ||
2695 | 588 | more specific type than other L{DelegatingRetriever}s, to make the | ||
2696 | 589 | order of composition more flexible.) | ||
2697 | 590 | """ | ||
2698 | 591 | |||
2699 | 592 | def resultRetrieved(self, path, subResult): | ||
2700 | 593 | """ | ||
2701 | 594 | Post-process retrieved results by determining if lighting applies to | ||
2702 | 595 | them. | ||
2703 | 596 | """ | ||
2704 | 597 | litlinks = list(path.of(ILitLink)) | ||
2705 | 598 | if not litlinks: | ||
2706 | 599 | return subResult | ||
2707 | 600 | # XXX what if there aren't any IThings on the path? | ||
2708 | 601 | litThing = list(path.eachTargetAs(IThing))[-1] | ||
2709 | 602 | # you need to be able to look from a light room to a dark room, so only | ||
2710 | 603 | # apply the most "recent" lighting properties. | ||
2711 | 604 | return litlinks[-1].applyLighting( | ||
2712 | 605 | litThing, subResult, self.retriever.interface) | ||
2713 | 606 | |||
2714 | 607 | |||
2715 | 608 | def shouldStillKeepGoing(self, path): | ||
2716 | 609 | """ | ||
2717 | 610 | Don't keep going through links that are opaque to the observer. | ||
2718 | 611 | """ | ||
2719 | 612 | for opacity in path.of(IElectromagneticMedium): | ||
2720 | 613 | if opacity.isOpaque(): | ||
2721 | 614 | return False | ||
2722 | 615 | return True | ||
2723 | 616 | |||
2724 | 617 | |||
2725 | 618 | def moreObjectionsTo(self, path, result): | ||
2726 | 619 | """ | ||
2727 | 620 | Object to paths which have L{ILitLink} annotations which are not lit. | ||
2728 | 621 | """ | ||
2729 | 622 | for lighting in path.of(ILitLink): | ||
2730 | 623 | if not lighting.isItLit(path, result): | ||
2731 | 624 | tmwn = lighting.whyNotLit() | ||
2732 | 625 | yield tmwn | ||
2733 | 0 | 626 | ||
2734 | === modified file 'Imaginary/imaginary/iimaginary.py' | |||
2735 | --- Imaginary/imaginary/iimaginary.py 2009-01-14 05:21:23 +0000 | |||
2736 | +++ Imaginary/imaginary/iimaginary.py 2011-09-16 20:42:26 +0000 | |||
2737 | @@ -40,16 +40,18 @@ | |||
2738 | 40 | A powerup interface which can add more connections between objects in the | 40 | A powerup interface which can add more connections between objects in the |
2739 | 41 | world graph. | 41 | world graph. |
2740 | 42 | 42 | ||
2743 | 43 | All ILinkContributors which are powered up on a particular Thing will be | 43 | All L{ILinkContributors} which are powered up on a particular |
2744 | 44 | given a chance to add to the L{IThing.link} method's return value. | 44 | L{imaginary.objects.Thing} will be appended to that |
2745 | 45 | L{imaginary.objects.Thing}'s value. | ||
2746 | 45 | """ | 46 | """ |
2747 | 46 | 47 | ||
2748 | 47 | def links(): | 48 | def links(): |
2749 | 48 | """ | 49 | """ |
2751 | 49 | Return a C{dict} mapping names of connections to C{IThings}. | 50 | @return: an iterable of L{imaginary.idea.Link}s. |
2752 | 50 | """ | 51 | """ |
2753 | 51 | 52 | ||
2754 | 52 | 53 | ||
2755 | 54 | |||
2756 | 53 | class IDescriptionContributor(Interface): | 55 | class IDescriptionContributor(Interface): |
2757 | 54 | """ | 56 | """ |
2758 | 55 | A powerup interface which can add text to the description of an object. | 57 | A powerup interface which can add text to the description of an object. |
2759 | @@ -64,6 +66,70 @@ | |||
2760 | 64 | """ | 66 | """ |
2761 | 65 | 67 | ||
2762 | 66 | 68 | ||
2763 | 69 | class INameable(Interface): | ||
2764 | 70 | """ | ||
2765 | 71 | A provider of L{INameable} is an object which can be identified by an | ||
2766 | 72 | imaginary actor by a name. | ||
2767 | 73 | """ | ||
2768 | 74 | |||
2769 | 75 | def knownTo(observer, name): | ||
2770 | 76 | """ | ||
2771 | 77 | Is this L{INameable} known to the given C{observer} by the given | ||
2772 | 78 | C{name}? | ||
2773 | 79 | |||
2774 | 80 | @param name: the name to test for | ||
2775 | 81 | |||
2776 | 82 | @type name: L{unicode} | ||
2777 | 83 | |||
2778 | 84 | @param observer: the thing which is observing this namable. | ||
2779 | 85 | |||
2780 | 86 | @type observer: L{IThing} | ||
2781 | 87 | |||
2782 | 88 | @rtype: L{bool} | ||
2783 | 89 | |||
2784 | 90 | @return: L{True} if C{name} identifies this L{INameable}, L{False} | ||
2785 | 91 | otherwise. | ||
2786 | 92 | """ | ||
2787 | 93 | |||
2788 | 94 | |||
2789 | 95 | class ILitLink(Interface): | ||
2790 | 96 | """ | ||
2791 | 97 | This interface is an annotation interface for L{imaginary.idea.Link} | ||
2792 | 98 | objects, for indicating that the link can apply lighting. | ||
2793 | 99 | """ | ||
2794 | 100 | |||
2795 | 101 | def applyLighting(litThing, eventualTarget, requestedInterface): | ||
2796 | 102 | """ | ||
2797 | 103 | Apply a transformation to an object that an | ||
2798 | 104 | L{imaginary.idea.Idea.obtain} is requesting, based on the light level | ||
2799 | 105 | of this link and its surroundings. | ||
2800 | 106 | |||
2801 | 107 | @param litThing: The L{IThing} to apply lighting to. | ||
2802 | 108 | |||
2803 | 109 | @type litThing: L{IThing} | ||
2804 | 110 | |||
2805 | 111 | @param eventualTarget: The eventual, ultimate target of the path in | ||
2806 | 112 | question. | ||
2807 | 113 | |||
2808 | 114 | @type eventualTarget: C{requestedInterface} | ||
2809 | 115 | |||
2810 | 116 | @param requestedInterface: The interface requested by the query that | ||
2811 | 117 | resulted in this path; this is the interface which | ||
2812 | 118 | C{eventualTarget} should implement. | ||
2813 | 119 | |||
2814 | 120 | @type requestedInterface: L{Interface} | ||
2815 | 121 | |||
2816 | 122 | @return: C{eventualTarget}, or, if this L{ILitLink} knows how to deal | ||
2817 | 123 | with lighting specifically for C{requestedInterface}, a modified | ||
2818 | 124 | version thereof which still implements C{requestedInterface}. If | ||
2819 | 125 | insufficient lighting results in the player being unable to access | ||
2820 | 126 | the desired object at all, C{None} will be returned. | ||
2821 | 127 | |||
2822 | 128 | @rtype: C{NoneType}, or C{requestedInterface} | ||
2823 | 129 | """ | ||
2824 | 130 | |||
2825 | 131 | |||
2826 | 132 | |||
2827 | 67 | 133 | ||
2828 | 68 | class IThing(Interface): | 134 | class IThing(Interface): |
2829 | 69 | """ | 135 | """ |
2830 | @@ -71,6 +137,12 @@ | |||
2831 | 71 | """ | 137 | """ |
2832 | 72 | location = Attribute("An IThing which contains this IThing") | 138 | location = Attribute("An IThing which contains this IThing") |
2833 | 73 | 139 | ||
2834 | 140 | proper = Attribute( | ||
2835 | 141 | "A boolean indicating the definiteness of this thing's pronoun.") | ||
2836 | 142 | |||
2837 | 143 | name = Attribute( | ||
2838 | 144 | "A unicode string, the name of this Thing.") | ||
2839 | 145 | |||
2840 | 74 | 146 | ||
2841 | 75 | def moveTo(where, arrivalEventFactory=None): | 147 | def moveTo(where, arrivalEventFactory=None): |
2842 | 76 | """ | 148 | """ |
2843 | @@ -78,7 +150,7 @@ | |||
2844 | 78 | 150 | ||
2845 | 79 | @type where: L{IThing} provider. | 151 | @type where: L{IThing} provider. |
2846 | 80 | @param where: The new location to be moved to. | 152 | @param where: The new location to be moved to. |
2848 | 81 | 153 | ||
2849 | 82 | @type arrivalEventFactory: A callable which takes a single | 154 | @type arrivalEventFactory: A callable which takes a single |
2850 | 83 | argument, the thing being moved, and returns an event. | 155 | argument, the thing being moved, and returns an event. |
2851 | 84 | @param arrivalEventFactory: Will be called to produce the | 156 | @param arrivalEventFactory: Will be called to produce the |
2852 | @@ -86,7 +158,6 @@ | |||
2853 | 86 | thing. If not specified (or None), no event will be broadcast. | 158 | thing. If not specified (or None), no event will be broadcast. |
2854 | 87 | """ | 159 | """ |
2855 | 88 | 160 | ||
2856 | 89 | |||
2857 | 90 | def findProviders(interface, distance): | 161 | def findProviders(interface, distance): |
2858 | 91 | """ | 162 | """ |
2859 | 92 | Retrieve all game objects which provide C{interface} within C{distance}. | 163 | Retrieve all game objects which provide C{interface} within C{distance}. |
2860 | @@ -95,19 +166,31 @@ | |||
2861 | 95 | """ | 166 | """ |
2862 | 96 | 167 | ||
2863 | 97 | 168 | ||
2877 | 98 | def proxiedThing(thing, interface, distance): | 169 | |
2878 | 99 | """ | 170 | class IMovementRestriction(Interface): |
2879 | 100 | Given an L{IThing} provider, return a provider of L{interface} as it is | 171 | """ |
2880 | 101 | accessible from C{self}. Any necessary proxies will be applied. | 172 | A L{MovementRestriction} is a powerup that can respond to a L{Thing}'s |
2881 | 102 | """ | 173 | movement before it occurs, and thereby restrict it. |
2882 | 103 | 174 | ||
2883 | 104 | 175 | Powerups of this type are consulted on L{Thing} before movement is allowed | |
2884 | 105 | def knownAs(name): | 176 | to complete. |
2885 | 106 | """ | 177 | """ |
2886 | 107 | Return a boolean indicating whether this thing might reasonably be | 178 | |
2887 | 108 | called C{name}. | 179 | def movementImminent(movee, destination): |
2888 | 109 | 180 | """ | |
2889 | 110 | @type name: C{unicode} | 181 | An object is about to move. Implementations can raise an exception if |
2890 | 182 | they wish to to prevent it. | ||
2891 | 183 | |||
2892 | 184 | @param movee: the object that is moving. | ||
2893 | 185 | |||
2894 | 186 | @type movee: L{Thing} | ||
2895 | 187 | |||
2896 | 188 | @param destination: The L{Thing} of the container that C{movee} will be | ||
2897 | 189 | moving to. | ||
2898 | 190 | |||
2899 | 191 | @type destination: L{IThing} | ||
2900 | 192 | |||
2901 | 193 | @raise Exception: if the movement is to be prevented. | ||
2902 | 111 | """ | 194 | """ |
2903 | 112 | 195 | ||
2904 | 113 | 196 | ||
2905 | @@ -116,6 +199,7 @@ | |||
2906 | 116 | hitpoints = Attribute("L{Points} instance representing hit points") | 199 | hitpoints = Attribute("L{Points} instance representing hit points") |
2907 | 117 | experience = Attribute("C{int} representing experience") | 200 | experience = Attribute("C{int} representing experience") |
2908 | 118 | level = Attribute("C{int} representing player's level") | 201 | level = Attribute("C{int} representing player's level") |
2909 | 202 | thing = Attribute("L{IThing} which represents the actor's physical body.") | ||
2910 | 119 | 203 | ||
2911 | 120 | def send(event): | 204 | def send(event): |
2912 | 121 | """Describe something to the actor. | 205 | """Describe something to the actor. |
2913 | @@ -224,22 +308,68 @@ | |||
2914 | 224 | 308 | ||
2915 | 225 | 309 | ||
2916 | 226 | 310 | ||
2917 | 311 | class IExit(Interface): | ||
2918 | 312 | """ | ||
2919 | 313 | An interface representing one direction that a player may move in. While | ||
2920 | 314 | L{IExit} only represents one half of a passageway, it is not necessarily | ||
2921 | 315 | one-way; in most cases, a parallel exit will exist on the other side. | ||
2922 | 316 | (However, it I{may} be one-way; there is no guarantee that you will be able | ||
2923 | 317 | to traverse it backwards, or even indeed that it will take you somewhere at | ||
2924 | 318 | all!) | ||
2925 | 319 | """ | ||
2926 | 320 | |||
2927 | 321 | name = Attribute( | ||
2928 | 322 | """ | ||
2929 | 323 | The name of this exit. This must be something adaptable to | ||
2930 | 324 | L{IConcept}, to display to players. | ||
2931 | 325 | """) | ||
2932 | 326 | |||
2933 | 327 | def traverse(thing): | ||
2934 | 328 | """ | ||
2935 | 329 | Attempt to move the given L{IThing} through this L{IExit} to the other | ||
2936 | 330 | side. (Note that this may not necessarily result in actual movement, | ||
2937 | 331 | if the exit does something tricky like disorienting you or hurting | ||
2938 | 332 | you.) | ||
2939 | 333 | |||
2940 | 334 | @param thing: Something which is passing through this exit. | ||
2941 | 335 | |||
2942 | 336 | @type thing: L{IThing} | ||
2943 | 337 | """ | ||
2944 | 338 | |||
2945 | 339 | |||
2946 | 340 | |||
2947 | 341 | |||
2948 | 342 | class IObstruction(Interface): | ||
2949 | 343 | """ | ||
2950 | 344 | An L{IObstruction} is a link annotation indicating that there is a physical | ||
2951 | 345 | obstruction preventing solid objects from reaching between the two ends of | ||
2952 | 346 | the link. For example, a closed door might annotate its link to its | ||
2953 | 347 | destination with an L{IObstruction}. | ||
2954 | 348 | """ | ||
2955 | 349 | |||
2956 | 350 | def whyNot(): | ||
2957 | 351 | """ | ||
2958 | 352 | @return: a reason why this is obstructed. | ||
2959 | 353 | |||
2960 | 354 | @rtype: L{IWhyNot} | ||
2961 | 355 | """ | ||
2962 | 356 | |||
2963 | 357 | |||
2964 | 358 | |||
2965 | 227 | class IContainer(Interface): | 359 | class IContainer(Interface): |
2966 | 228 | """ | 360 | """ |
2967 | 229 | An object which can contain other objects. | 361 | An object which can contain other objects. |
2968 | 230 | """ | 362 | """ |
2981 | 231 | capacity = Attribute(""" | 363 | capacity = Attribute( |
2982 | 232 | The maximum weight this container is capable of holding. | 364 | """ |
2983 | 233 | """) | 365 | The maximum weight this container is capable of holding. |
2984 | 234 | 366 | """) | |
2985 | 235 | # lid = Attribute(""" | 367 | |
2986 | 236 | # A reference to an L{IThing} which serves as this containers lid, or | 368 | closed = Attribute( |
2987 | 237 | # C{None} if there is no lid. | 369 | """ |
2988 | 238 | # """) | 370 | A boolean indicating whether this container is closed. |
2989 | 239 | 371 | """) | |
2990 | 240 | closed = Attribute(""" | 372 | |
2979 | 241 | A boolean indicating whether this container is closed. | ||
2980 | 242 | """) | ||
2991 | 243 | 373 | ||
2992 | 244 | def add(object): | 374 | def add(object): |
2993 | 245 | """ | 375 | """ |
2994 | @@ -331,63 +461,84 @@ | |||
2995 | 331 | 461 | ||
2996 | 332 | 462 | ||
2997 | 333 | 463 | ||
3055 | 334 | class IProxy(Interface): | 464 | class ILinkAnnotator(Interface): |
3056 | 335 | """ | 465 | """ |
3057 | 336 | | > look | 466 | An L{ILinkAnnotator} provides annotations for links from one |
3058 | 337 | | [ Nuclear Reactor Core ] | 467 | L{imaginary.idea.Idea} to another. |
3059 | 338 | | High-energy particles are wizzing around here at a fantastic rate. You can | 468 | """ |
3060 | 339 | | feel the molecules in your body splitting apart as neutrons bombard the | 469 | |
3061 | 340 | | nuclei of their constituent atoms. In a few moments you will be dead. | 470 | def annotationsFor(link, idea): |
3062 | 341 | | There is a radiation suit on the floor. | 471 | """ |
3063 | 342 | | > take radiation suit | 472 | Produce an iterator of annotations to be applied to a link whose source |
3064 | 343 | | You take the radiation suit. | 473 | or target is the L{Idea} that this L{ILinkAnnotator} has been applied |
3065 | 344 | | Your internal organs hurt a lot. | 474 | to. |
3066 | 345 | | > wear radiation suit | 475 | """ |
3067 | 346 | | You wear the radiation suit. | 476 | |
3068 | 347 | | You start to feel better. | 477 | |
3069 | 348 | 478 | ||
3070 | 349 | That is to say, a factory for objects which take the place of elements in | 479 | class ILocationLinkAnnotator(Interface): |
3071 | 350 | the result of L{IThing.findProviders} for the purpose of altering their | 480 | """ |
3072 | 351 | behavior in some manner due to a particular property of the path in the | 481 | L{ILocationLinkAnnotator} is a powerup interface to allow powerups for a |
3073 | 352 | game object graph through which the original element would have been found. | 482 | L{Thing} to act as L{ILinkAnnotator}s for every L{Thing} contained within |
3074 | 353 | 483 | it. This allows area-effect link annotators to be implemented simply, | |
3075 | 354 | Another example to consider is that of a pair of sunglasses worn by a | 484 | without needing to monitor movement. |
3076 | 355 | player: these might power up that player for IProxy so as to be able to | 485 | """ |
3077 | 356 | proxy IVisible in such a way as to reduce glaring light. | 486 | |
3078 | 357 | """ | 487 | def annotationsFor(link, idea): |
3079 | 358 | # XXX: Perhaps add 'distance' here, so Fog can be implemented as an | 488 | """ |
3080 | 359 | # IVisibility proxy which reduces the distance a observer can see. | 489 | Produce an iterator of annotations to be applied to a link whose source |
3081 | 360 | def proxy(iface, facet): | 490 | or target is an L{Idea} of a L{Thing} contained in the L{Thing} that |
3082 | 361 | """ | 491 | this L{ILocationLinkAnnotator} has been applied to. |
3083 | 362 | Proxy C{facet} which provides C{iface}. | 492 | """ |
3084 | 363 | 493 | ||
3085 | 364 | @param facet: A candidate for inclusion in the set of objects returned | 494 | |
3086 | 365 | by findProviders. | 495 | |
3087 | 366 | 496 | class IRetriever(Interface): | |
3088 | 367 | @return: Either a provider of C{iface} or C{None}. If C{None} is | 497 | """ |
3089 | 368 | returned, then the object will not be returned from findProviders. | 498 | An L{IRetriever} examines a L{Path} and retrieves a desirable object from |
3090 | 369 | """ | 499 | it to yield from L{Idea.obtain}, if the L{Path} is suitable. |
3091 | 370 | 500 | ||
3092 | 371 | 501 | Every L{IRetriever} has a different definition of suitability; you should | |
3093 | 372 | 502 | examine some of their implementations for more detail. | |
3094 | 373 | class ILocationProxy(Interface): | 503 | """ |
3095 | 374 | """ | 504 | |
3096 | 375 | Similar to L{IProxy}, except the pathway between the observer and the | 505 | def retrieve(path): |
3097 | 376 | target is not considered: instead, all targets are wrapped by all | 506 | """ |
3098 | 377 | ILocationProxy providers on their location. | 507 | Return the suitable object described by C{path}, or None if the path is |
3099 | 378 | """ | 508 | unsuitable for this retriever's purposes. |
3100 | 379 | 509 | """ | |
3101 | 380 | def proxy(iface, facet): | 510 | |
3102 | 381 | """ | 511 | def shouldKeepGoing(path): |
3103 | 382 | Proxy C{facet} which provides C{iface}. | 512 | """ |
3104 | 383 | 513 | Inspect a L{Path}. True if it should be searched, False if not. | |
3105 | 384 | @param facet: A candidate B{contained by the location on which this is | 514 | """ |
3106 | 385 | a powerup} for inclusion in the set of objects returned by | 515 | |
3107 | 386 | findProviders. | 516 | |
3108 | 387 | 517 | def objectionsTo(path, result): | |
3109 | 388 | @return: Either a provider of C{iface} or C{None}. If C{None} is | 518 | """ |
3110 | 389 | returned, then the object will not be returned from findProviders. | 519 | @return: an iterator of IWhyNot, if you object to this result being |
3111 | 390 | """ | 520 | yielded. |
3112 | 521 | """ | ||
3113 | 522 | |||
3114 | 523 | |||
3115 | 524 | |||
3116 | 525 | class IContainmentRelationship(Interface): | ||
3117 | 526 | """ | ||
3118 | 527 | Indicate the containment of one idea within another, via a link. | ||
3119 | 528 | |||
3120 | 529 | This is an annotation interface, used to annotate L{iimaginary.idea.Link}s | ||
3121 | 530 | to specify that the relationship between linked objects is one of | ||
3122 | 531 | containment. In other words, the presence of an | ||
3123 | 532 | L{IContainmentRelationship} annotation on a L{iimaginary.idea.Link} | ||
3124 | 533 | indicates that the target of that link is contained by the source of that | ||
3125 | 534 | link. | ||
3126 | 535 | """ | ||
3127 | 536 | |||
3128 | 537 | containedBy = Attribute( | ||
3129 | 538 | """ | ||
3130 | 539 | A reference to the L{IContainer} which contains the target of the link | ||
3131 | 540 | that this L{IContainmentRelationship} annotates. | ||
3132 | 541 | """) | ||
3133 | 391 | 542 | ||
3134 | 392 | 543 | ||
3135 | 393 | 544 | ||
3136 | @@ -395,6 +546,7 @@ | |||
3137 | 395 | """ | 546 | """ |
3138 | 396 | A thing which can be seen. | 547 | A thing which can be seen. |
3139 | 397 | """ | 548 | """ |
3140 | 549 | |||
3141 | 398 | def visualize(): | 550 | def visualize(): |
3142 | 399 | """ | 551 | """ |
3143 | 400 | Return an IConcept which represents the visible aspects of this | 552 | Return an IConcept which represents the visible aspects of this |
3144 | @@ -402,6 +554,15 @@ | |||
3145 | 402 | """ | 554 | """ |
3146 | 403 | 555 | ||
3147 | 404 | 556 | ||
3148 | 557 | def isViewOf(thing): | ||
3149 | 558 | """ | ||
3150 | 559 | Is this L{IVisible} a view of a given L{Thing}? | ||
3151 | 560 | |||
3152 | 561 | @rtype: L{bool} | ||
3153 | 562 | """ | ||
3154 | 563 | |||
3155 | 564 | |||
3156 | 565 | |||
3157 | 405 | 566 | ||
3158 | 406 | class ILightSource(Interface): | 567 | class ILightSource(Interface): |
3159 | 407 | """ | 568 | """ |
3160 | @@ -478,6 +639,36 @@ | |||
3161 | 478 | """) | 639 | """) |
3162 | 479 | 640 | ||
3163 | 480 | 641 | ||
3164 | 642 | def nowWornBy(wearer): | ||
3165 | 643 | """ | ||
3166 | 644 | This article of clothing is now being worn by C{wearer}. | ||
3167 | 645 | |||
3168 | 646 | @param wearer: The wearer of the clothing. | ||
3169 | 647 | |||
3170 | 648 | @type wearer: L{IClothingWearer} | ||
3171 | 649 | """ | ||
3172 | 650 | |||
3173 | 651 | |||
3174 | 652 | def noLongerWorn(): | ||
3175 | 653 | """ | ||
3176 | 654 | This article of clothing is no longer being worn. | ||
3177 | 655 | """ | ||
3178 | 656 | |||
3179 | 657 | |||
3180 | 658 | |||
3181 | 659 | class ISittable(Interface): | ||
3182 | 660 | """ | ||
3183 | 661 | Something you can sit on. | ||
3184 | 662 | """ | ||
3185 | 663 | |||
3186 | 664 | def seat(sitterThing): | ||
3187 | 665 | """ | ||
3188 | 666 | @param sitterThing: The person sitting down on this sittable surface. | ||
3189 | 667 | |||
3190 | 668 | @type sitterThing: L{imaginary.objects.Thing} | ||
3191 | 669 | """ | ||
3192 | 670 | |||
3193 | 671 | |||
3194 | 481 | 672 | ||
3195 | 482 | class IDescriptor(IThingPowerUp): | 673 | class IDescriptor(IThingPowerUp): |
3196 | 483 | """ | 674 | """ |
3197 | @@ -494,4 +685,39 @@ | |||
3198 | 494 | """ | 685 | """ |
3199 | 495 | 686 | ||
3200 | 496 | 687 | ||
3201 | 688 | class IWhyNot(Interface): | ||
3202 | 689 | """ | ||
3203 | 690 | This interface is an idea link annotation interface, designed to be applied | ||
3204 | 691 | by L{ILinkAnnotator}s, that indicates a reason why a given path cannot | ||
3205 | 692 | yield a provider. This is respected by L{imaginary.idea.ProviderOf}. | ||
3206 | 693 | """ | ||
3207 | 694 | |||
3208 | 695 | def tellMeWhyNot(): | ||
3209 | 696 | """ | ||
3210 | 697 | Return something adaptable to L{IConcept}, that explains why this link | ||
3211 | 698 | is unsuitable for producing results. For example, the string "It's too | ||
3212 | 699 | dark in here." | ||
3213 | 700 | """ | ||
3214 | 701 | |||
3215 | 702 | |||
3216 | 703 | |||
3217 | 704 | class IDistance(Interface): | ||
3218 | 705 | """ | ||
3219 | 706 | A link annotation that provides a distance. | ||
3220 | 707 | """ | ||
3221 | 708 | |||
3222 | 709 | distance = Attribute("floating point, distance in meters") | ||
3223 | 710 | |||
3224 | 711 | |||
3225 | 712 | |||
3226 | 713 | class IElectromagneticMedium(Interface): | ||
3227 | 714 | """ | ||
3228 | 715 | A medium through which electromagnetic radiation may or may not pass; used | ||
3229 | 716 | as a link annotation. | ||
3230 | 717 | """ | ||
3231 | 718 | |||
3232 | 719 | def isOpaque(): | ||
3233 | 720 | """ | ||
3234 | 721 | Will this propagate radiation the visible spectrum? | ||
3235 | 722 | """ | ||
3236 | 497 | 723 | ||
3237 | 498 | 724 | ||
3238 | === modified file 'Imaginary/imaginary/language.py' | |||
3239 | --- Imaginary/imaginary/language.py 2008-05-04 21:35:09 +0000 | |||
3240 | +++ Imaginary/imaginary/language.py 2011-09-16 20:42:26 +0000 | |||
3241 | @@ -136,6 +136,17 @@ | |||
3242 | 136 | Concepts will be ordered by the C{preferredOrder} class attribute. | 136 | Concepts will be ordered by the C{preferredOrder} class attribute. |
3243 | 137 | Concepts not named in this list will appear last in an unpredictable | 137 | Concepts not named in this list will appear last in an unpredictable |
3244 | 138 | order. | 138 | order. |
3245 | 139 | |||
3246 | 140 | @ivar name: The name of the thing being described. | ||
3247 | 141 | |||
3248 | 142 | @ivar description: A basic description of the thing being described, the | ||
3249 | 143 | first thing to show up. | ||
3250 | 144 | |||
3251 | 145 | @ivar exits: An iterable of L{IExit}, to be listed as exits in the | ||
3252 | 146 | description. | ||
3253 | 147 | |||
3254 | 148 | @ivar others: An iterable of L{IDescriptionContributor} that will | ||
3255 | 149 | supplement the description. | ||
3256 | 139 | """ | 150 | """ |
3257 | 140 | implements(iimaginary.IConcept) | 151 | implements(iimaginary.IConcept) |
3258 | 141 | 152 | ||
3259 | @@ -167,6 +178,7 @@ | |||
3260 | 167 | description = (T.fg.green, self.description, u'\n') | 178 | description = (T.fg.green, self.description, u'\n') |
3261 | 168 | 179 | ||
3262 | 169 | descriptionConcepts = [] | 180 | descriptionConcepts = [] |
3263 | 181 | |||
3264 | 170 | for pup in self.others: | 182 | for pup in self.others: |
3265 | 171 | descriptionConcepts.append(pup.conceptualize()) | 183 | descriptionConcepts.append(pup.conceptualize()) |
3266 | 172 | 184 | ||
3267 | 173 | 185 | ||
3268 | === modified file 'Imaginary/imaginary/objects.py' | |||
3269 | --- Imaginary/imaginary/objects.py 2009-06-29 04:03:17 +0000 | |||
3270 | +++ Imaginary/imaginary/objects.py 2011-09-16 20:42:26 +0000 | |||
3271 | @@ -1,4 +1,12 @@ | |||
3273 | 1 | # -*- test-case-name: imaginary.test.test_objects -*- | 1 | # -*- test-case-name: imaginary.test.test_objects,imaginary.test.test_actions -*- |
3274 | 2 | |||
3275 | 3 | """ | ||
3276 | 4 | This module contains the core, basic objects in Imaginary. | ||
3277 | 5 | |||
3278 | 6 | L{imaginary.objects} contains the physical simulation (L{Thing}), objects | ||
3279 | 7 | associated with scoring (L{Points}), and the basic actor interface which allows | ||
3280 | 8 | the user to perform simple actions (L{Actor}). | ||
3281 | 9 | """ | ||
3282 | 2 | 10 | ||
3283 | 3 | from __future__ import division | 11 | from __future__ import division |
3284 | 4 | 12 | ||
3285 | @@ -9,6 +17,7 @@ | |||
3286 | 9 | from twisted.python import reflect, components | 17 | from twisted.python import reflect, components |
3287 | 10 | 18 | ||
3288 | 11 | from epsilon import structlike | 19 | from epsilon import structlike |
3289 | 20 | from epsilon.remember import remembered | ||
3290 | 12 | 21 | ||
3291 | 13 | from axiom import item, attributes | 22 | from axiom import item, attributes |
3292 | 14 | 23 | ||
3293 | @@ -16,17 +25,9 @@ | |||
3294 | 16 | 25 | ||
3295 | 17 | from imaginary.enhancement import Enhancement as _Enhancement | 26 | from imaginary.enhancement import Enhancement as _Enhancement |
3296 | 18 | 27 | ||
3308 | 19 | def merge(d1, *dn): | 28 | from imaginary.idea import ( |
3309 | 20 | """ | 29 | Idea, Link, Proximity, Reachable, ProviderOf, Named, AlsoKnownAs, CanSee, |
3310 | 21 | da = {a: [1, 2]} | 30 | Vector, DelegatingRetriever) |
3300 | 22 | db = {b: [3]} | ||
3301 | 23 | dc = {b: [5], c: [2, 4]} | ||
3302 | 24 | merge(da, db, dc) | ||
3303 | 25 | da == {a: [1, 2], b: [3, 5], c: [2, 4]} | ||
3304 | 26 | """ | ||
3305 | 27 | for d in dn: | ||
3306 | 28 | for (k, v) in d.iteritems(): | ||
3307 | 29 | d1.setdefault(k, []).extend(v) | ||
3311 | 30 | 31 | ||
3312 | 31 | 32 | ||
3313 | 32 | class Points(item.Item): | 33 | class Points(item.Item): |
3314 | @@ -66,8 +67,58 @@ | |||
3315 | 66 | return self.current | 67 | return self.current |
3316 | 67 | 68 | ||
3317 | 68 | 69 | ||
3318 | 70 | |||
3319 | 69 | class Thing(item.Item): | 71 | class Thing(item.Item): |
3321 | 70 | implements(iimaginary.IThing, iimaginary.IVisible) | 72 | """ |
3322 | 73 | A L{Thing} is a physically located object in the game world. | ||
3323 | 74 | |||
3324 | 75 | While a game object in Imaginary is composed of many different Python | ||
3325 | 76 | objects, the L{Thing} is the central object that most game objects will | ||
3326 | 77 | share. It's central for several reasons. | ||
3327 | 78 | |||
3328 | 79 | First, a L{Thing} is connected to the point-of-interest simulation that | ||
3329 | 80 | makes up the environment of an Imaginary game. A L{Thing} has a location, | ||
3330 | 81 | and a L{Container} can list the L{Thing}s located within it, which is how | ||
3331 | 82 | you can see the objects in your surroundings or a container. | ||
3332 | 83 | |||
3333 | 84 | Each L{Thing} has an associated L{Idea}, which provides the graph that can | ||
3334 | 85 | be traversed to find other L{Thing}s to be the target for actions or | ||
3335 | 86 | events. | ||
3336 | 87 | |||
3337 | 88 | A L{Thing} is also the object which serves as the persistent nexus of | ||
3338 | 89 | powerups that define behavior. An L{_Enhancement} is a powerup for a | ||
3339 | 90 | L{Thing}. L{Thing}s can be powered up for a number of different interfaces: | ||
3340 | 91 | |||
3341 | 92 | - L{iimaginary.IMovementRestriction}, for preventing the L{Thing} from | ||
3342 | 93 | moving around, | ||
3343 | 94 | |||
3344 | 95 | - L{iimaginary.ILinkContributor}, which can provide links from the | ||
3345 | 96 | L{Thing}'s L{Idea} to other L{Idea}s, | ||
3346 | 97 | |||
3347 | 98 | - L{iimaginary.ILinkAnnotator}, which can provide annotations on links | ||
3348 | 99 | incoming to or outgoing from the L{Thing}'s L{Idea}, | ||
3349 | 100 | |||
3350 | 101 | - L{iimaginary.ILocationLinkAnnotator}, which can provide annotations on | ||
3351 | 102 | links to or from any L{Thing}'s L{Idea} which is ultimately located | ||
3352 | 103 | within the powered-up L{Thing}. | ||
3353 | 104 | |||
3354 | 105 | - L{iimaginary.IDescriptionContributor}, which provide components of | ||
3355 | 106 | the L{Thing}'s description when viewed with the L{Look} action. | ||
3356 | 107 | |||
3357 | 108 | - and finally, any interface used as a target for an action or event. | ||
3358 | 109 | |||
3359 | 110 | The way this all fits together is as follows: if you wanted to make a | ||
3360 | 111 | shirt, for example, you would make a L{Thing}, give it an appropriate name | ||
3361 | 112 | and description, make a new L{Enhancement} class which implements | ||
3362 | 113 | L{IMovementRestriction} to prevent the shirt from moving around unless it | ||
3363 | 114 | is correctly in the un-worn state, and then power up that L{Enhancement} on | ||
3364 | 115 | the L{Thing}. This particular example is implemented in | ||
3365 | 116 | L{imaginary.garments}, but almost any game-logic implementation will follow | ||
3366 | 117 | this general pattern. | ||
3367 | 118 | """ | ||
3368 | 119 | |||
3369 | 120 | implements(iimaginary.IThing, iimaginary.IVisible, iimaginary.INameable, | ||
3370 | 121 | iimaginary.ILinkAnnotator, iimaginary.ILinkContributor) | ||
3371 | 71 | 122 | ||
3372 | 72 | weight = attributes.integer(doc=""" | 123 | weight = attributes.integer(doc=""" |
3373 | 73 | Units of weight of this object. | 124 | Units of weight of this object. |
3374 | @@ -106,117 +157,114 @@ | |||
3375 | 106 | 157 | ||
3376 | 107 | 158 | ||
3377 | 108 | def links(self): | 159 | def links(self): |
3381 | 109 | d = {self.name.lower(): [self]} | 160 | """ |
3382 | 110 | if self.location is not None: | 161 | Implement L{ILinkContributor.links()} by offering a link to this |
3383 | 111 | merge(d, {self.location.name: [self.location]}) | 162 | L{Thing}'s C{location} (if it has one). |
3384 | 163 | """ | ||
3385 | 164 | # since my link contribution is to go up (out), put this last, since | ||
3386 | 165 | # containment (i.e. going down (in)) is a powerup. we want to explore | ||
3387 | 166 | # contained items first. | ||
3388 | 112 | for pup in self.powerupsFor(iimaginary.ILinkContributor): | 167 | for pup in self.powerupsFor(iimaginary.ILinkContributor): |
3396 | 113 | merge(d, pup.links()) | 168 | for link in pup.links(): |
3397 | 114 | return d | 169 | # wooo composition |
3398 | 115 | 170 | yield link | |
3399 | 116 | 171 | if self.location is not None: | |
3400 | 117 | thing = property(lambda self: self) | 172 | l = Link(self.idea, self.location.idea) |
3401 | 118 | 173 | # XXX this incorrectly identifies any container with an object in | |
3402 | 119 | _ProviderStackElement = structlike.record('distance stability target proxies') | 174 | # it as 'here', since it doesn't distinguish the observer; however, |
3403 | 175 | # cycle detection will prevent these links from being considered in | ||
3404 | 176 | # any case I can think of. However, 'here' is ambiguous in the | ||
3405 | 177 | # case where you are present inside a container, and that should | ||
3406 | 178 | # probably be dealt with. | ||
3407 | 179 | l.annotate([AlsoKnownAs('here')]) | ||
3408 | 180 | yield l | ||
3409 | 181 | |||
3410 | 182 | |||
3411 | 183 | def allAnnotators(self): | ||
3412 | 184 | """ | ||
3413 | 185 | A generator which yields all L{iimaginary.ILinkAnnotator} providers | ||
3414 | 186 | that should affect this L{Thing}'s L{Idea}. This includes: | ||
3415 | 187 | |||
3416 | 188 | - all L{iimaginary.ILocationLinkAnnotator} powerups on all | ||
3417 | 189 | L{Thing}s which contain this L{Thing} (the container it's in, the | ||
3418 | 190 | room its container is in, etc) | ||
3419 | 191 | |||
3420 | 192 | - all L{iimaginary.ILinkAnnotator} powerups on this L{Thing}. | ||
3421 | 193 | """ | ||
3422 | 194 | loc = self | ||
3423 | 195 | while loc is not None: | ||
3424 | 196 | # TODO Test the loc is None case | ||
3425 | 197 | if loc is not None: | ||
3426 | 198 | for pup in loc.powerupsFor(iimaginary.ILocationLinkAnnotator): | ||
3427 | 199 | yield pup | ||
3428 | 200 | loc = loc.location | ||
3429 | 201 | for pup in self.powerupsFor(iimaginary.ILinkAnnotator): | ||
3430 | 202 | yield pup | ||
3431 | 203 | |||
3432 | 204 | |||
3433 | 205 | def annotationsFor(self, link, idea): | ||
3434 | 206 | """ | ||
3435 | 207 | Implement L{ILinkAnnotator.annotationsFor} to consult each | ||
3436 | 208 | L{ILinkAnnotator} for this L{Thing}, as defined by | ||
3437 | 209 | L{Thing.allAnnotators}, and yield each annotation for the given L{Link} | ||
3438 | 210 | and L{Idea}. | ||
3439 | 211 | """ | ||
3440 | 212 | for annotator in self.allAnnotators(): | ||
3441 | 213 | for annotation in annotator.annotationsFor(link, idea): | ||
3442 | 214 | yield annotation | ||
3443 | 215 | |||
3444 | 216 | |||
3445 | 217 | @remembered | ||
3446 | 218 | def idea(self): | ||
3447 | 219 | """ | ||
3448 | 220 | An L{Idea} which represents this L{Thing}. | ||
3449 | 221 | """ | ||
3450 | 222 | idea = Idea(self) | ||
3451 | 223 | idea.linkers.append(self) | ||
3452 | 224 | idea.annotators.append(self) | ||
3453 | 225 | return idea | ||
3454 | 226 | |||
3455 | 120 | 227 | ||
3456 | 121 | def findProviders(self, interface, distance): | 228 | def findProviders(self, interface, distance): |
3555 | 122 | 229 | """ | |
3556 | 123 | # Dictionary keyed on Thing instances used to ensure any particular | 230 | Temporary emulation of the old way of doing things so that I can |
3557 | 124 | # Thing is yielded at most once. | 231 | surgically replace findProviders. |
3558 | 125 | seen = {} | 232 | """ |
3559 | 126 | 233 | return self.idea.obtain( | |
3560 | 127 | # Dictionary keyed on Thing instances used to ensure any particular | 234 | Proximity(distance, CanSee(ProviderOf(interface)))) |
3561 | 128 | # Thing only has its links inspected at most once. | 235 | |
3562 | 129 | visited = {self: True} | 236 | |
3563 | 130 | 237 | def obtainOrReportWhyNot(self, retriever): | |
3564 | 131 | # Load proxies that are installed directly on this Thing as well as | 238 | """ |
3565 | 132 | # location proxies on this Thing's location: if self is adaptable to | 239 | Invoke L{Idea.obtain} on C{self.idea} with the given C{retriever}. |
3566 | 133 | # interface, use them as arguments to _applyProxies and yield a proxied | 240 | |
3567 | 134 | # and adapted facet of self. | 241 | If no results are yielded, then investigate the reasons why no results |
3568 | 135 | facet = interface(self, None) | 242 | have been yielded, and raise an exception describing one of them. |
3569 | 136 | initialProxies = list(self.powerupsFor(iimaginary.IProxy)) | 243 | |
3570 | 137 | locationProxies = set() | 244 | Objections may be registered by: |
3571 | 138 | if self.location is not None: | 245 | |
3572 | 139 | locationProxies.update(set(self.location.powerupsFor(iimaginary.ILocationProxy))) | 246 | - an L{iimaginary.IWhyNot} annotation on any link traversed in the |
3573 | 140 | if facet is not None: | 247 | attempt to discover results, or, |
3574 | 141 | seen[self] = True | 248 | |
3575 | 142 | proxiedFacet = self._applyProxies(locationProxies, initialProxies, facet, interface) | 249 | - an L{iimaginary.IWhyNot} yielded by the given C{retriever}'s |
3576 | 143 | if proxiedFacet is not None: | 250 | L{iimaginary.IRetriever.objectionsTo} method. |
3577 | 144 | yield proxiedFacet | 251 | |
3578 | 145 | 252 | @return: a list of objects returned by C{retriever.retrieve} | |
3579 | 146 | # Toss in for the _ProviderStackElement list/stack. Ensures ordering | 253 | |
3580 | 147 | # in the descendTo list remains consistent with a breadth-first | 254 | @rtype: C{list} |
3581 | 148 | # traversal of links (there is probably a better way to do this). | 255 | |
3582 | 149 | stabilityHelper = 1 | 256 | @raise eimaginary.ActionFailure: if no results are available, and an |
3583 | 150 | 257 | objection has been registered. | |
3584 | 151 | # Set up a stack of Things to ask for links to visit - start with just | 258 | """ |
3585 | 152 | # ourself and the proxies we have found already. | 259 | obt = self.idea.obtain(retriever) |
3586 | 153 | descendTo = [self._ProviderStackElement(distance, 0, self, initialProxies)] | 260 | results = list(obt) |
3587 | 154 | 261 | if not results: | |
3588 | 155 | while descendTo: | 262 | reasons = list(obt.reasonsWhyNot) |
3589 | 156 | element = descendTo.pop() | 263 | if reasons: |
3590 | 157 | distance, target, proxies = (element.distance, element.target, | 264 | raise eimaginary.ActionFailure(events.ThatDoesntWork( |
3591 | 158 | element.proxies) | 265 | actor=self, |
3592 | 159 | links = target.links().items() | 266 | actorMessage=reasons[0].tellMeWhyNot())) |
3593 | 160 | links.sort() | 267 | return results |
3496 | 161 | for (linkName, linkedThings) in links: | ||
3497 | 162 | for linkedThing in linkedThings: | ||
3498 | 163 | if distance: | ||
3499 | 164 | if linkedThing not in visited: | ||
3500 | 165 | # A Thing which was linked and has not yet been | ||
3501 | 166 | # visited. Create a new list of proxies from the | ||
3502 | 167 | # current list and any which it has and push this | ||
3503 | 168 | # state onto the stack. Also extend the total list | ||
3504 | 169 | # of location proxies with any location proxies it | ||
3505 | 170 | # has. | ||
3506 | 171 | visited[linkedThing] = True | ||
3507 | 172 | stabilityHelper += 1 | ||
3508 | 173 | locationProxies.update(set(linkedThing.powerupsFor(iimaginary.ILocationProxy))) | ||
3509 | 174 | proxies = proxies + list( | ||
3510 | 175 | linkedThing.powerupsFor(iimaginary.IProxy)) | ||
3511 | 176 | descendTo.append(self._ProviderStackElement( | ||
3512 | 177 | distance - 1, stabilityHelper, | ||
3513 | 178 | linkedThing, proxies)) | ||
3514 | 179 | |||
3515 | 180 | # If the linked Thing hasn't been yielded before and is | ||
3516 | 181 | # adaptable to the desired interface, wrap it in the | ||
3517 | 182 | # appropriate proxies and yield it. | ||
3518 | 183 | facet = interface(linkedThing, None) | ||
3519 | 184 | if facet is not None and linkedThing not in seen: | ||
3520 | 185 | seen[linkedThing] = True | ||
3521 | 186 | proxiedFacet = self._applyProxies(locationProxies, proxies, facet, interface) | ||
3522 | 187 | if proxiedFacet is not None: | ||
3523 | 188 | yield proxiedFacet | ||
3524 | 189 | |||
3525 | 190 | # Re-order anything we've appended so that we visit it in the right | ||
3526 | 191 | # order. | ||
3527 | 192 | descendTo.sort() | ||
3528 | 193 | |||
3529 | 194 | |||
3530 | 195 | def _applyProxies(self, locationProxies, proxies, obj, interface): | ||
3531 | 196 | # Extremely pathetic algorithm - loop over all location proxies we have | ||
3532 | 197 | # seen and apply any which belong to the location of the target object. | ||
3533 | 198 | # This could do with some serious optimization. | ||
3534 | 199 | for proxy in locationProxies: | ||
3535 | 200 | if iimaginary.IContainer(proxy.thing).contains(obj.thing) or proxy.thing is obj.thing: | ||
3536 | 201 | obj = proxy.proxy(obj, interface) | ||
3537 | 202 | if obj is None: | ||
3538 | 203 | return None | ||
3539 | 204 | |||
3540 | 205 | # Loop over the other proxies and simply apply them in turn, giving up | ||
3541 | 206 | # as soon as one eliminates the object entirely. | ||
3542 | 207 | for proxy in proxies: | ||
3543 | 208 | obj = proxy.proxy(obj, interface) | ||
3544 | 209 | if obj is None: | ||
3545 | 210 | return None | ||
3546 | 211 | |||
3547 | 212 | return obj | ||
3548 | 213 | |||
3549 | 214 | |||
3550 | 215 | def proxiedThing(self, thing, interface, distance): | ||
3551 | 216 | for prospectiveFacet in self.findProviders(interface, distance): | ||
3552 | 217 | if prospectiveFacet.thing is thing: | ||
3553 | 218 | return prospectiveFacet | ||
3554 | 219 | raise eimaginary.ThingNotFound(thing) | ||
3594 | 220 | 268 | ||
3595 | 221 | 269 | ||
3596 | 222 | def search(self, distance, interface, name): | 270 | def search(self, distance, interface, name): |
3597 | @@ -224,59 +272,59 @@ | |||
3598 | 224 | Retrieve game objects answering to the given name which provide the | 272 | Retrieve game objects answering to the given name which provide the |
3599 | 225 | given interface and are within the given distance. | 273 | given interface and are within the given distance. |
3600 | 226 | 274 | ||
3601 | 227 | @type distance: C{int} | ||
3602 | 228 | @param distance: How many steps to traverse (note: this is wrong, it | 275 | @param distance: How many steps to traverse (note: this is wrong, it |
3604 | 229 | will become a real distance-y thing with real game-meaning someday). | 276 | will become a real distance-y thing with real game-meaning |
3605 | 277 | someday). | ||
3606 | 278 | @type distance: C{float} | ||
3607 | 230 | 279 | ||
3608 | 231 | @param interface: The interface which objects within the required range | 280 | @param interface: The interface which objects within the required range |
3610 | 232 | must be adaptable to in order to be returned. | 281 | must be adaptable to in order to be returned. |
3611 | 233 | 282 | ||
3612 | 283 | @param name: The name of the stuff. | ||
3613 | 234 | @type name: C{str} | 284 | @type name: C{str} |
3614 | 235 | @param name: The name of the stuff. | ||
3615 | 236 | 285 | ||
3616 | 237 | @return: An iterable of L{iimaginary.IThing} providers which are found. | 286 | @return: An iterable of L{iimaginary.IThing} providers which are found. |
3617 | 238 | """ | 287 | """ |
3650 | 239 | # TODO - Move this into the action system. It is about finding things | 288 | return self.obtainOrReportWhyNot( |
3651 | 240 | # using strings, which isn't what the action system is all about, but | 289 | Proximity( |
3652 | 241 | # the action system is where we do that sort of thing now. -exarkun | 290 | distance, |
3653 | 242 | extras = [] | 291 | Reachable(Named(name, CanSee(ProviderOf(interface)), self)))) |
3622 | 243 | |||
3623 | 244 | container = iimaginary.IContainer(self.location, None) | ||
3624 | 245 | if container is not None: | ||
3625 | 246 | potentialExit = container.getExitNamed(name, None) | ||
3626 | 247 | if potentialExit is not None: | ||
3627 | 248 | try: | ||
3628 | 249 | potentialThing = self.proxiedThing( | ||
3629 | 250 | potentialExit.toLocation, interface, distance) | ||
3630 | 251 | except eimaginary.ThingNotFound: | ||
3631 | 252 | pass | ||
3632 | 253 | else: | ||
3633 | 254 | yield potentialThing | ||
3634 | 255 | |||
3635 | 256 | if name == "me" or name == "self": | ||
3636 | 257 | facet = interface(self, None) | ||
3637 | 258 | if facet is not None: | ||
3638 | 259 | extras.append(self) | ||
3639 | 260 | |||
3640 | 261 | if name == "here" and self.location is not None: | ||
3641 | 262 | facet = interface(self.location, None) | ||
3642 | 263 | if facet is not None: | ||
3643 | 264 | extras.append(self.location) | ||
3644 | 265 | |||
3645 | 266 | for res in self.findProviders(interface, distance): | ||
3646 | 267 | if res.thing in extras: | ||
3647 | 268 | yield res | ||
3648 | 269 | elif res.thing.knownAs(name): | ||
3649 | 270 | yield res | ||
3654 | 271 | 292 | ||
3655 | 272 | 293 | ||
3656 | 273 | def moveTo(self, where, arrivalEventFactory=None): | 294 | def moveTo(self, where, arrivalEventFactory=None): |
3657 | 274 | """ | 295 | """ |
3659 | 275 | @see: L{iimaginary.IThing.moveTo}. | 296 | Implement L{iimaginary.IThing.moveTo} to change the C{location} of this |
3660 | 297 | L{Thing} to a new L{Thing}, broadcasting an L{events.DepartureEvent} to | ||
3661 | 298 | note this object's departure from its current C{location}. | ||
3662 | 299 | |||
3663 | 300 | Before moving it, invoke each L{IMovementRestriction} powerup on this | ||
3664 | 301 | L{Thing} to allow them to prevent this movement. | ||
3665 | 276 | """ | 302 | """ |
3667 | 277 | if where is self.location: | 303 | whereContainer = iimaginary.IContainer(where, None) |
3668 | 304 | if (whereContainer is | ||
3669 | 305 | iimaginary.IContainer(self.location, None)): | ||
3670 | 306 | # Early out if I'm being moved to the same location that I was | ||
3671 | 307 | # already in. | ||
3672 | 278 | return | 308 | return |
3673 | 309 | if whereContainer is None: | ||
3674 | 310 | whereThing = None | ||
3675 | 311 | else: | ||
3676 | 312 | whereThing = whereContainer.thing | ||
3677 | 313 | if whereThing is not None and whereThing.location is self: | ||
3678 | 314 | # XXX should be checked against _all_ locations of whereThing, not | ||
3679 | 315 | # just the proximate one. | ||
3680 | 316 | |||
3681 | 317 | # XXX actor= here is wrong, who knows who is moving this thing. | ||
3682 | 318 | raise eimaginary.ActionFailure(events.ThatDoesntWork( | ||
3683 | 319 | actor=self, | ||
3684 | 320 | actorMessage=[ | ||
3685 | 321 | language.Noun(where.thing).definiteNounPhrase() | ||
3686 | 322 | .capitalizeConcept(), | ||
3687 | 323 | " won't fit inside itself."])) | ||
3688 | 324 | |||
3689 | 279 | oldLocation = self.location | 325 | oldLocation = self.location |
3690 | 326 | for restriction in self.powerupsFor(iimaginary.IMovementRestriction): | ||
3691 | 327 | restriction.movementImminent(self, where) | ||
3692 | 280 | if oldLocation is not None: | 328 | if oldLocation is not None: |
3693 | 281 | events.DepartureEvent(oldLocation, self).broadcast() | 329 | events.DepartureEvent(oldLocation, self).broadcast() |
3694 | 282 | if where is not None: | 330 | if where is not None: |
3695 | @@ -290,20 +338,33 @@ | |||
3696 | 290 | iimaginary.IContainer(oldLocation).remove(self) | 338 | iimaginary.IContainer(oldLocation).remove(self) |
3697 | 291 | 339 | ||
3698 | 292 | 340 | ||
3705 | 293 | def knownAs(self, name): | 341 | def knownTo(self, observer, name): |
3706 | 294 | """ | 342 | """ |
3707 | 295 | Return C{True} if C{name} might refer to this L{Thing}, C{False} otherwise. | 343 | Implement L{INameable.knownTo} to compare the name to L{Thing.name} as |
3708 | 296 | 344 | well as few constant values based on the relationship of the observer | |
3709 | 297 | XXX - See #2604. | 345 | to this L{Thing}, such as 'me', 'self', and 'here'. |
3710 | 298 | """ | 346 | |
3711 | 347 | @param observer: an L{IThing} provider. | ||
3712 | 348 | """ | ||
3713 | 349 | |||
3714 | 350 | mine = self.name.lower() | ||
3715 | 299 | name = name.lower() | 351 | name = name.lower() |
3718 | 300 | mine = self.name.lower() | 352 | if name == mine or name in mine.split(): |
3719 | 301 | return name == mine or name in mine.split() | 353 | return True |
3720 | 354 | if observer == self: | ||
3721 | 355 | if name in ('me', 'self'): | ||
3722 | 356 | return True | ||
3723 | 357 | return False | ||
3724 | 302 | 358 | ||
3725 | 303 | 359 | ||
3726 | 304 | # IVisible | 360 | # IVisible |
3727 | 305 | def visualize(self): | 361 | def visualize(self): |
3729 | 306 | container = iimaginary.IContainer(self.thing, None) | 362 | """ |
3730 | 363 | Implement L{IVisible.visualize} to return a | ||
3731 | 364 | L{language.DescriptionConcept} that describes this L{Thing}, including | ||
3732 | 365 | all its L{iimaginary.IDescriptionContributor} powerups. | ||
3733 | 366 | """ | ||
3734 | 367 | container = iimaginary.IContainer(self, None) | ||
3735 | 307 | if container is not None: | 368 | if container is not None: |
3736 | 308 | exits = list(container.getExits()) | 369 | exits = list(container.getExits()) |
3737 | 309 | else: | 370 | else: |
3738 | @@ -313,8 +374,41 @@ | |||
3739 | 313 | self.name, | 374 | self.name, |
3740 | 314 | self.description, | 375 | self.description, |
3741 | 315 | exits, | 376 | exits, |
3742 | 377 | # Maybe we should listify this or something; see | ||
3743 | 378 | # http://divmod.org/trac/ticket/2905 | ||
3744 | 316 | self.powerupsFor(iimaginary.IDescriptionContributor)) | 379 | self.powerupsFor(iimaginary.IDescriptionContributor)) |
3746 | 317 | components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(), Thing, iimaginary.IConcept) | 380 | |
3747 | 381 | |||
3748 | 382 | def isViewOf(self, thing): | ||
3749 | 383 | """ | ||
3750 | 384 | Implement L{IVisible.isViewOf} to return C{True} if its argument is | ||
3751 | 385 | C{self}. In other words, this L{Thing} is only a view of itself. | ||
3752 | 386 | """ | ||
3753 | 387 | return (thing is self) | ||
3754 | 388 | |||
3755 | 389 | components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(), | ||
3756 | 390 | Thing, | ||
3757 | 391 | iimaginary.IConcept) | ||
3758 | 392 | |||
3759 | 393 | |||
3760 | 394 | def _eventuallyContains(containerThing, containeeThing): | ||
3761 | 395 | """ | ||
3762 | 396 | Does a container, or any containers within it (or any containers within any | ||
3763 | 397 | of those, etc etc) contain some object? | ||
3764 | 398 | |||
3765 | 399 | @param containeeThing: The L{Thing} which may be contained. | ||
3766 | 400 | |||
3767 | 401 | @param containerThing: The L{Thing} which may have a L{Container} that | ||
3768 | 402 | contains C{containeeThing}. | ||
3769 | 403 | |||
3770 | 404 | @return: L{True} if the containee is contained by the container. | ||
3771 | 405 | """ | ||
3772 | 406 | while containeeThing is not None: | ||
3773 | 407 | if containeeThing is containerThing: | ||
3774 | 408 | return True | ||
3775 | 409 | containeeThing = containeeThing.location | ||
3776 | 410 | return False | ||
3777 | 411 | |||
3778 | 318 | 412 | ||
3779 | 319 | 413 | ||
3780 | 320 | 414 | ||
3781 | @@ -323,18 +417,41 @@ | |||
3782 | 323 | u"west": u"east", | 417 | u"west": u"east", |
3783 | 324 | u"northwest": u"southeast", | 418 | u"northwest": u"southeast", |
3784 | 325 | u"northeast": u"southwest"} | 419 | u"northeast": u"southwest"} |
3787 | 326 | for (k, v) in OPPOSITE_DIRECTIONS.items(): | 420 | |
3788 | 327 | OPPOSITE_DIRECTIONS[v] = k | 421 | |
3789 | 422 | def _populateOpposite(): | ||
3790 | 423 | """ | ||
3791 | 424 | Populate L{OPPOSITE_DIRECTIONS} with inverse directions. | ||
3792 | 425 | |||
3793 | 426 | (Without leaking any loop locals into the global scope, thank you very | ||
3794 | 427 | much.) | ||
3795 | 428 | """ | ||
3796 | 429 | for (k, v) in OPPOSITE_DIRECTIONS.items(): | ||
3797 | 430 | OPPOSITE_DIRECTIONS[v] = k | ||
3798 | 431 | |||
3799 | 432 | _populateOpposite() | ||
3800 | 433 | |||
3801 | 328 | 434 | ||
3802 | 329 | 435 | ||
3803 | 330 | class Exit(item.Item): | 436 | class Exit(item.Item): |
3811 | 331 | fromLocation = attributes.reference(doc=""" | 437 | """ |
3812 | 332 | Where this exit leads from. | 438 | An L{Exit} is an oriented pathway between two L{Thing}s which each |
3813 | 333 | """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing) | 439 | represent a room. |
3814 | 334 | 440 | """ | |
3815 | 335 | toLocation = attributes.reference(doc=""" | 441 | |
3816 | 336 | Where this exit leads to. | 442 | implements(iimaginary.INameable, iimaginary.IExit) |
3817 | 337 | """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing) | 443 | |
3818 | 444 | fromLocation = attributes.reference( | ||
3819 | 445 | doc=""" | ||
3820 | 446 | Where this exit leads from. | ||
3821 | 447 | """, allowNone=False, | ||
3822 | 448 | whenDeleted=attributes.reference.CASCADE, reftype=Thing) | ||
3823 | 449 | |||
3824 | 450 | toLocation = attributes.reference( | ||
3825 | 451 | doc=""" | ||
3826 | 452 | Where this exit leads to. | ||
3827 | 453 | """, allowNone=False, | ||
3828 | 454 | whenDeleted=attributes.reference.CASCADE, reftype=Thing) | ||
3829 | 338 | 455 | ||
3830 | 339 | name = attributes.text(doc=""" | 456 | name = attributes.text(doc=""" |
3831 | 340 | What this exit is called/which direction it is in. | 457 | What this exit is called/which direction it is in. |
3832 | @@ -344,15 +461,70 @@ | |||
3833 | 344 | The reverse exit object, if one exists. | 461 | The reverse exit object, if one exists. |
3834 | 345 | """) | 462 | """) |
3835 | 346 | 463 | ||
3838 | 347 | 464 | distance = attributes.ieee754_double( | |
3839 | 348 | def link(cls, a, b, forwardName, backwardName=None): | 465 | doc=""" |
3840 | 466 | How far, in meters, does a user have to travel to traverse this exit? | ||
3841 | 467 | """, allowNone=False, default=1.0) | ||
3842 | 468 | |||
3843 | 469 | def knownTo(self, observer, name): | ||
3844 | 470 | """ | ||
3845 | 471 | Implement L{iimaginary.INameable.knownTo} to identify this L{Exit} as | ||
3846 | 472 | its C{name} attribute. | ||
3847 | 473 | """ | ||
3848 | 474 | return name == self.name | ||
3849 | 475 | |||
3850 | 476 | |||
3851 | 477 | def traverse(self, thing): | ||
3852 | 478 | """ | ||
3853 | 479 | Implement L{iimaginary.IExit} to move the given L{Thing} to this | ||
3854 | 480 | L{Exit}'s C{toLocation}. | ||
3855 | 481 | """ | ||
3856 | 482 | if self.sibling is not None: | ||
3857 | 483 | arriveDirection = self.sibling.name | ||
3858 | 484 | else: | ||
3859 | 485 | arriveDirection = OPPOSITE_DIRECTIONS.get(self.name) | ||
3860 | 486 | |||
3861 | 487 | thing.moveTo( | ||
3862 | 488 | self.toLocation, | ||
3863 | 489 | arrivalEventFactory=lambda player: events.MovementArrivalEvent( | ||
3864 | 490 | thing=thing, | ||
3865 | 491 | origin=None, | ||
3866 | 492 | direction=arriveDirection)) | ||
3867 | 493 | |||
3868 | 494 | |||
3869 | 495 | # XXX This really needs to be renamed now that links are a thing. | ||
3870 | 496 | @classmethod | ||
3871 | 497 | def link(cls, a, b, forwardName, backwardName=None, distance=1.0): | ||
3872 | 498 | """ | ||
3873 | 499 | Create two L{Exit}s connecting two rooms. | ||
3874 | 500 | |||
3875 | 501 | @param a: The first room. | ||
3876 | 502 | |||
3877 | 503 | @type a: L{Thing} | ||
3878 | 504 | |||
3879 | 505 | @param b: The second room. | ||
3880 | 506 | |||
3881 | 507 | @type b: L{Thing} | ||
3882 | 508 | |||
3883 | 509 | @param forwardName: The name of the link going from C{a} to C{b}. For | ||
3884 | 510 | example, u'east'. | ||
3885 | 511 | |||
3886 | 512 | @type forwardName: L{unicode} | ||
3887 | 513 | |||
3888 | 514 | @param backwardName: the name of the link going from C{b} to C{a}. For | ||
3889 | 515 | example, u'west'. If not provided or L{None}, this will be | ||
3890 | 516 | computed based on L{OPPOSITE_DIRECTIONS}. | ||
3891 | 517 | |||
3892 | 518 | @type backwardName: L{unicode} | ||
3893 | 519 | """ | ||
3894 | 349 | if backwardName is None: | 520 | if backwardName is None: |
3895 | 350 | backwardName = OPPOSITE_DIRECTIONS[forwardName] | 521 | backwardName = OPPOSITE_DIRECTIONS[forwardName] |
3901 | 351 | me = cls(store=a.store, fromLocation=a, toLocation=b, name=forwardName) | 522 | forward = cls(store=a.store, fromLocation=a, toLocation=b, |
3902 | 352 | him = cls(store=b.store, fromLocation=b, toLocation=a, name=backwardName) | 523 | name=forwardName, distance=distance) |
3903 | 353 | me.sibling = him | 524 | backward = cls(store=b.store, fromLocation=b, toLocation=a, |
3904 | 354 | him.sibling = me | 525 | name=backwardName, distance=distance) |
3905 | 355 | link = classmethod(link) | 526 | forward.sibling = backward |
3906 | 527 | backward.sibling = forward | ||
3907 | 356 | 528 | ||
3908 | 357 | 529 | ||
3909 | 358 | def destroy(self): | 530 | def destroy(self): |
3910 | @@ -361,29 +533,79 @@ | |||
3911 | 361 | self.deleteFromStore() | 533 | self.deleteFromStore() |
3912 | 362 | 534 | ||
3913 | 363 | 535 | ||
3915 | 364 | # NOTHING | 536 | @remembered |
3916 | 537 | def exitIdea(self): | ||
3917 | 538 | """ | ||
3918 | 539 | This property is the L{Idea} representing this L{Exit}; this is a | ||
3919 | 540 | fairly simple L{Idea} that will link only to the L{Exit.toLocation} | ||
3920 | 541 | pointed to by this L{Exit}, with a distance annotation indicating the | ||
3921 | 542 | distance traversed to go through this L{Exit}. | ||
3922 | 543 | """ | ||
3923 | 544 | x = Idea(self) | ||
3924 | 545 | x.linkers.append(self) | ||
3925 | 546 | return x | ||
3926 | 547 | |||
3927 | 548 | |||
3928 | 549 | def links(self): | ||
3929 | 550 | """ | ||
3930 | 551 | Generate a link to the location that this exit points at. | ||
3931 | 552 | |||
3932 | 553 | @return: an iterator which yields a single L{Link}, annotated with a | ||
3933 | 554 | L{Vector} that indicates a distance of 1.0 (a temporary measure, | ||
3934 | 555 | since L{Exit}s don't have distances yet) and a direction of this | ||
3935 | 556 | exit's C{name}. | ||
3936 | 557 | """ | ||
3937 | 558 | l = Link(self.exitIdea, self.toLocation.idea) | ||
3938 | 559 | l.annotate([Vector(self.distance, self.name), | ||
3939 | 560 | # We annotate this link with ourselves because the 'Named' | ||
3940 | 561 | # retriever will use the last link in the path to determine | ||
3941 | 562 | # if an object has any aliases. We want this direction | ||
3942 | 563 | # name to be an alias for the room itself as well as the | ||
3943 | 564 | # exit, so we want to annotate the link with an INameable. | ||
3944 | 565 | # This also has an effect of annotating the link with an | ||
3945 | 566 | # IExit, and possibly one day an IItem as well (if such a | ||
3946 | 567 | # thing ever comes to exist), so perhaps we eventually want | ||
3947 | 568 | # a wrapper which elides all references here except | ||
3948 | 569 | # INameable since that's what we want. proxyForInterface | ||
3949 | 570 | # perhaps? However, for the moment, the extra annotations | ||
3950 | 571 | # do no harm, so we'll leave them there. | ||
3951 | 572 | self]) | ||
3952 | 573 | yield l | ||
3953 | 574 | |||
3954 | 575 | |||
3955 | 365 | def conceptualize(self): | 576 | def conceptualize(self): |
3958 | 366 | return language.ExpressList([u'the exit to ', language.Noun(self.toLocation).nounPhrase()]) | 577 | return language.ExpressList( |
3959 | 367 | components.registerAdapter(lambda exit: exit.conceptualize(), Exit, iimaginary.IConcept) | 578 | [u'the exit to ', language.Noun(self.toLocation).nounPhrase()]) |
3960 | 579 | |||
3961 | 580 | components.registerAdapter(lambda exit: exit.conceptualize(), | ||
3962 | 581 | Exit, iimaginary.IConcept) | ||
3963 | 582 | |||
3964 | 583 | |||
3965 | 584 | |||
3966 | 585 | class ContainmentRelationship(structlike.record("containedBy")): | ||
3967 | 586 | """ | ||
3968 | 587 | Implementation of L{iimaginary.IContainmentRelationship}. The interface | ||
3969 | 588 | specifies no methods or attributes. See its documentation for more | ||
3970 | 589 | information. | ||
3971 | 590 | """ | ||
3972 | 591 | implements(iimaginary.IContainmentRelationship) | ||
3973 | 368 | 592 | ||
3974 | 369 | 593 | ||
3975 | 370 | 594 | ||
3976 | 371 | class Containment(object): | 595 | class Containment(object): |
3978 | 372 | """Functionality for containment to be used as a mixin in Powerups. | 596 | """ |
3979 | 597 | Functionality for containment to be used as a mixin in Powerups. | ||
3980 | 373 | """ | 598 | """ |
3981 | 374 | 599 | ||
3982 | 375 | implements(iimaginary.IContainer, iimaginary.IDescriptionContributor, | 600 | implements(iimaginary.IContainer, iimaginary.IDescriptionContributor, |
3983 | 376 | iimaginary.ILinkContributor) | 601 | iimaginary.ILinkContributor) |
3985 | 377 | powerupInterfaces = (iimaginary.IContainer, iimaginary.ILinkContributor, | 602 | powerupInterfaces = (iimaginary.IContainer, |
3986 | 603 | iimaginary.ILinkContributor, | ||
3987 | 378 | iimaginary.IDescriptionContributor) | 604 | iimaginary.IDescriptionContributor) |
3988 | 379 | 605 | ||
3989 | 380 | # Units of weight which can be contained | 606 | # Units of weight which can be contained |
3990 | 381 | capacity = None | 607 | capacity = None |
3991 | 382 | 608 | ||
3992 | 383 | # Reference to another object which serves as this container's lid. | ||
3993 | 384 | # If None, this container cannot be opened or closed. | ||
3994 | 385 | # lid = None | ||
3995 | 386 | |||
3996 | 387 | # Boolean indicating whether the container is currently closed or open. | 609 | # Boolean indicating whether the container is currently closed or open. |
3997 | 388 | closed = False | 610 | closed = False |
3998 | 389 | 611 | ||
3999 | @@ -443,18 +665,164 @@ | |||
4000 | 443 | 665 | ||
4001 | 444 | # ILinkContributor | 666 | # ILinkContributor |
4002 | 445 | def links(self): | 667 | def links(self): |
4004 | 446 | d = {} | 668 | """ |
4005 | 669 | Implement L{ILinkContributor} to contribute L{Link}s to all contents of | ||
4006 | 670 | this container, as well as all of its exits, and its entrance from its | ||
4007 | 671 | location. | ||
4008 | 672 | """ | ||
4009 | 447 | if not self.closed: | 673 | if not self.closed: |
4010 | 448 | for ob in self.getContents(): | 674 | for ob in self.getContents(): |
4012 | 449 | merge(d, ob.links()) | 675 | content = Link(self.thing.idea, ob.idea) |
4013 | 676 | content.annotate([ContainmentRelationship(self)]) | ||
4014 | 677 | yield content | ||
4015 | 678 | yield Link(self.thing.idea, self._entranceIdea) | ||
4016 | 679 | yield Link(self.thing.idea, self._exitIdea) | ||
4017 | 450 | for exit in self.getExits(): | 680 | for exit in self.getExits(): |
4020 | 451 | merge(d, {exit.name: [exit.toLocation]}) | 681 | yield Link(self.thing.idea, exit.exitIdea) |
4021 | 452 | return d | 682 | |
4022 | 683 | |||
4023 | 684 | @remembered | ||
4024 | 685 | def _entranceIdea(self): | ||
4025 | 686 | """ | ||
4026 | 687 | Return an L{Idea} that reflects the implicit entrance from this | ||
4027 | 688 | container's location to the interior of the container. | ||
4028 | 689 | """ | ||
4029 | 690 | return Idea(delegate=_ContainerEntrance(self)) | ||
4030 | 691 | |||
4031 | 692 | |||
4032 | 693 | @remembered | ||
4033 | 694 | def _exitIdea(self): | ||
4034 | 695 | """ | ||
4035 | 696 | Return an L{Idea} that reflects the implicit exit from this container | ||
4036 | 697 | to its location. | ||
4037 | 698 | """ | ||
4038 | 699 | return Idea(delegate=_ContainerExit(self)) | ||
4039 | 453 | 700 | ||
4040 | 454 | 701 | ||
4041 | 455 | # IDescriptionContributor | 702 | # IDescriptionContributor |
4042 | 456 | def conceptualize(self): | 703 | def conceptualize(self): |
4044 | 457 | return ExpressSurroundings(self.getContents()) | 704 | """ |
4045 | 705 | Implement L{IDescriptionContributor} to enumerate the contents of this | ||
4046 | 706 | containment. | ||
4047 | 707 | |||
4048 | 708 | @return: an L{ExpressSurroundings} with an iterable of all visible | ||
4049 | 709 | contents of this container. | ||
4050 | 710 | """ | ||
4051 | 711 | return ExpressSurroundings( | ||
4052 | 712 | self.thing.idea.obtain( | ||
4053 | 713 | _ContainedBy(CanSee(ProviderOf(iimaginary.IThing)), self))) | ||
4054 | 714 | |||
4055 | 715 | |||
4056 | 716 | |||
4057 | 717 | class _ContainedBy(DelegatingRetriever): | ||
4058 | 718 | """ | ||
4059 | 719 | An L{iimaginary.IRetriever} which discovers only things present in a given | ||
4060 | 720 | container. Currently used only for discovering the list of things to list | ||
4061 | 721 | in a container's description. | ||
4062 | 722 | |||
4063 | 723 | @ivar retriever: a retriever to delegate to. | ||
4064 | 724 | |||
4065 | 725 | @type retriever: L{iimaginary.IRetriever} | ||
4066 | 726 | |||
4067 | 727 | @ivar container: the container to test containment by | ||
4068 | 728 | |||
4069 | 729 | @type container: L{IThing} | ||
4070 | 730 | """ | ||
4071 | 731 | |||
4072 | 732 | implements(iimaginary.IRetriever) | ||
4073 | 733 | |||
4074 | 734 | def __init__(self, retriever, container): | ||
4075 | 735 | DelegatingRetriever.__init__(self, retriever) | ||
4076 | 736 | self.container = container | ||
4077 | 737 | |||
4078 | 738 | |||
4079 | 739 | def resultRetrieved(self, path, result): | ||
4080 | 740 | """ | ||
4081 | 741 | If this L{_ContainedBy}'s container contains the last L{IThing} target | ||
4082 | 742 | of the given path, return the result of this L{_ContainedBy}'s | ||
4083 | 743 | retriever retrieving from the given C{path}, otherwise C{None}. | ||
4084 | 744 | """ | ||
4085 | 745 | containments = list(path.of(iimaginary.IContainmentRelationship)) | ||
4086 | 746 | if containments: | ||
4087 | 747 | if containments[-1].containedBy is self.container: | ||
4088 | 748 | return result | ||
4089 | 749 | |||
4090 | 750 | |||
4091 | 751 | |||
4092 | 752 | class _ContainerEntrance(structlike.record('container')): | ||
4093 | 753 | """ | ||
4094 | 754 | A L{_ContainerEntrance} is the implicit entrance to a container from its | ||
4095 | 755 | location. If a container is open, and big enough, it can be entered. | ||
4096 | 756 | |||
4097 | 757 | @ivar container: the container that this L{_ContainerEntrance} points to. | ||
4098 | 758 | |||
4099 | 759 | @type container: L{Containment} | ||
4100 | 760 | """ | ||
4101 | 761 | |||
4102 | 762 | implements(iimaginary.IExit, iimaginary.INameable) | ||
4103 | 763 | |||
4104 | 764 | @property | ||
4105 | 765 | def name(self): | ||
4106 | 766 | """ | ||
4107 | 767 | Implement L{iimaginary.IExit.name} to return a descriptive name for the | ||
4108 | 768 | inward exit of this specific container. | ||
4109 | 769 | """ | ||
4110 | 770 | return 'into ', language.Noun(self.container.thing).definiteNounPhrase() | ||
4111 | 771 | |||
4112 | 772 | |||
4113 | 773 | def traverse(self, thing): | ||
4114 | 774 | """ | ||
4115 | 775 | Implement L{iimaginary.IExit.traverse} to move the thing in transit to | ||
4116 | 776 | the container specified. | ||
4117 | 777 | """ | ||
4118 | 778 | thing.moveTo(self.container) | ||
4119 | 779 | |||
4120 | 780 | |||
4121 | 781 | def knownTo(self, observer, name): | ||
4122 | 782 | """ | ||
4123 | 783 | Delegate L{iimaginary.INameable.knownTo} to this | ||
4124 | 784 | L{_ContainerEntrance}'s container's thing. | ||
4125 | 785 | """ | ||
4126 | 786 | return self.container.thing.knownTo(observer, name) | ||
4127 | 787 | |||
4128 | 788 | |||
4129 | 789 | |||
4130 | 790 | class _ContainerExit(structlike.record('container')): | ||
4131 | 791 | """ | ||
4132 | 792 | A L{_ContainerExit} is the exit from a container, or specifically, a | ||
4133 | 793 | L{Containment}; an exit by which actors may move to the container's | ||
4134 | 794 | container. | ||
4135 | 795 | |||
4136 | 796 | @ivar container: the container that this L{_ContainerExit} points out from. | ||
4137 | 797 | |||
4138 | 798 | @type container: L{Containment} | ||
4139 | 799 | """ | ||
4140 | 800 | |||
4141 | 801 | implements(iimaginary.IExit, iimaginary.INameable) | ||
4142 | 802 | |||
4143 | 803 | @property | ||
4144 | 804 | def name(self): | ||
4145 | 805 | """ | ||
4146 | 806 | Implement L{iimaginary.IExit.name} to return a descriptive name for the | ||
4147 | 807 | outward exit of this specific container. | ||
4148 | 808 | """ | ||
4149 | 809 | return 'out of ', language.Noun(self.container.thing).definiteNounPhrase() | ||
4150 | 810 | |||
4151 | 811 | |||
4152 | 812 | def traverse(self, thing): | ||
4153 | 813 | """ | ||
4154 | 814 | Implement L{iimaginary.IExit.traverse} to move the thing in transit to | ||
4155 | 815 | the container specified. | ||
4156 | 816 | """ | ||
4157 | 817 | thing.moveTo(self.container.thing.location) | ||
4158 | 818 | |||
4159 | 819 | |||
4160 | 820 | def knownTo(self, observer, name): | ||
4161 | 821 | """ | ||
4162 | 822 | This L{_ContainerExit} is known to observers inside it as 'out' | ||
4163 | 823 | (i.e. 'go out', 'look out'), but otherwise it has no known description. | ||
4164 | 824 | """ | ||
4165 | 825 | return (observer.location == self.container.thing) and (name == 'out') | ||
4166 | 458 | 826 | ||
4167 | 459 | 827 | ||
4168 | 460 | 828 | ||
4169 | @@ -467,19 +835,24 @@ | |||
4170 | 467 | 835 | ||
4171 | 468 | 836 | ||
4172 | 469 | class Container(item.Item, Containment, _Enhancement): | 837 | class Container(item.Item, Containment, _Enhancement): |
4186 | 470 | """A generic powerup that implements containment.""" | 838 | """ |
4187 | 471 | 839 | A generic L{_Enhancement} that implements containment. | |
4188 | 472 | capacity = attributes.integer(doc=""" | 840 | """ |
4189 | 473 | Units of weight which can be contained. | 841 | |
4190 | 474 | """, allowNone=False, default=1) | 842 | capacity = attributes.integer( |
4191 | 475 | 843 | doc=""" | |
4192 | 476 | closed = attributes.boolean(doc=""" | 844 | Units of weight which can be contained. |
4193 | 477 | Indicates whether the container is currently closed or open. | 845 | """, allowNone=False, default=1) |
4194 | 478 | """, allowNone=False, default=False) | 846 | |
4195 | 479 | 847 | closed = attributes.boolean( | |
4196 | 480 | thing = attributes.reference(doc=""" | 848 | doc=""" |
4197 | 481 | The object this container powers up. | 849 | Indicates whether the container is currently closed or open. |
4198 | 482 | """) | 850 | """, allowNone=False, default=False) |
4199 | 851 | |||
4200 | 852 | thing = attributes.reference( | ||
4201 | 853 | doc=""" | ||
4202 | 854 | The object this container powers up. | ||
4203 | 855 | """) | ||
4204 | 483 | 856 | ||
4205 | 484 | 857 | ||
4206 | 485 | 858 | ||
4207 | @@ -488,14 +861,17 @@ | |||
4208 | 488 | 861 | ||
4209 | 489 | def vt102(self, observer): | 862 | def vt102(self, observer): |
4210 | 490 | return [ | 863 | return [ |
4212 | 491 | [T.bold, T.fg.yellow, language.Noun(self.original.thing).shortName().plaintext(observer)], | 864 | [T.bold, T.fg.yellow, language.Noun( |
4213 | 865 | self.original.thing).shortName().plaintext(observer)], | ||
4214 | 492 | u" is ", | 866 | u" is ", |
4215 | 493 | [T.bold, T.fg.red, self.original._condition(), u"."]] | 867 | [T.bold, T.fg.red, self.original._condition(), u"."]] |
4216 | 494 | 868 | ||
4217 | 495 | 869 | ||
4218 | 496 | class Actable(object): | 870 | class Actable(object): |
4219 | 497 | implements(iimaginary.IActor, iimaginary.IEventObserver) | 871 | implements(iimaginary.IActor, iimaginary.IEventObserver) |
4221 | 498 | powerupInterfaces = (iimaginary.IActor, iimaginary.IEventObserver, iimaginary.IDescriptionContributor) | 872 | |
4222 | 873 | powerupInterfaces = (iimaginary.IActor, iimaginary.IEventObserver, | ||
4223 | 874 | iimaginary.IDescriptionContributor) | ||
4224 | 499 | 875 | ||
4225 | 500 | # Yay, experience! | 876 | # Yay, experience! |
4226 | 501 | experience = 0 | 877 | experience = 0 |
4227 | @@ -514,8 +890,6 @@ | |||
4228 | 514 | 'great') | 890 | 'great') |
4229 | 515 | 891 | ||
4230 | 516 | 892 | ||
4231 | 517 | |||
4232 | 518 | |||
4233 | 519 | # IDescriptionContributor | 893 | # IDescriptionContributor |
4234 | 520 | def conceptualize(self): | 894 | def conceptualize(self): |
4235 | 521 | return ExpressCondition(self) | 895 | return ExpressCondition(self) |
4236 | @@ -651,62 +1025,182 @@ | |||
4237 | 651 | 1025 | ||
4238 | 652 | 1026 | ||
4239 | 653 | class LocationLighting(item.Item, _Enhancement): | 1027 | class LocationLighting(item.Item, _Enhancement): |
4251 | 654 | implements(iimaginary.ILocationProxy) | 1028 | """ |
4252 | 655 | powerupInterfaces = (iimaginary.ILocationProxy,) | 1029 | A L{LocationLighting} is an enhancement for a location which allows the |
4253 | 656 | 1030 | location's description and behavior to depend on its lighting. While | |
4254 | 657 | candelas = attributes.integer(doc=""" | 1031 | L{LocationLighting} includes its own ambient lighting number, it is not |
4255 | 658 | The luminous intensity in candelas. | 1032 | really a light source, it's just a location which is I{affected by} light |
4256 | 659 | 1033 | sources; for lighting, you should use L{LightSource}. | |
4257 | 660 | See U{http://en.wikipedia.org/wiki/Candela}. | 1034 | |
4258 | 661 | """, default=100, allowNone=False) | 1035 | By default, in Imaginary, rooms are considered by to be lit to an |
4259 | 662 | 1036 | acceptable level that actors can see and interact with both the room and | |
4260 | 663 | thing = attributes.reference() | 1037 | everything in it without worrying about light. By contrast, any room that |
4261 | 664 | 1038 | can be dark needs to have a L{LocationLighting} installed. A room affected | |
4262 | 1039 | by a L{LocationLighting} which is lit will behave like a normal room, but a | ||
4263 | 1040 | room affected by a L{LocationLighting} with no available light sources will | ||
4264 | 1041 | prevent players from performing actions which require targets that need to | ||
4265 | 1042 | be seen, and seeing the room's description. | ||
4266 | 1043 | """ | ||
4267 | 1044 | |||
4268 | 1045 | implements(iimaginary.ILocationLinkAnnotator) | ||
4269 | 1046 | powerupInterfaces = (iimaginary.ILocationLinkAnnotator,) | ||
4270 | 1047 | |||
4271 | 1048 | candelas = attributes.integer( | ||
4272 | 1049 | doc=""" | ||
4273 | 1050 | The ambient luminous intensity in candelas. | ||
4274 | 1051 | |||
4275 | 1052 | See U{http://en.wikipedia.org/wiki/Candela}. | ||
4276 | 1053 | """, default=100, allowNone=False) | ||
4277 | 1054 | |||
4278 | 1055 | thing = attributes.reference( | ||
4279 | 1056 | doc=""" | ||
4280 | 1057 | The location being affected by lighting. | ||
4281 | 1058 | """, | ||
4282 | 1059 | reftype=Thing, | ||
4283 | 1060 | allowNone=False, | ||
4284 | 1061 | whenDeleted=attributes.reference.CASCADE) | ||
4285 | 665 | 1062 | ||
4286 | 666 | def getCandelas(self): | 1063 | def getCandelas(self): |
4287 | 667 | """ | 1064 | """ |
4288 | 668 | Sum the candelas of all light sources within a limited distance from | 1065 | Sum the candelas of all light sources within a limited distance from |
4289 | 669 | the location this is installed on and return the result. | 1066 | the location this is installed on and return the result. |
4290 | 670 | """ | 1067 | """ |
4293 | 671 | sum = 0 | 1068 | sum = self.candelas |
4294 | 672 | for candle in self.thing.findProviders(iimaginary.ILightSource, 1): | 1069 | for candle in self.thing.idea.obtain( |
4295 | 1070 | Proximity(1, ProviderOf(iimaginary.ILightSource))): | ||
4296 | 673 | sum += candle.candelas | 1071 | sum += candle.candelas |
4297 | 674 | return sum | 1072 | return sum |
4298 | 675 | 1073 | ||
4299 | 676 | 1074 | ||
4309 | 677 | def proxy(self, facet, interface): | 1075 | def annotationsFor(self, link, idea): |
4310 | 678 | if interface is iimaginary.IVisible: | 1076 | """ |
4311 | 679 | if self.getCandelas(): | 1077 | Yield a L{_PossiblyDark} annotation for all links pointing to objects |
4312 | 680 | return facet | 1078 | located in the C{thing} attribute of this L{LocationLighting}. |
4313 | 681 | elif facet.thing is self.thing: | 1079 | """ |
4314 | 682 | return _DarkLocationProxy(self.thing) | 1080 | if link.target is idea: |
4315 | 683 | else: | 1081 | yield _PossiblyDark(self) |
4307 | 684 | return None | ||
4308 | 685 | return facet | ||
4316 | 686 | 1082 | ||
4317 | 687 | 1083 | ||
4318 | 688 | 1084 | ||
4319 | 689 | class _DarkLocationProxy(structlike.record('thing')): | 1085 | class _DarkLocationProxy(structlike.record('thing')): |
4320 | 1086 | """ | ||
4321 | 1087 | An L{IVisible} implementation for darkened locations. | ||
4322 | 1088 | """ | ||
4323 | 1089 | |||
4324 | 690 | implements(iimaginary.IVisible) | 1090 | implements(iimaginary.IVisible) |
4325 | 691 | 1091 | ||
4326 | 692 | def visualize(self): | 1092 | def visualize(self): |
4327 | 1093 | """ | ||
4328 | 1094 | Return a L{DescriptionConcept} that tells the player they can't see. | ||
4329 | 1095 | """ | ||
4330 | 693 | return language.DescriptionConcept( | 1096 | return language.DescriptionConcept( |
4331 | 694 | u"Blackness", | 1097 | u"Blackness", |
4332 | 695 | u"You cannot see anything because it is very dark.") | 1098 | u"You cannot see anything because it is very dark.") |
4333 | 696 | 1099 | ||
4334 | 697 | 1100 | ||
4335 | 1101 | def isViewOf(self, thing): | ||
4336 | 1102 | """ | ||
4337 | 1103 | Implement L{IVisible.isViewOf} to delegate to this | ||
4338 | 1104 | L{_DarkLocationProxy}'s L{Thing}'s L{IVisible.isViewOf}. | ||
4339 | 1105 | |||
4340 | 1106 | In other words, this L{_DarkLocationProxy} C{isViewOf} its C{thing}. | ||
4341 | 1107 | """ | ||
4342 | 1108 | return self.thing.isViewOf(thing) | ||
4343 | 1109 | |||
4344 | 1110 | |||
4345 | 698 | 1111 | ||
4346 | 699 | class LightSource(item.Item, _Enhancement): | 1112 | class LightSource(item.Item, _Enhancement): |
4347 | 1113 | """ | ||
4348 | 1114 | A simple implementation of L{ILightSource} which provides a fixed number of | ||
4349 | 1115 | candelas of luminous intensity, assumed to be emitted uniformly in all | ||
4350 | 1116 | directions. | ||
4351 | 1117 | """ | ||
4352 | 1118 | |||
4353 | 700 | implements(iimaginary.ILightSource) | 1119 | implements(iimaginary.ILightSource) |
4354 | 701 | powerupInterfaces = (iimaginary.ILightSource,) | 1120 | powerupInterfaces = (iimaginary.ILightSource,) |
4355 | 702 | 1121 | ||
4366 | 703 | candelas = attributes.integer(doc=""" | 1122 | candelas = attributes.integer( |
4367 | 704 | The luminous intensity in candelas. | 1123 | doc=""" |
4368 | 705 | 1124 | The luminous intensity in candelas. | |
4369 | 706 | See U{http://en.wikipedia.org/wiki/Candela}. | 1125 | |
4370 | 707 | """, default=1, allowNone=False) | 1126 | See U{http://en.wikipedia.org/wiki/Candela}. |
4371 | 708 | 1127 | """, default=1, allowNone=False) | |
4372 | 709 | thing = attributes.reference() | 1128 | |
4373 | 710 | 1129 | thing = attributes.reference( | |
4374 | 711 | 1130 | doc=""" | |
4375 | 712 | 1131 | The physical body emitting the light. | |
4376 | 1132 | """, | ||
4377 | 1133 | reftype=Thing, | ||
4378 | 1134 | allowNone=False, | ||
4379 | 1135 | whenDeleted=attributes.reference.CASCADE) | ||
4380 | 1136 | |||
4381 | 1137 | |||
4382 | 1138 | |||
4383 | 1139 | class _PossiblyDark(structlike.record("lighting")): | ||
4384 | 1140 | """ | ||
4385 | 1141 | A L{_PossiblyDark} is a link annotation which specifies that the target of | ||
4386 | 1142 | the link may be affected by lighting. | ||
4387 | 1143 | |||
4388 | 1144 | @ivar lighting: the lighting for a particular location. | ||
4389 | 1145 | |||
4390 | 1146 | @type lighting: L{LocationLighting} | ||
4391 | 1147 | """ | ||
4392 | 1148 | |||
4393 | 1149 | implements(iimaginary.IWhyNot, iimaginary.ILitLink) | ||
4394 | 1150 | |||
4395 | 1151 | def tellMeWhyNot(self): | ||
4396 | 1152 | """ | ||
4397 | 1153 | Return a helpful message explaining why something may not be accessible | ||
4398 | 1154 | due to poor lighting. | ||
4399 | 1155 | """ | ||
4400 | 1156 | return "It's too dark to see." | ||
4401 | 1157 | |||
4402 | 1158 | |||
4403 | 1159 | def isItLit(self, path, result): | ||
4404 | 1160 | """ | ||
4405 | 1161 | Determine if the given result, viewed via the given path, appears to be | ||
4406 | 1162 | lit. | ||
4407 | 1163 | |||
4408 | 1164 | @return: L{True} if the result should be lit, L{False} if it is dark. | ||
4409 | 1165 | |||
4410 | 1166 | @rtype: C{bool} | ||
4411 | 1167 | """ | ||
4412 | 1168 | # XXX wrong, we need to examine this exactly the same way applyLighting | ||
4413 | 1169 | # does. CanSee and Visibility *are* the same object now so it is | ||
4414 | 1170 | # possible to do. | ||
4415 | 1171 | if self.lighting.getCandelas(): | ||
4416 | 1172 | return True | ||
4417 | 1173 | litThing = list(path.eachTargetAs(iimaginary.IThing))[-1] | ||
4418 | 1174 | if _eventuallyContains(self.lighting.thing, litThing): | ||
4419 | 1175 | val = litThing is self.lighting.thing | ||
4420 | 1176 | #print 'checking if', litThing, 'is lit:', val | ||
4421 | 1177 | return val | ||
4422 | 1178 | else: | ||
4423 | 1179 | return True | ||
4424 | 1180 | |||
4425 | 1181 | |||
4426 | 1182 | def whyNotLit(self): | ||
4427 | 1183 | """ | ||
4428 | 1184 | Return an L{iimaginary.IWhyNot} provider explaining why the target of | ||
4429 | 1185 | this link is not lit. (Return 'self', since L{_PossiblyDark} is an | ||
4430 | 1186 | L{iimaginary.IWhyNot} provider itself.) | ||
4431 | 1187 | """ | ||
4432 | 1188 | return self | ||
4433 | 1189 | |||
4434 | 1190 | |||
4435 | 1191 | def applyLighting(self, litThing, eventualTarget, requestedInterface): | ||
4436 | 1192 | """ | ||
4437 | 1193 | Implement L{iimaginary.ILitLink.applyLighting} to return a | ||
4438 | 1194 | L{_DarkLocationProxy} for the room lit by this | ||
4439 | 1195 | L{_PossiblyDark.lighting}, C{None} for any items in that room, or | ||
4440 | 1196 | C{eventualTarget} if the target is in a different place. | ||
4441 | 1197 | """ | ||
4442 | 1198 | if self.lighting.getCandelas(): | ||
4443 | 1199 | return eventualTarget | ||
4444 | 1200 | elif (eventualTarget is self.lighting.thing and | ||
4445 | 1201 | requestedInterface is iimaginary.IVisible): | ||
4446 | 1202 | return _DarkLocationProxy(self.lighting.thing) | ||
4447 | 1203 | elif _eventuallyContains(self.lighting.thing, litThing): | ||
4448 | 1204 | return None | ||
4449 | 1205 | else: | ||
4450 | 1206 | return eventualTarget | ||
4451 | 713 | 1207 | ||
4452 | === modified file 'Imaginary/imaginary/resources/motd' | |||
4453 | --- Imaginary/imaginary/resources/motd 2006-04-12 02:41:46 +0000 | |||
4454 | +++ Imaginary/imaginary/resources/motd 2011-09-16 20:42:26 +0000 | |||
4455 | @@ -2,4 +2,4 @@ | |||
4456 | 2 | TWISTED %(twistedVersion)s | 2 | TWISTED %(twistedVersion)s |
4457 | 3 | PYTHON %(pythonVersion)s | 3 | PYTHON %(pythonVersion)s |
4458 | 4 | 4 | ||
4460 | 5 | Written by Jp Calderone (exarkun@twistedmatrix.com) | 5 | Created by IMAGINARY TEAM |
4461 | 6 | 6 | ||
4462 | === modified file 'Imaginary/imaginary/test/commandutils.py' | |||
4463 | --- Imaginary/imaginary/test/commandutils.py 2009-06-29 12:25:10 +0000 | |||
4464 | +++ Imaginary/imaginary/test/commandutils.py 2011-09-16 20:42:26 +0000 | |||
4465 | @@ -26,6 +26,17 @@ | |||
4466 | 26 | """ | 26 | """ |
4467 | 27 | A mixin for TestCase classes which provides support for testing Imaginary | 27 | A mixin for TestCase classes which provides support for testing Imaginary |
4468 | 28 | environments via command-line transcripts. | 28 | environments via command-line transcripts. |
4469 | 29 | |||
4470 | 30 | @ivar store: the L{store.Store} containing all the relevant game objects. | ||
4471 | 31 | |||
4472 | 32 | @ivar location: The location where the test is taking place. | ||
4473 | 33 | |||
4474 | 34 | @ivar world: The L{ImaginaryWorld} that created the player. | ||
4475 | 35 | |||
4476 | 36 | @ivar player: The L{Thing} representing the main player. | ||
4477 | 37 | |||
4478 | 38 | @ivar observer: The L{Thing} representing the observer who sees the main | ||
4479 | 39 | player's actions. | ||
4480 | 29 | """ | 40 | """ |
4481 | 30 | 41 | ||
4482 | 31 | def setUp(self): | 42 | def setUp(self): |
4483 | 32 | 43 | ||
4484 | === modified file 'Imaginary/imaginary/test/test_actions.py' | |||
4485 | --- Imaginary/imaginary/test/test_actions.py 2009-06-29 04:03:17 +0000 | |||
4486 | +++ Imaginary/imaginary/test/test_actions.py 2011-09-16 20:42:26 +0000 | |||
4487 | @@ -500,6 +500,49 @@ | |||
4488 | 500 | ["Test Player arrives from the west."]) | 500 | ["Test Player arrives from the west."]) |
4489 | 501 | 501 | ||
4490 | 502 | 502 | ||
4491 | 503 | def test_goThroughOneWayExit(self): | ||
4492 | 504 | """ | ||
4493 | 505 | Going through a one-way exit with a known direction will announce that | ||
4494 | 506 | the player arrived from that direction; with an unknown direction it | ||
4495 | 507 | will simply announce that they have arrived. | ||
4496 | 508 | """ | ||
4497 | 509 | secretRoom = objects.Thing(store=self.store, name=u'Secret Room!') | ||
4498 | 510 | objects.Container.createFor(secretRoom, capacity=1000) | ||
4499 | 511 | myExit = objects.Exit(store=self.store, fromLocation=secretRoom, | ||
4500 | 512 | toLocation=self.location, name=u'north') | ||
4501 | 513 | self.player.moveTo(secretRoom) | ||
4502 | 514 | self._test( | ||
4503 | 515 | "north", | ||
4504 | 516 | [E("[ Test Location ]"), | ||
4505 | 517 | "Location for testing.", | ||
4506 | 518 | "Observer Player"], | ||
4507 | 519 | ["Test Player arrives from the south."]) | ||
4508 | 520 | self.player.moveTo(secretRoom) | ||
4509 | 521 | myExit.name = u'elsewhere' | ||
4510 | 522 | self.assertCommandOutput( | ||
4511 | 523 | "go elsewhere", | ||
4512 | 524 | [E("[ Test Location ]"), | ||
4513 | 525 | "Location for testing.", | ||
4514 | 526 | "Observer Player"], | ||
4515 | 527 | ["Test Player arrives."]) | ||
4516 | 528 | |||
4517 | 529 | |||
4518 | 530 | def test_goDoesntJumpOverExits(self): | ||
4519 | 531 | """ | ||
4520 | 532 | You can't go through an exit without passing through exits which lead | ||
4521 | 533 | to it. Going through an exit named 'east' will only work if it is east | ||
4522 | 534 | of your I{present} location, even if it is easily reachable from where | ||
4523 | 535 | you stand. | ||
4524 | 536 | """ | ||
4525 | 537 | northRoom = objects.Thing(store=self.store, name=u'Northerly') | ||
4526 | 538 | eastRoom = objects.Thing(store=self.store, name=u'Easterly') | ||
4527 | 539 | for room in northRoom, eastRoom: | ||
4528 | 540 | objects.Container.createFor(room, capacity=1000) | ||
4529 | 541 | objects.Exit.link(self.location, northRoom, u'north', distance=0.1) | ||
4530 | 542 | objects.Exit.link(northRoom, eastRoom, u'east', distance=0.1) | ||
4531 | 543 | self.assertCommandOutput("go east", [E("You can't go that way.")], []) | ||
4532 | 544 | |||
4533 | 545 | |||
4534 | 503 | def testDirectionalMovement(self): | 546 | def testDirectionalMovement(self): |
4535 | 504 | # A couple tweaks to state to make the test simpler | 547 | # A couple tweaks to state to make the test simpler |
4536 | 505 | self.observer.location = None | 548 | self.observer.location = None |
4537 | 506 | 549 | ||
4538 | === modified file 'Imaginary/imaginary/test/test_container.py' | |||
4539 | --- Imaginary/imaginary/test/test_container.py 2009-06-29 04:03:17 +0000 | |||
4540 | +++ Imaginary/imaginary/test/test_container.py 2011-09-16 20:42:26 +0000 | |||
4541 | @@ -5,6 +5,7 @@ | |||
4542 | 5 | from axiom import store | 5 | from axiom import store |
4543 | 6 | 6 | ||
4544 | 7 | from imaginary import eimaginary, objects | 7 | from imaginary import eimaginary, objects |
4545 | 8 | from imaginary.test.commandutils import CommandTestCaseMixin, E | ||
4546 | 8 | 9 | ||
4547 | 9 | class ContainerTestCase(unittest.TestCase): | 10 | class ContainerTestCase(unittest.TestCase): |
4548 | 10 | def setUp(self): | 11 | def setUp(self): |
4549 | @@ -65,3 +66,57 @@ | |||
4550 | 65 | self.assertRaises(eimaginary.Closed, self.container.remove, self.object) | 66 | self.assertRaises(eimaginary.Closed, self.container.remove, self.object) |
4551 | 66 | self.assertEquals(list(self.container.getContents()), [self.object]) | 67 | self.assertEquals(list(self.container.getContents()), [self.object]) |
4552 | 67 | self.assertIdentical(self.object.location, self.containmentCore) | 68 | self.assertIdentical(self.object.location, self.containmentCore) |
4553 | 69 | |||
4554 | 70 | |||
4555 | 71 | |||
4556 | 72 | class IngressAndEgressTestCase(CommandTestCaseMixin, unittest.TestCase): | ||
4557 | 73 | """ | ||
4558 | 74 | I should be able to enter and exit containers that are sufficiently big. | ||
4559 | 75 | """ | ||
4560 | 76 | |||
4561 | 77 | def setUp(self): | ||
4562 | 78 | """ | ||
4563 | 79 | Create a container, C{self.box} that is large enough to stand in. | ||
4564 | 80 | """ | ||
4565 | 81 | CommandTestCaseMixin.setUp(self) | ||
4566 | 82 | self.box = objects.Thing(store=self.store, name=u'box') | ||
4567 | 83 | self.container = objects.Container.createFor(self.box, capacity=1000) | ||
4568 | 84 | self.box.moveTo(self.location) | ||
4569 | 85 | |||
4570 | 86 | |||
4571 | 87 | def test_enterBox(self): | ||
4572 | 88 | """ | ||
4573 | 89 | I should be able to enter the box. | ||
4574 | 90 | """ | ||
4575 | 91 | self.assertCommandOutput( | ||
4576 | 92 | 'enter box', | ||
4577 | 93 | [E('[ Test Location ]'), | ||
4578 | 94 | 'Location for testing.', | ||
4579 | 95 | 'Observer Player and a box'], | ||
4580 | 96 | ['Test Player leaves into the box.']) | ||
4581 | 97 | |||
4582 | 98 | |||
4583 | 99 | def test_exitBox(self): | ||
4584 | 100 | """ | ||
4585 | 101 | I should be able to exit the box. | ||
4586 | 102 | """ | ||
4587 | 103 | self.player.moveTo(self.container) | ||
4588 | 104 | self.assertCommandOutput( | ||
4589 | 105 | 'exit out', | ||
4590 | 106 | [E('[ Test Location ]'), | ||
4591 | 107 | 'Location for testing.', | ||
4592 | 108 | 'Observer Player and a box'], | ||
4593 | 109 | ['Test Player leaves out of the box.']) | ||
4594 | 110 | self.assertEquals(self.player.location, | ||
4595 | 111 | self.location) | ||
4596 | 112 | |||
4597 | 113 | |||
4598 | 114 | def test_enterWhileHoldingBox(self): | ||
4599 | 115 | """ | ||
4600 | 116 | When I'm holding a container, I shouldn't be able to enter it. | ||
4601 | 117 | """ | ||
4602 | 118 | self.container.thing.moveTo(self.player) | ||
4603 | 119 | self.assertCommandOutput('enter box', | ||
4604 | 120 | ["The box won't fit inside itself."], | ||
4605 | 121 | []) | ||
4606 | 122 | |||
4607 | 68 | 123 | ||
4608 | === modified file 'Imaginary/imaginary/test/test_garments.py' | |||
4609 | --- Imaginary/imaginary/test/test_garments.py 2009-06-29 04:03:17 +0000 | |||
4610 | +++ Imaginary/imaginary/test/test_garments.py 2011-09-16 20:42:26 +0000 | |||
4611 | @@ -31,8 +31,7 @@ | |||
4612 | 31 | 31 | ||
4613 | 32 | def testWearing(self): | 32 | def testWearing(self): |
4614 | 33 | self.wearer.putOn(self.shirtGarment) | 33 | self.wearer.putOn(self.shirtGarment) |
4617 | 34 | 34 | self.assertIdentical(self.shirt.location, self.dummy) | |
4616 | 35 | self.assertEquals(self.shirt.location, None) | ||
4618 | 36 | 35 | ||
4619 | 37 | 36 | ||
4620 | 38 | 37 | ||
4621 | @@ -115,6 +114,26 @@ | |||
4622 | 115 | self.assertIdentical(self.dukes.location, self.daisy) | 114 | self.assertIdentical(self.dukes.location, self.daisy) |
4623 | 116 | 115 | ||
4624 | 117 | 116 | ||
4625 | 117 | def test_cantDropSomethingYouAreWearing(self): | ||
4626 | 118 | """ | ||
4627 | 119 | If you're wearing an article of clothing, you should not be able to | ||
4628 | 120 | drop it until you first take it off. After taking it off, however, you | ||
4629 | 121 | can move it around just fine. | ||
4630 | 122 | """ | ||
4631 | 123 | wearer = iimaginary.IClothingWearer(self.daisy) | ||
4632 | 124 | wearer.putOn(iimaginary.IClothing(self.undies)) | ||
4633 | 125 | af = self.assertRaises(ActionFailure, self.undies.moveTo, | ||
4634 | 126 | self.daisy.location) | ||
4635 | 127 | self.assertEquals( | ||
4636 | 128 | u''.join(af.event.plaintext(self.daisy)), | ||
4637 | 129 | u"You can't move the pair of lacy underwear " | ||
4638 | 130 | u"without removing it first.\n") | ||
4639 | 131 | |||
4640 | 132 | wearer.takeOff(iimaginary.IClothing(self.undies)) | ||
4641 | 133 | self.undies.moveTo(self.daisy.location) | ||
4642 | 134 | self.assertEquals(self.daisy.location, self.undies.location) | ||
4643 | 135 | |||
4644 | 136 | |||
4645 | 118 | def testTakeOffUnderwearBeforePants(self): | 137 | def testTakeOffUnderwearBeforePants(self): |
4646 | 119 | # TODO - underwear removal skill | 138 | # TODO - underwear removal skill |
4647 | 120 | wearer = iimaginary.IClothingWearer(self.daisy) | 139 | wearer = iimaginary.IClothingWearer(self.daisy) |
4648 | @@ -174,10 +193,19 @@ | |||
4649 | 174 | 193 | ||
4650 | 175 | 194 | ||
4651 | 176 | class FunSimulationStuff(commandutils.CommandTestCaseMixin, unittest.TestCase): | 195 | class FunSimulationStuff(commandutils.CommandTestCaseMixin, unittest.TestCase): |
4652 | 196 | |||
4653 | 197 | def createPants(self): | ||
4654 | 198 | """ | ||
4655 | 199 | Create a pair of Daisy Dukes for the test player to wear. | ||
4656 | 200 | """ | ||
4657 | 201 | self._test("create pants named 'pair of daisy dukes'", | ||
4658 | 202 | ["You create a pair of daisy dukes."], | ||
4659 | 203 | ["Test Player creates a pair of daisy dukes."]) | ||
4660 | 204 | |||
4661 | 205 | |||
4662 | 206 | |||
4663 | 177 | def testWearIt(self): | 207 | def testWearIt(self): |
4667 | 178 | self._test("create pants named 'pair of daisy dukes'", | 208 | self.createPants() |
4665 | 179 | ["You create a pair of daisy dukes."], | ||
4666 | 180 | ["Test Player creates a pair of daisy dukes."]) | ||
4668 | 181 | self._test("wear 'pair of daisy dukes'", | 209 | self._test("wear 'pair of daisy dukes'", |
4669 | 182 | ["You put on the pair of daisy dukes."], | 210 | ["You put on the pair of daisy dukes."], |
4670 | 183 | ["Test Player puts on a pair of daisy dukes."]) | 211 | ["Test Player puts on a pair of daisy dukes."]) |
4671 | @@ -188,9 +216,7 @@ | |||
4672 | 188 | A garment can be removed with the I{take off} action or the | 216 | A garment can be removed with the I{take off} action or the |
4673 | 189 | I{remove} action. | 217 | I{remove} action. |
4674 | 190 | """ | 218 | """ |
4678 | 191 | self._test("create pants named 'pair of daisy dukes'", | 219 | self.createPants() |
4676 | 192 | ["You create a pair of daisy dukes."], | ||
4677 | 193 | ["Test Player creates a pair of daisy dukes."]) | ||
4679 | 194 | self._test("wear 'pair of daisy dukes'", | 220 | self._test("wear 'pair of daisy dukes'", |
4680 | 195 | ["You put on the pair of daisy dukes."], | 221 | ["You put on the pair of daisy dukes."], |
4681 | 196 | ["Test Player puts on a pair of daisy dukes."]) | 222 | ["Test Player puts on a pair of daisy dukes."]) |
4682 | @@ -207,9 +233,7 @@ | |||
4683 | 207 | 233 | ||
4684 | 208 | 234 | ||
4685 | 209 | def testProperlyDressed(self): | 235 | def testProperlyDressed(self): |
4689 | 210 | self._test("create pants named 'pair of daisy dukes'", | 236 | self.createPants() |
4687 | 211 | ["You create a pair of daisy dukes."], | ||
4688 | 212 | ["Test Player creates a pair of daisy dukes."]) | ||
4690 | 213 | self._test("create underwear named 'pair of lace panties'", | 237 | self._test("create underwear named 'pair of lace panties'", |
4691 | 214 | ["You create a pair of lace panties."], | 238 | ["You create a pair of lace panties."], |
4692 | 215 | ["Test Player creates a pair of lace panties."]) | 239 | ["Test Player creates a pair of lace panties."]) |
4693 | @@ -227,9 +251,7 @@ | |||
4694 | 227 | 251 | ||
4695 | 228 | 252 | ||
4696 | 229 | def testTooBulky(self): | 253 | def testTooBulky(self): |
4700 | 230 | self._test("create pants named 'pair of daisy dukes'", | 254 | self.createPants() |
4698 | 231 | ["You create a pair of daisy dukes."], | ||
4699 | 232 | ["Test Player creates a pair of daisy dukes."]) | ||
4701 | 233 | self._test("create pants named 'pair of overalls'", | 255 | self._test("create pants named 'pair of overalls'", |
4702 | 234 | ["You create a pair of overalls."], | 256 | ["You create a pair of overalls."], |
4703 | 235 | ["Test Player creates a pair of overalls."]) | 257 | ["Test Player creates a pair of overalls."]) |
4704 | @@ -248,9 +270,7 @@ | |||
4705 | 248 | 270 | ||
4706 | 249 | 271 | ||
4707 | 250 | def testInaccessibleGarment(self): | 272 | def testInaccessibleGarment(self): |
4711 | 251 | self._test("create pants named 'pair of daisy dukes'", | 273 | self.createPants() |
4709 | 252 | ["You create a pair of daisy dukes."], | ||
4710 | 253 | ["Test Player creates a pair of daisy dukes."]) | ||
4712 | 254 | self._test("create underwear named 'pair of lace panties'", | 274 | self._test("create underwear named 'pair of lace panties'", |
4713 | 255 | ["You create a pair of lace panties."], | 275 | ["You create a pair of lace panties."], |
4714 | 256 | ["Test Player creates a pair of lace panties."]) | 276 | ["Test Player creates a pair of lace panties."]) |
4715 | @@ -266,9 +286,7 @@ | |||
4716 | 266 | 286 | ||
4717 | 267 | 287 | ||
4718 | 268 | def testEquipment(self): | 288 | def testEquipment(self): |
4722 | 269 | self._test("create pants named 'pair of daisy dukes'", | 289 | self.createPants() |
4720 | 270 | ["You create a pair of daisy dukes."], | ||
4721 | 271 | ["Test Player creates a pair of daisy dukes."]) | ||
4723 | 272 | self._test("create underwear named 'pair of lace panties'", | 290 | self._test("create underwear named 'pair of lace panties'", |
4724 | 273 | ["You create a pair of lace panties."], | 291 | ["You create a pair of lace panties."], |
4725 | 274 | ["Test Player creates a pair of lace panties."]) | 292 | ["Test Player creates a pair of lace panties."]) |
4726 | 275 | 293 | ||
4727 | === added file 'Imaginary/imaginary/test/test_idea.py' | |||
4728 | --- Imaginary/imaginary/test/test_idea.py 1970-01-01 00:00:00 +0000 | |||
4729 | +++ Imaginary/imaginary/test/test_idea.py 2011-09-16 20:42:26 +0000 | |||
4730 | @@ -0,0 +1,241 @@ | |||
4731 | 1 | |||
4732 | 2 | """ | ||
4733 | 3 | Some basic unit tests for L{imaginary.idea} (but many tests for this code are in | ||
4734 | 4 | other modules instead). | ||
4735 | 5 | """ | ||
4736 | 6 | |||
4737 | 7 | from zope.interface import implements | ||
4738 | 8 | |||
4739 | 9 | from twisted.trial.unittest import TestCase | ||
4740 | 10 | |||
4741 | 11 | from epsilon.structlike import record | ||
4742 | 12 | |||
4743 | 13 | from imaginary.iimaginary import ( | ||
4744 | 14 | IWhyNot, INameable, ILinkContributor, IObstruction, ILinkAnnotator, | ||
4745 | 15 | IElectromagneticMedium) | ||
4746 | 16 | from imaginary.language import ExpressString | ||
4747 | 17 | from imaginary.idea import ( | ||
4748 | 18 | Idea, Link, Path, AlsoKnownAs, ProviderOf, Named, DelegatingRetriever, | ||
4749 | 19 | Reachable, CanSee) | ||
4750 | 20 | |||
4751 | 21 | |||
4752 | 22 | class Reprable(record('repr')): | ||
4753 | 23 | def __repr__(self): | ||
4754 | 24 | return self.repr | ||
4755 | 25 | |||
4756 | 26 | |||
4757 | 27 | class PathTests(TestCase): | ||
4758 | 28 | """ | ||
4759 | 29 | Tests for L{imaginary.idea.Path}. | ||
4760 | 30 | """ | ||
4761 | 31 | def test_repr(self): | ||
4762 | 32 | """ | ||
4763 | 33 | A L{Path} instance can be rendered into a string by C{repr}. | ||
4764 | 34 | """ | ||
4765 | 35 | key = Idea(AlsoKnownAs("key")) | ||
4766 | 36 | table = Idea(AlsoKnownAs("table")) | ||
4767 | 37 | hall = Idea(AlsoKnownAs("hall")) | ||
4768 | 38 | path = Path([Link(hall, table), Link(table, key)]) | ||
4769 | 39 | self.assertEquals( | ||
4770 | 40 | repr(path), | ||
4771 | 41 | "Path(\n" | ||
4772 | 42 | "\t'hall' => 'table' []\n" | ||
4773 | 43 | "\t'table' => 'key' [])") | ||
4774 | 44 | |||
4775 | 45 | |||
4776 | 46 | def test_unnamedDelegate(self): | ||
4777 | 47 | """ | ||
4778 | 48 | The I{repr} of a L{Path} containing delegates without names includes the | ||
4779 | 49 | I{repr} of the delegates. | ||
4780 | 50 | """ | ||
4781 | 51 | key = Idea(Reprable("key")) | ||
4782 | 52 | table = Idea(Reprable("table")) | ||
4783 | 53 | hall = Idea(Reprable("hall")) | ||
4784 | 54 | path = Path([Link(hall, table), Link(table, key)]) | ||
4785 | 55 | self.assertEquals( | ||
4786 | 56 | repr(path), | ||
4787 | 57 | "Path(\n" | ||
4788 | 58 | "\thall => table []\n" | ||
4789 | 59 | "\ttable => key [])") | ||
4790 | 60 | |||
4791 | 61 | |||
4792 | 62 | |||
4793 | 63 | class OneLink(record('link')): | ||
4794 | 64 | implements(ILinkContributor) | ||
4795 | 65 | |||
4796 | 66 | def links(self): | ||
4797 | 67 | return [self.link] | ||
4798 | 68 | |||
4799 | 69 | |||
4800 | 70 | class TooHigh(object): | ||
4801 | 71 | implements(IWhyNot) | ||
4802 | 72 | |||
4803 | 73 | def tellMeWhyNot(self): | ||
4804 | 74 | return ExpressString("the table is too high") | ||
4805 | 75 | |||
4806 | 76 | |||
4807 | 77 | class ArmsReach(DelegatingRetriever): | ||
4808 | 78 | """ | ||
4809 | 79 | Restrict retrievable to things within arm's reach. | ||
4810 | 80 | |||
4811 | 81 | alas for poor Alice! when she got to the door, she found he had | ||
4812 | 82 | forgotten the little golden key, and when she went back to the table for | ||
4813 | 83 | it, she found she could not possibly reach it: | ||
4814 | 84 | """ | ||
4815 | 85 | def moreObjectionsTo(self, path, result): | ||
4816 | 86 | """ | ||
4817 | 87 | Object to finding the key. | ||
4818 | 88 | """ | ||
4819 | 89 | # This isn't a very good implementation of ArmsReach. It doesn't | ||
4820 | 90 | # actually check distances or paths or anything. It just knows the | ||
4821 | 91 | # key is on the table, and Alice is too short. | ||
4822 | 92 | named = path.targetAs(INameable) | ||
4823 | 93 | if named.knownTo(None, "key"): | ||
4824 | 94 | return [TooHigh()] | ||
4825 | 95 | return [] | ||
4826 | 96 | |||
4827 | 97 | |||
4828 | 98 | class WonderlandSetupMixin: | ||
4829 | 99 | """ | ||
4830 | 100 | A test case mixin which sets up a graph based on a scene from Alice in | ||
4831 | 101 | Wonderland. | ||
4832 | 102 | """ | ||
4833 | 103 | def setUp(self): | ||
4834 | 104 | garden = Idea(AlsoKnownAs("garden")) | ||
4835 | 105 | door = Idea(AlsoKnownAs("door")) | ||
4836 | 106 | hall = Idea(AlsoKnownAs("hall")) | ||
4837 | 107 | alice = Idea(AlsoKnownAs("alice")) | ||
4838 | 108 | key = Idea(AlsoKnownAs("key")) | ||
4839 | 109 | table = Idea(AlsoKnownAs("table")) | ||
4840 | 110 | |||
4841 | 111 | alice.linkers.append(OneLink(Link(alice, hall))) | ||
4842 | 112 | hall.linkers.append(OneLink(Link(hall, door))) | ||
4843 | 113 | hall.linkers.append(OneLink(Link(hall, table))) | ||
4844 | 114 | table.linkers.append(OneLink(Link(table, key))) | ||
4845 | 115 | door.linkers.append(OneLink(Link(door, garden))) | ||
4846 | 116 | |||
4847 | 117 | self.alice = alice | ||
4848 | 118 | self.hall = hall | ||
4849 | 119 | self.door = door | ||
4850 | 120 | self.garden = garden | ||
4851 | 121 | self.table = table | ||
4852 | 122 | self.key = key | ||
4853 | 123 | |||
4854 | 124 | |||
4855 | 125 | |||
4856 | 126 | class IdeaTests(WonderlandSetupMixin, TestCase): | ||
4857 | 127 | """ | ||
4858 | 128 | Tests for L{imaginary.idea.Idea}. | ||
4859 | 129 | """ | ||
4860 | 130 | def test_objections(self): | ||
4861 | 131 | """ | ||
4862 | 132 | The L{IRetriver} passed to L{Idea.obtain} can object to certain results. | ||
4863 | 133 | This excludes them from the result returned by L{Idea.obtain}. | ||
4864 | 134 | """ | ||
4865 | 135 | # XXX The last argument is the observer, and is supposed to be an | ||
4866 | 136 | # IThing. | ||
4867 | 137 | retriever = Named("key", ProviderOf(INameable), self.alice) | ||
4868 | 138 | |||
4869 | 139 | # Sanity check. Alice should be able to reach the key if we don't | ||
4870 | 140 | # restrict things based on her height. | ||
4871 | 141 | self.assertEquals( | ||
4872 | 142 | list(self.alice.obtain(retriever)), [self.key.delegate]) | ||
4873 | 143 | |||
4874 | 144 | # But when we consider how short she is, she should not be able to reach | ||
4875 | 145 | # it. | ||
4876 | 146 | results = self.alice.obtain(ArmsReach(retriever)) | ||
4877 | 147 | self.assertEquals(list(results), []) | ||
4878 | 148 | |||
4879 | 149 | |||
4880 | 150 | class Closed(object): | ||
4881 | 151 | implements(IObstruction) | ||
4882 | 152 | |||
4883 | 153 | def whyNot(self): | ||
4884 | 154 | return ExpressString("the door is closed") | ||
4885 | 155 | |||
4886 | 156 | |||
4887 | 157 | |||
4888 | 158 | class ConstantAnnotation(record('annotation')): | ||
4889 | 159 | implements(ILinkAnnotator) | ||
4890 | 160 | |||
4891 | 161 | def annotationsFor(self, link, idea): | ||
4892 | 162 | return [self.annotation] | ||
4893 | 163 | |||
4894 | 164 | |||
4895 | 165 | |||
4896 | 166 | class ReachableTests(WonderlandSetupMixin, TestCase): | ||
4897 | 167 | """ | ||
4898 | 168 | Tests for L{imaginary.idea.Reachable}. | ||
4899 | 169 | """ | ||
4900 | 170 | def setUp(self): | ||
4901 | 171 | WonderlandSetupMixin.setUp(self) | ||
4902 | 172 | # XXX The last argument is the observer, and is supposed to be an | ||
4903 | 173 | # IThing. | ||
4904 | 174 | self.retriever = Reachable( | ||
4905 | 175 | Named("garden", ProviderOf(INameable), self.alice)) | ||
4906 | 176 | |||
4907 | 177 | |||
4908 | 178 | def test_anyObstruction(self): | ||
4909 | 179 | """ | ||
4910 | 180 | If there are any obstructions in the path traversed by the retriever | ||
4911 | 181 | wrapped by L{Reachable}, L{Reachable} objects to them and they are not | ||
4912 | 182 | returned by L{Idea.obtain}. | ||
4913 | 183 | """ | ||
4914 | 184 | # Make the door closed.. Now Alice cannot reach the garden. | ||
4915 | 185 | self.door.annotators.append(ConstantAnnotation(Closed())) | ||
4916 | 186 | self.assertEquals(list(self.alice.obtain(self.retriever)), []) | ||
4917 | 187 | |||
4918 | 188 | |||
4919 | 189 | def test_noObstruction(self): | ||
4920 | 190 | """ | ||
4921 | 191 | If there are no obstructions in the path traversed by the retriever | ||
4922 | 192 | wrapped by L{Reachable}, all results are returned by L{Idea.obtain}. | ||
4923 | 193 | """ | ||
4924 | 194 | self.assertEquals( | ||
4925 | 195 | list(self.alice.obtain(self.retriever)), | ||
4926 | 196 | [self.garden.delegate]) | ||
4927 | 197 | |||
4928 | 198 | |||
4929 | 199 | class Wood(object): | ||
4930 | 200 | implements(IElectromagneticMedium) | ||
4931 | 201 | |||
4932 | 202 | def isOpaque(self): | ||
4933 | 203 | return True | ||
4934 | 204 | |||
4935 | 205 | |||
4936 | 206 | |||
4937 | 207 | class Glass(object): | ||
4938 | 208 | implements(IElectromagneticMedium) | ||
4939 | 209 | |||
4940 | 210 | def isOpaque(self): | ||
4941 | 211 | return False | ||
4942 | 212 | |||
4943 | 213 | |||
4944 | 214 | class CanSeeTests(WonderlandSetupMixin, TestCase): | ||
4945 | 215 | """ | ||
4946 | 216 | Tests for L{imaginary.idea.CanSee}. | ||
4947 | 217 | """ | ||
4948 | 218 | def setUp(self): | ||
4949 | 219 | WonderlandSetupMixin.setUp(self) | ||
4950 | 220 | self.retriever = CanSee( | ||
4951 | 221 | Named("garden", ProviderOf(INameable), self.alice)) | ||
4952 | 222 | |||
4953 | 223 | |||
4954 | 224 | def test_throughTransparent(self): | ||
4955 | 225 | """ | ||
4956 | 226 | L{Idea.obtain} continues past an L{IElectromagneticMedium} which returns | ||
4957 | 227 | C{False} from its C{isOpaque} method. | ||
4958 | 228 | """ | ||
4959 | 229 | self.door.annotators.append(ConstantAnnotation(Glass())) | ||
4960 | 230 | self.assertEquals( | ||
4961 | 231 | list(self.alice.obtain(self.retriever)), [self.garden.delegate]) | ||
4962 | 232 | |||
4963 | 233 | |||
4964 | 234 | def test_notThroughOpaque(self): | ||
4965 | 235 | """ | ||
4966 | 236 | L{Idea.obtain} does not continue past an L{IElectromagneticMedium} which | ||
4967 | 237 | returns C{True} from its C{isOpaque} method. | ||
4968 | 238 | """ | ||
4969 | 239 | # Make the door opaque. Now Alice cannot see the garden. | ||
4970 | 240 | self.door.annotators.append(ConstantAnnotation(Wood())) | ||
4971 | 241 | self.assertEquals(list(self.alice.obtain(self.retriever)), []) | ||
4972 | 0 | 242 | ||
4973 | === modified file 'Imaginary/imaginary/test/test_illumination.py' | |||
4974 | --- Imaginary/imaginary/test/test_illumination.py 2009-06-29 04:03:17 +0000 | |||
4975 | +++ Imaginary/imaginary/test/test_illumination.py 2011-09-16 20:42:26 +0000 | |||
4976 | @@ -1,8 +1,13 @@ | |||
4977 | 1 | |||
4978 | 2 | from zope.interface import implements | ||
4979 | 3 | |||
4980 | 1 | from twisted.trial import unittest | 4 | from twisted.trial import unittest |
4981 | 2 | 5 | ||
4983 | 3 | from axiom import store | 6 | from axiom import store, item, attributes |
4984 | 4 | 7 | ||
4986 | 5 | from imaginary import iimaginary, objects | 8 | from imaginary.enhancement import Enhancement |
4987 | 9 | from imaginary import iimaginary, objects, idea | ||
4988 | 10 | from imaginary.language import ExpressString | ||
4989 | 6 | from imaginary.manipulation import Manipulator | 11 | from imaginary.manipulation import Manipulator |
4990 | 7 | 12 | ||
4991 | 8 | from imaginary.test import commandutils | 13 | from imaginary.test import commandutils |
4992 | @@ -67,14 +72,27 @@ | |||
4993 | 67 | self.assertEquals(len(found), 3) | 72 | self.assertEquals(len(found), 3) |
4994 | 68 | 73 | ||
4995 | 69 | 74 | ||
4997 | 70 | def testNonVisibilityUnaffected(self): | 75 | def test_nonVisibilityAffected(self): |
4998 | 71 | """ | 76 | """ |
4999 | 72 | Test that the LocationLightning thingy doesn't block out non-IVisible | ||
5000 | 73 | stuff. |
The diff has been truncated for viewing.
Okay. Branch is too large. It is a challenge to understand (either the change or just the new code on its own). Too large partly because it does too many things - adds better naming features, updates unrelated documentation, changes how actions are dispatched, changes the action class hierarchy naming, etc. Hard to understand because the simulation core now hinges on a number of distinct concepts - links, annotations, obstructions, retrievers, maybe some others - but lacks any documentation about how they relate to each other (Thing.__doc__ is a noble effort, but gives up about 1/6th of the way though).
I think near future efforts on Imaginary need to focus on documentation, examples, and test coverage. Once the branch is merged, I hope efforts will pick up on those fronts.