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