Merge lp:~divmod-dev/divmod.org/obtain-2824-6 into lp:divmod.org

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
Reviewer Review Type Date Requested Status
Jean-Paul Calderone Approve
Review via email: mp+71631@code.launchpad.net

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 _DisregardYourWearingIt 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
Jean-Paul Calderone (exarkun) wrote :

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.

review: Approve
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.

Subscribers

People subscribed via source and target branches

to all changes: