← Back to team overview

divmod-dev team mailing list archive

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

 

Glyph Lefkowitz has proposed merging lp:~divmod-dev/divmod.org/obtain-2824-6 into lp:divmod.org.

Requested reviews:
  Divmod-dev (divmod-dev)

For more details, see:
https://code.launchpad.net/~divmod-dev/divmod.org/obtain-2824-6/+merge/71631

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.
-- 
https://code.launchpad.net/~divmod-dev/divmod.org/obtain-2824-6/+merge/71631
Your team Divmod-dev is requested to review the proposed merge of lp:~divmod-dev/divmod.org/obtain-2824-6 into lp:divmod.org.
=== added file 'Epsilon/epsilon/remember.py'
--- Epsilon/epsilon/remember.py	1970-01-01 00:00:00 +0000
+++ Epsilon/epsilon/remember.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,39 @@
+# -*- test-case-name: epsilon.test.test_remember -*-
+
+"""
+This module implements a utility for managing the lifecycle of attributes
+related to a particular object.
+"""
+
+from epsilon.structlike import record
+
+class remembered(record('creationFunction')):
+    """
+    This descriptor decorator is applied to a function to create an attribute
+    which will be created on-demand, but remembered for the lifetime of the
+    instance to which it is attached.  Subsequent accesses of the attribute
+    will return the remembered value.
+
+    @ivar creationFunction: the decorated function, to be called to create the
+        value.  This should be a 1-argument callable, that takes only a 'self'
+        parameter, like a method.
+    """
+
+    value = None
+
+    def __get__(self, oself, type):
+        """
+        Retrieve the value if already cached, otherwise, call the
+        C{creationFunction} to create it.
+        """
+        remembername = "_remembered_" + self.creationFunction.func_name
+        rememberedval = oself.__dict__.get(remembername, None)
+        if rememberedval is not None:
+            return rememberedval
+        rememberme = self.creationFunction(oself)
+        oself.__dict__[remembername] = rememberme
+        return rememberme
+
+
+
+__all__ = ['remembered']

=== added file 'Epsilon/epsilon/test/test_remember.py'
--- Epsilon/epsilon/test/test_remember.py	1970-01-01 00:00:00 +0000
+++ Epsilon/epsilon/test/test_remember.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,90 @@
+
+from twisted.trial.unittest import TestCase
+
+from epsilon.remember import remembered
+from epsilon.structlike import record
+
+class Rememberee(record("rememberer whichValue")):
+    """
+    A sample value that holds on to its L{Rememberer}.
+    """
+
+
+class Rememberer(object):
+    """
+    Sample application code which uses epsilon.remember.
+
+    @ivar invocations: The number of times that it is invoked.
+    """
+
+    invocations = 0
+    otherInvocations = 0
+
+    @remembered
+    def value1(self):
+        """
+        I remember a value.
+        """
+        self.invocations += 1
+        return Rememberee(self, 1)
+
+
+    @remembered
+    def value2(self):
+        """
+        A separate value.
+        """
+        self.otherInvocations += 1
+        return Rememberee(self, 2)
+
+
+class RememberedTests(TestCase):
+    """
+    The "remembered" decorator allows you to lazily create an attribute and
+    remember it.
+    """
+
+    def setUp(self):
+        """
+        Create a L{Rememberer} for use with the tests.
+        """
+        self.rememberer = Rememberer()
+
+
+    def test_selfArgument(self):
+        """
+        The "self" argument to the decorated creation function will be the
+        instance the property is accessed upon.
+        """
+        value = self.rememberer.value1
+        self.assertIdentical(value.rememberer, self.rememberer)
+
+
+    def test_onlyOneInvocation(self):
+        """
+        The callable wrapped by C{@remembered} will only be invoked once,
+        regardless of how many times the attribute is accessed.
+        """
+        self.assertEquals(self.rememberer.invocations, 0)
+        firstTime = self.rememberer.value1
+        self.assertEquals(self.rememberer.invocations, 1)
+        secondTime = self.rememberer.value1
+        self.assertEquals(self.rememberer.invocations, 1)
+        self.assertIdentical(firstTime, secondTime)
+
+
+    def test_twoValues(self):
+        """
+        If the L{@remembered} decorator is used more than once, each one will
+        be an attribute with its own identity.
+        """
+        self.assertEquals(self.rememberer.invocations, 0)
+        self.assertEquals(self.rememberer.otherInvocations, 0)
+        firstValue1 = self.rememberer.value1
+        self.assertEquals(self.rememberer.invocations, 1)
+        self.assertEquals(self.rememberer.otherInvocations, 0)
+        firstValue2 = self.rememberer.value2
+        self.assertEquals(self.rememberer.otherInvocations, 1)
+        self.assertNotIdentical(firstValue1, firstValue2)
+        secondValue2 = self.rememberer.value2
+        self.assertIdentical(firstValue2, secondValue2)

=== added file 'Imaginary/COMPATIBILITY.txt'
--- Imaginary/COMPATIBILITY.txt	1970-01-01 00:00:00 +0000
+++ Imaginary/COMPATIBILITY.txt	2011-08-16 01:57:22 +0000
@@ -0,0 +1,6 @@
+Imaginary provides _no_ source-level compatibility from one release to the next.
+
+Efforts will be made to provide database level compatibility (i.e. data from
+one release can be loaded with the next).  However, although we will try to
+ensure that data will load, there is no guarantee that it will be meaningfully
+upgraded yet.

=== added file 'Imaginary/ExampleGame/examplegame/furniture.py'
--- Imaginary/ExampleGame/examplegame/furniture.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/furniture.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,139 @@
+# -*- test-case-name: examplegame.test.test_furniture -*-
+
+"""
+
+    Furniture is the mass noun for the movable objects which may support the
+    human body (seating furniture and beds), provide storage, or hold objects
+    on horizontal surfaces above the ground.
+
+        -- Wikipedia, http://en.wikipedia.org/wiki/Furniture
+
+L{imaginary.furniture} contains L{Action}s which allow players to interact with
+household objects such as chairs and tables, and L{Enhancement}s which allow
+L{Thing}s to behave as same.
+
+This has the same implementation weakness as L{examplegame.tether}, in that it
+needs to make some assumptions about who is moving what in its restrictions of
+movement; it should be moved into imaginary proper when that can be properly
+addressed.  It's also a bit too draconian in terms of preventing the player
+from moving for any reason just because they're seated.  However, it's a
+workable approximation for some uses, and thus useful as an example.
+"""
+
+from zope.interface import implements
+
+from axiom.item import Item
+from axiom.attributes import reference
+
+from imaginary.iimaginary import ISittable, IContainer, IMovementRestriction
+from imaginary.eimaginary import ActionFailure
+from imaginary.events import ThatDoesntWork
+from imaginary.language import Noun
+from imaginary.action import Action, TargetAction
+from imaginary.events import Success
+from imaginary.enhancement import Enhancement
+from imaginary.objects import Container
+from imaginary.pyparsing import Literal, Optional, restOfLine
+
+class Sit(TargetAction):
+    """
+    An action allowing a player to sit down in a chair.
+    """
+    expr = (Literal("sit") + Optional(Literal("on")) +
+            restOfLine.setResultsName("target"))
+
+    targetInterface = ISittable
+
+    def do(self, player, line, target):
+        """
+        Do the action; sit down.
+        """
+        target.seat(player)
+
+        actorMessage=["You sit in ",
+                      Noun(target.thing).definiteNounPhrase(),"."]
+        otherMessage=[player.thing, " sits in ",
+                      Noun(target.thing).definiteNounPhrase(),"."]
+        Success(actor=player.thing, location=player.thing.location,
+                actorMessage=actorMessage,
+                otherMessage=otherMessage).broadcast()
+
+
+class Stand(Action):
+    """
+    Stand up from a sitting position.
+    """
+    expr = (Literal("stand") + Optional(Literal("up")))
+
+    def do(self, player, line):
+        """
+        Do the action; stand up.
+        """
+        # XXX This is wrong.  I should be issuing an obtain() query to find
+        # something that qualifies as "my location" or "the thing I'm already
+        # sitting in".
+        chair = ISittable(player.thing.location, None)
+        if chair is None:
+            raise ActionFailure(ThatDoesntWork(
+                    actor=player.thing,
+                    actorMessage=["You're already standing."]))
+        chair.unseat(player)
+        Success(actor=player.thing, location=player.thing.location,
+                actorMessage=["You stand up."],
+                otherMessage=[player.thing, " stands up."]).broadcast()
+
+
+
+class Chair(Enhancement, Item):
+    """
+    A chair is a thing you can sit in.
+    """
+
+    implements(ISittable, IMovementRestriction)
+
+    powerupInterfaces = [ISittable]
+
+    thing = reference()
+    container = reference()
+
+
+    def movementImminent(self, movee, destination):
+        """
+        A player tried to move while they were seated.  Prevent them from doing
+        so, noting that they must stand first.
+
+        (Assume the player was trying to move themselves, although there's no
+        way to know currently.)
+        """
+        raise ActionFailure(ThatDoesntWork(
+                actor=movee,
+                actorMessage=u"You can't do that while sitting down."))
+
+
+    def applyEnhancement(self):
+        """
+        Apply this enhancement to this L{Chair}'s thing, creating a
+        L{Container} to hold the seated player, if necessary.
+        """
+        super(Chair, self).applyEnhancement()
+        container = IContainer(self.thing, None)
+        if container is None:
+            container = Container.createFor(self.thing, capacity=300)
+        self.container = container
+
+
+    def seat(self, player):
+        """
+        The player sat down on this chair; place them into it and prevent them
+        from moving elsewhere until they stand up.
+        """
+        player.thing.moveTo(self.container)
+        player.thing.powerUp(self, IMovementRestriction)
+
+
+    def unseat(self, player):
+        """
+        The player stood up; remove them from this chair.
+        """
+        player.thing.powerDown(self, IMovementRestriction)
+        player.thing.moveTo(self.container.thing.location)

=== added file 'Imaginary/ExampleGame/examplegame/glass.py'
--- Imaginary/ExampleGame/examplegame/glass.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/glass.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,72 @@
+# -*- test-case-name: examplegame.test.test_glass -*-
+"""
+This example implements a transparent container that you can see, but not
+reach, the contents of.
+"""
+from zope.interface import implements
+
+from axiom.item import Item
+from axiom.attributes import reference
+
+from imaginary.iimaginary import (
+    ILinkContributor, IWhyNot, IObstruction, IContainer)
+from imaginary.enhancement import Enhancement
+from imaginary.objects import ContainmentRelationship
+from imaginary.idea import Link
+
+class _CantReachThroughGlassBox(object):
+    """
+    This object provides an explanation for why the user cannot access a target
+    that is inside a L{imaginary.objects.Thing} enhanced with a L{GlassBox}.
+    """
+    implements(IWhyNot)
+
+    def tellMeWhyNot(self):
+        """
+        Return a simple message explaining that the user can't reach through
+        the glass box.
+        """
+        return "You can't reach through the glass box."
+
+
+
+class _ObstructedByGlass(object):
+    """
+    This is an annotation on a link between two objects which represents a
+    physical obstruction between them.  It is used to annotate between a
+    L{GlassBox} and its contents, so you can see them without reaching them.
+    """
+    implements(IObstruction)
+
+    def whyNot(self):
+        """
+        @return: an object which explains why you can't reach through the glass
+        box.
+        """
+        return _CantReachThroughGlassBox()
+
+
+
+class GlassBox(Item, Enhancement):
+    """
+    L{GlassBox} is an L{Enhancement} which modifies a container such that it is
+    contained.
+    """
+
+    powerupInterfaces = (ILinkContributor,)
+
+    thing = reference()
+
+    def links(self):
+        """
+        If the container attached to this L{GlassBox}'s C{thing} is closed,
+        yield its list of contents with each link annotated with
+        L{_ObstructedByGlass}, indicating that the object cannot be reached.
+        """
+        container = IContainer(self.thing)
+        if container.closed:
+            for content in container.getContents():
+                link = Link(self.thing.idea, content.idea)
+                link.annotate([_ObstructedByGlass(),
+                               ContainmentRelationship(container)])
+                yield link

=== modified file 'Imaginary/ExampleGame/examplegame/mice.py'
--- Imaginary/ExampleGame/examplegame/mice.py	2009-06-29 04:03:17 +0000
+++ Imaginary/ExampleGame/examplegame/mice.py	2011-08-16 01:57:22 +0000
@@ -242,7 +242,7 @@
             character = random.choice(japanese.hiragana.keys())
         self._currentChallenge = character
         actor = self._actor()
-        action.Say().do(actor.thing, None, character)
+        action.Say().do(actor, None, character)
 
 
 

=== added file 'Imaginary/ExampleGame/examplegame/squeaky.py'
--- Imaginary/ExampleGame/examplegame/squeaky.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/squeaky.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,42 @@
+# -*- test-case-name: examplegame.test.test_squeaky -*-
+
+"""
+This module implements an L{ILinkAnnotator} which causes an object to squeak
+when it is moved.  It should serve as a simple example for overriding what
+happens when an action is executed (in this case, 'take' and 'drop').
+"""
+
+from zope.interface import implements
+
+from axiom.item import Item
+from axiom.attributes import reference
+
+from imaginary.iimaginary import IMovementRestriction, IConcept
+from imaginary.events import Success
+from imaginary.enhancement import Enhancement
+from imaginary.objects import Thing
+
+
+class Squeaker(Item, Enhancement):
+    """
+    This is an L{Enhancement} which, when installed on a L{Thing}, causes that
+    L{Thing} to squeak when you pick it up.
+    """
+
+    implements(IMovementRestriction)
+
+    powerupInterfaces = [IMovementRestriction]
+
+    thing = reference(allowNone=False,
+                      whenDeleted=reference.CASCADE,
+                      reftype=Thing)
+
+
+    def movementImminent(self, movee, destination):
+        """
+        The object enhanced by this L{Squeaker} is about to move - emit a
+        L{Success} event which describes its squeak.
+        """
+        Success(otherMessage=(IConcept(self.thing).capitalizeConcept(),
+                              " emits a faint squeak."),
+                location=self.thing.location).broadcast()

=== added file 'Imaginary/ExampleGame/examplegame/test/test_furniture.py'
--- Imaginary/ExampleGame/examplegame/test/test_furniture.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_furniture.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,107 @@
+
+"""
+This module contains tests for the examplegame.furniture module.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from imaginary.test.commandutils import CommandTestCaseMixin, E
+
+from imaginary.objects import Thing, Container, Exit
+from examplegame.furniture import Chair
+
+class SitAndStandTests(CommandTestCaseMixin, TestCase):
+    """
+    Tests for the 'sit' and 'stand' actions.
+    """
+
+    def setUp(self):
+        """
+        Create a room, with a dude in it, and a chair he can sit in.
+        """
+        CommandTestCaseMixin.setUp(self)
+        self.chairThing = Thing(store=self.store, name=u"chair")
+        self.chairThing.moveTo(self.location)
+        self.chair = Chair.createFor(self.chairThing)
+
+
+    def test_sitDown(self):
+        """
+        Sitting in a chair should move your location to that chair.
+        """
+        self.assertCommandOutput(
+            "sit chair",
+            ["You sit in the chair."],
+            ["Test Player sits in the chair."])
+        self.assertEquals(self.player.location, self.chair.thing)
+
+
+    def test_standWhenStanding(self):
+        """
+        You can't stand up - you're already standing up.
+        """
+        self.assertCommandOutput(
+            "stand up",
+            ["You're already standing."])
+
+
+    def test_standWhenSitting(self):
+        """
+        If a player stands up when sitting in a chair, they should be seen to
+        stand up, and they should be placed back into the room where the chair
+        is located.
+        """
+        self.test_sitDown()
+        self.assertCommandOutput(
+            "stand up",
+            ["You stand up."],
+            ["Test Player stands up."])
+        self.assertEquals(self.player.location, self.location)
+
+
+    def test_takeWhenSitting(self):
+        """
+        When a player is seated, they should still be able to take objects on
+        the floor around them.
+        """
+        self.test_sitDown()
+        self.ball = Thing(store=self.store, name=u'ball')
+        self.ball.moveTo(self.location)
+        self.assertCommandOutput(
+            "take ball",
+            ["You take a ball."],
+            ["Test Player takes a ball."])
+
+
+    def test_moveWhenSitting(self):
+        """
+        A player who is sitting shouldn't be able to move without standing up
+        first.
+        """
+        self.test_sitDown()
+        otherRoom = Thing(store=self.store, name=u'elsewhere')
+        Container.createFor(otherRoom, capacity=1000)
+        Exit.link(self.location, otherRoom, u'north')
+        self.assertCommandOutput(
+            "go north",
+            ["You can't do that while sitting down."])
+        self.assertCommandOutput(
+            "go south",
+            ["You can't go that way."])
+
+
+    def test_lookWhenSitting(self):
+        """
+        Looking around when sitting should display the description of the room.
+        """
+        self.test_sitDown()
+        self.assertCommandOutput(
+            "look",
+            # I'd like to add ', in the chair' to this test, but there's
+            # currently no way to modify the name of the object being looked
+            # at.
+            [E("[ Test Location ]"),
+             "Location for testing.",
+             "Observer Player and a chair"])
+
+

=== added file 'Imaginary/ExampleGame/examplegame/test/test_glass.py'
--- Imaginary/ExampleGame/examplegame/test/test_glass.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_glass.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,95 @@
+
+"""
+Tests for L{examplegame.glass}
+"""
+
+from twisted.trial.unittest import TestCase
+
+from imaginary.test.commandutils import CommandTestCaseMixin, E
+
+from imaginary.objects import Thing, Container
+
+from examplegame.glass import GlassBox
+
+class GlassBoxTests(CommandTestCaseMixin, TestCase):
+    """
+    Tests for L{GlassBox}
+    """
+
+    def setUp(self):
+        """
+        Create a room with a L{GlassBox} in it, which itself contains a ball.
+        """
+        CommandTestCaseMixin.setUp(self)
+        self.box = Thing(store=self.store, name=u'box',
+                         description=u'The system under test.')
+        self.ball = Thing(store=self.store, name=u'ball',
+                          description=u'an interesting object')
+        self.container = Container.createFor(self.box)
+        GlassBox.createFor(self.box)
+        self.ball.moveTo(self.box)
+        self.box.moveTo(self.location)
+        self.container.closed = True
+
+
+    def test_lookThrough(self):
+        """
+        You can see items within a glass box by looking at them directly.
+        """
+        self.assertCommandOutput(
+            "look at ball",
+            [E("[ ball ]"),
+             "an interesting object"])
+
+
+    def test_lookAt(self):
+        """
+        You can see the contents within a glass box by looking at the box.
+        """
+        self.assertCommandOutput(
+            "look at box",
+            [E("[ box ]"),
+             "The system under test.",
+             "a ball"])
+
+
+    def test_take(self):
+        """
+        You can't take items within a glass box.
+        """
+        self.assertCommandOutput(
+            "get ball",
+            ["You can't reach through the glass box."])
+
+
+    def test_openTake(self):
+        """
+        Taking items from a glass box should work if it's open.
+        """
+        self.container.closed = False
+        self.assertCommandOutput(
+            "get ball",
+            ["You take a ball."],
+            ["Test Player takes a ball."])
+
+
+    def test_put(self):
+        """
+        You can't put items into a glass box.
+        """
+        self.container.closed = False
+        self.ball.moveTo(self.location)
+        self.container.closed = True
+        self.assertCommandOutput(
+            "put ball in box",
+            ["The box is closed."])
+
+
+    def test_whyNot(self):
+        """
+        A regression test; there was a bug where glass boxes would interfere
+        with normal target-acquisition error reporting.
+        """
+        self.assertCommandOutput(
+            "get foobar",
+            ["Nothing like that around here."])

=== added file 'Imaginary/ExampleGame/examplegame/test/test_squeaky.py'
--- Imaginary/ExampleGame/examplegame/test/test_squeaky.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_squeaky.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,51 @@
+
+from twisted.trial.unittest import TestCase
+
+from imaginary.test.commandutils import CommandTestCaseMixin
+
+from imaginary.objects import Thing, Container
+
+from examplegame.squeaky import Squeaker
+
+class SqueakTest(CommandTestCaseMixin, TestCase):
+    """
+    Squeak Test.
+    """
+
+    def setUp(self):
+        """
+        Set Up.
+        """
+        CommandTestCaseMixin.setUp(self)
+        self.squeaker = Thing(store=self.store, name=u"squeaker")
+        self.squeaker.moveTo(self.location)
+        self.squeakification = Squeaker.createFor(self.squeaker)
+
+
+    def test_itSqueaks(self):
+        """
+        Picking up a squeaky thing makes it emit a squeak.
+        """
+        self.assertCommandOutput(
+            "take squeaker",
+            ["You take a squeaker.",
+             "A squeaker emits a faint squeak."],
+            ["Test Player takes a squeaker.",
+             "A squeaker emits a faint squeak."])
+
+
+    def test_squeakyContainer(self):
+        """
+        If a container is squeaky, that shouldn't interfere with its function
+        as a container.  (i.e. let's make sure that links keep working even
+        though we're using an annotator here.)
+        """
+        cont = Container.createFor(self.squeaker)
+
+        mcguffin = Thing(store=self.store, name=u"mcguffin")
+        mcguffin.moveTo(cont)
+
+        self.assertCommandOutput(
+            "take mcguffin from squeaker",
+            ["You take a mcguffin from the squeaker."],
+            ["Test Player takes a mcguffin from the squeaker."])

=== added file 'Imaginary/ExampleGame/examplegame/test/test_tether.py'
--- Imaginary/ExampleGame/examplegame/test/test_tether.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_tether.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,96 @@
+
+from twisted.trial.unittest import TestCase
+
+from imaginary.test.commandutils import CommandTestCaseMixin, E
+
+from imaginary.objects import Thing, Container, Exit
+from imaginary.garments import Garment
+
+from examplegame.furniture import Chair
+from examplegame.tether import Tether
+
+class TetherTest(CommandTestCaseMixin, TestCase):
+    """
+    A test for tethering an item to its location, such that a player who picks
+    it up can't leave until they drop it.
+    """
+
+    def setUp(self):
+        """
+        Tether a ball to the room.
+        """
+        CommandTestCaseMixin.setUp(self)
+        self.ball = Thing(store=self.store, name=u'ball')
+        self.ball.moveTo(self.location)
+        self.tether = Tether.createFor(self.ball, to=self.location)
+        self.otherPlace = Thing(store=self.store, name=u'elsewhere')
+        Container.createFor(self.otherPlace, capacity=1000)
+        Exit.link(self.location, self.otherPlace, u'north')
+
+
+    def test_takeAndLeave(self):
+        """
+        You can't leave the room if you're holding the ball that's tied to it.
+        """
+        self.assertCommandOutput(
+            "take ball",
+            ["You take a ball."],
+            ["Test Player takes a ball."])
+        self.assertCommandOutput(
+            "go north",
+            ["You can't move, you're still holding a ball."],
+            ["Test Player struggles with a ball."])
+        self.assertCommandOutput(
+            "drop ball",
+            ["You drop the ball."],
+            ["Test Player drops a ball."])
+        self.assertCommandOutput(
+            "go north",
+            [E("[ elsewhere ]"),
+             E("( south )"),
+             ""],
+            ["Test Player leaves north."])
+
+
+    def test_allTiedUp(self):
+        """
+        If you're tied to a chair, you can't leave.
+        """
+        chairThing = Thing(store=self.store, name=u'chair')
+        chairThing.moveTo(self.location)
+        chair = Chair.createFor(chairThing)
+        self.assertCommandOutput("sit chair",
+                                 ["You sit in the chair."],
+                                 ["Test Player sits in the chair."])
+        Tether.createFor(self.player, to=chairThing)
+        self.assertCommandOutput(
+            "stand up",
+            ["You can't move, you're tied to a chair."],
+            ["Test Player struggles."])
+
+
+    def test_tetheredClothing(self):
+        """
+        Clothing that is tethered will also prevent movement if you wear it.
+
+        This isn't just simply a test for clothing; it's an example of
+        integrating with a foreign system which doesn't know about tethering,
+        but can move objects itself.
+
+        Tethering should I{not} have any custom logic related to clothing to
+        make this test pass; if it does get custom clothing code for some
+        reason, more tests should be added to deal with other systems that do
+        not take tethering into account (and vice versa).
+        """
+        Garment.createFor(self.ball, garmentDescription=u"A lovely ball.",
+                          garmentSlots=[u"head"])
+        self.assertCommandOutput(
+            "wear ball",
+            ["You put on the ball."],
+            ["Test Player puts on a ball."])
+        self.assertCommandOutput(
+            "go north",
+            ["You can't move, you're still holding a ball."],
+            ["Test Player struggles with a ball."])
+
+

=== added file 'Imaginary/ExampleGame/examplegame/tether.py'
--- Imaginary/ExampleGame/examplegame/tether.py	1970-01-01 00:00:00 +0000
+++ Imaginary/ExampleGame/examplegame/tether.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,121 @@
+# -*- test-case-name: examplegame.test.test_tether -*-
+
+"""
+A simplistic implementation of tethering, which demonstrates how to prevent
+someone from moving around.
+
+This implementation is somewhat limited, as it assumes that tethered objects
+can only be located in players' inventories and on the ground.  It also makes
+several assumptions about who is actually doing the moving in moveTo; in order
+to be really correct, the implementation of movement needs to relay more
+information about what is moving and how.
+"""
+
+from zope.interface import implements
+
+from axiom.item import Item
+from axiom.attributes import reference
+
+from imaginary.iimaginary import IMovementRestriction, IActor
+from imaginary.eimaginary import ActionFailure
+from imaginary.events import ThatDoesntWork
+from imaginary.enhancement import Enhancement
+from imaginary.objects import Thing
+
+
+class Tether(Item, Enhancement):
+    """
+    I am a force that binds two objects together.
+
+    Right now this force isn't symmetric; the idea is that the thing that we
+    are tethered 'to' is immovable for some other reason.  This is why we're in
+    the example rather than a real robust piece of game-library functionality
+    in imaginary proper.
+
+    The C{thing} that we are installed on is prevented from moving more than a
+    certain distance away from the thing it is tethered C{to}.
+
+    This is accomplished by preventing movement of the object's container;
+    i.e. if you pick up a ball that is tied to the ground, you can't move until
+    you drop it.
+    """
+
+    thing = reference(reftype=Thing,
+                      whenDeleted=reference.CASCADE,
+                      allowNone=False)
+
+    # XXX 'thing' and 'to' should be treated more consistently, or at least the
+    # differences between them explained officially.
+    to = reference(reftype=Thing,
+                   whenDeleted=reference.CASCADE,
+                   allowNone=False)
+
+    implements(IMovementRestriction)
+
+    powerupInterfaces = [IMovementRestriction]
+
+    def movementImminent(self, movee, destination):
+        """
+        The object which is tethered is trying to move somewhere.  If it has an
+        IActor, assume that it's a player trying to move on its own, and emit
+        an appropriate message.
+
+        Otherwise, assume that it is moving *to* an actor, and install a
+        L{MovementBlocker} on that actor.
+        """
+        # There isn't enough information provided to moveTo just yet; we need
+        # to know who is doing the moving.  In the meanwhile, if you have an
+        # actor, we'll assume you're a player.
+        if IActor(movee, None) is not None:
+            raise ActionFailure(
+                ThatDoesntWork(
+                    actor=self.thing,
+                    actorMessage=[u"You can't move, you're tied to ",
+                                  self.to,
+                                  "."],
+                    otherMessage=[self.thing, u' struggles.']))
+        MovementBlocker.destroyFor(self.thing.location)
+        if self.to != destination:
+            MovementBlocker.createFor(destination, tether=self)
+
+        return False
+
+
+class MovementBlocker(Item, Enhancement):
+    """
+    A L{MovementBlocker} is an L{Enhancement} which prevents the movement of a
+    player holding a tethered object.
+    """
+    implements(IMovementRestriction)
+
+    powerupInterfaces = [IMovementRestriction]
+
+    thing = reference(
+        doc="""
+        The L{Thing} whose movement is blocked.
+        """, reftype=Thing, allowNone=False,
+        whenDeleted=reference.CASCADE)
+
+    tether = reference(
+        doc="""
+        The L{Tether} ultimely responsible for blocking movement.
+        """,
+        reftype=Tether, allowNone=False,
+        whenDeleted=reference.CASCADE)
+
+
+    def movementImminent(self, movee, destination):
+        """
+        The player this blocker is installed on is trying to move.  Assume that
+        they are trying to move themselves (via a 'go' action) and prevent it
+        by raising an L{ActionFailure} with an appropriate error message for
+        the player.
+        """
+        raise ActionFailure(
+            ThatDoesntWork(
+                actor=self.thing,
+                actorMessage=
+                [u"You can't move, you're still holding ",
+                 self.tether.thing,u'.'],
+                otherMessage=
+                [self.thing, u' struggles with ', self.tether.thing,u'.']))

=== added file 'Imaginary/TODO.txt'
--- Imaginary/TODO.txt	1970-01-01 00:00:00 +0000
+++ Imaginary/TODO.txt	2011-08-16 01:57:22 +0000
@@ -0,0 +1,258 @@
+
+(This list of tasks is for the #2824 branch, it shouldn't be merged to trunk.)
+
+self-review:
+
+    (Since this is already a large branch, I am going to clean up the obvious
+    stuff before putting it into review for someone else)
+
+    * test coverage:
+
+        * there need to be direct tests for imaginary.idea.  This is obviously
+          the biggest issue.
+
+        * lifecycle testing (testing that if these are not identical on
+          subsequent method calls, bad stuff happens):
+            * Thing.idea
+            * Exit.exitIdea
+            * Containment._exitIdea
+            * Containment._entranceIdea
+
+        * direct tests for IContainmentRelationship and
+          ContainmentRelationship.
+
+-------------------------------------------------------------------------------
+
+I think everything below this line is probably for a separate branch, but I
+need to clean it up and file some additional tickets before deleting it.
+
+-------------------------------------------------------------------------------
+
+General high-level structural issues:
+
+    * the "sensory" system could address multiple issues.  It would be
+      worthwhile to have a rudiment of it, if only to remove duplication
+      between "findProviders" and "search" and the thing that computes the list
+      for ExpressSurroundings, so we can have a consistent way to construct
+      that thing.
+
+    * movement restrictions want to raise ActionFailure for pretty error
+      handling, but don't know who the actor is.  This should be dealt with in
+      more than one way:
+
+        * There should be an error-handling path which allows actions to fail
+          with feedback only to the actor.  "You can't do that because..."
+
+        * moveTo should receive more information, specifically the actor who
+          initiated the movement.  There should probably be a TON of extra
+          information, like the number of joules used in support of the
+          movement etc.
+
+        * moveTo should not be raising ActionFailure directly.  There should be
+          a conventional exception type to raise specifically for saying "not
+          movable", and its callers should catch it.
+
+    * Navigators and retrievers need to be reconciled.  Specifically, CanSee
+      and Visibility need to be smashed into the same object somehow.
+
+Some use-cases that should be implemented / tested:
+
+    * container travel:
+
+        * I should *not* be able to get out of a closed container that I'm in.
+
+        * Bugfix, sort of: If I'm inside a closed container, I should be able
+          to see and access the stuff around me.
+
+    * containment fixes
+
+        * the obtain() call used to pass to ExpressSurroundings is wrong; it's
+          asking "what can you, the location, see from here"; whereas it should
+          be asking "what can you, the player, see in this location".  If it
+          were to be really smart / correct, it would be passing an initial
+          path to obtain(), since we know the relationship between these two
+          things.  Dumb implementation of that could simply re-evaluate the
+          path up to that point and ignore all the links in it to validate that
+          it's a valid path.
+
+    * ranged actions
+
+        * 'get coolant rod with claw'
+
+        * 'shoot target'
+
+            * A shooting range.  There are two rooms: one with targets in it,
+              one with the player and their gun.
+
+                * 'look' and 'shoot' should work, although ideally 'go' should
+                  not: it should say something like "that's live fire over
+                  there!"
+
+            * the gun should be implicitly located, since it's the only valid
+              tool.
+
+    * I should not be able to see *or* reach objects that are around corners.
+
+    * I should be able to exit a darkened room, perhaps with some modifiers.
+      Some effects that some games might want, should these be default?:
+
+        * Stumbling around in the dark will occasionally send you in a random
+          direction.
+
+        * You can always exit in the direction of an exit where the target of
+          the exit is itself lit.
+
+        * Definitely some games will want this, some not: You are likely to be
+          eaten by a lurking grue.
+
+    * I shouldn't be able to see an event that transpires in a dark room.
+
+        * I should be able to pick up a shirt in a dark room if I have
+          something that lets only me see in the dark (night-vision goggles?)
+          but others should not be able to see that.
+
+Some restructuring:
+
+    * What paramters should findProviders and search take?  We're starting with
+      'distance' and 'interface'.  Let's enumerate some verbs:
+
+        * take: something you can physically reach without walking
+
+        * drop: something you are holding
+
+        * wear: something you are holding
+
+        * sit: something in the room, something you are *NOT* holding
+
+        * stand: something *which is your location*.  (something which is
+          holding you?)
+
+        * unwear/remove: something you are *wearing*?  something you're holding
+          would be good enough.
+
+        * look: something you can see
+
+            * we need a 'near look' and a 'far look'.  When the user types
+              'look' they only want to see items in their immediate vicinity,
+              but 'look around' or 'look north' or whatever should still allow
+              them to see things that are close enough.
+
+        * shoot: something you can see?  If there's something in a glass box,
+          you should be able to shoot it.
+
+        * eat: something you're holding
+
+        defaults:
+
+            * actor -> "you" (still not sure what this means)
+
+                a thought: the self-link of the Idea starting an obtain()
+                search should apply annotationsForIncoming, but it should not
+                apply annotationsForOutgoing.  Then the actor idea can always
+                apply an annotationsForOutgoing that says "this isn't you any
+                more"?  then you can have a (retriever?  sense?)
+
+            * target -> something you're holding
+
+            * tool -> something you're holding
+
+        None of these verbs really know anything about distance, except
+        possibly "shoot" - which really cares about the distance to the target
+        *in the hit-probability calculation*; i.e. it wants to know about the
+        path during the execution of the action, not during the location of the
+        target.
+
+    * Rather than an ad-hoc construction of a Navigator and Retriever in
+      findProviders() and search(), there should be a (pluggable, eventually:
+      this is how one would implement night-vision goggles) way to get objects
+      representing "sensory" inputs. (although "reachability" is a bit of a
+      stretch for a sense, it does make sense as 'touch'.)  Practically
+      speaking, that means the logic in the "CanSee" retriever ought to be in
+      Visibility somehow.  Options for implementing this:
+
+        * smash the Retriever and the Navigator into one object, and compose
+          them.  Then build each one (Visibility, Reachability, etc) by
+          wrapping around the other.  Named goes around the outside in
+          search().
+
+    * add a convenient API for getCandelas and friends to use.  getCandelas
+      needs to not take lighting into account.  If we have an reification of
+      'sensory' objects, then we can construct a custom one for this query.
+
+    * Make an "_Idealized" (or something) base class which implements an Item
+      with an 'idea' attribute so that we can manage it on L{Exit}, L{Thing}
+      and perhaps something else.
+
+    * The special-cased just-to-self path in Idea.obtain sucks, because it's
+      inconsistent.  There's no 'shouldKeepGoing' call for the link that can
+      prevent it from yielding.  If we move the responsibility for doing
+      lighting back into the navigator (where, really, it belongs: Visibility
+      is supposed to be a navigator, right?) this means the navigator can't
+      prevent you from accessing an actor aspect of yourself.
+
+        Other cases which will use this same system:
+
+            * Restraints.  Let's say there's a chair that you sit in which
+              restrains you.  It needs a proxy which can prevent you from
+              reaching your actor interface.  This is much like darkness,
+              except it's going to want to *not* restrict visual events or
+              'examine'.
+
+            * Suppression / Anti-Magic Field.  Restraints and darkness both
+              prevent a blanket "everything" with a few possible exceptions;
+              you might also want an effect which suspends only specific
+              actions / interfaces.
+
+            * Blindfold.
+
+        * The two systems that all of these seem to touch are 'vision' and
+          'reachability'.  So we need link types that say "you can't see beyond
+          this point" and "you can't reach beyond this point".  That's
+          "Visibility" and "Proximity", as implemented already, except
+          Visibility can't say "don't keep going" for darkness.  It has to have
+          a way to say "you can't see stuff that is immediately on the other
+          side of this link, but if you go through another link that *is*
+          visible, then you can see stuff".  i.e. the case where you can't see
+          yourself, or any of your inventory, but you *can* see another room.
+
+    * (test for) additional L{ILinkContributor} powerups being added, removed
+      (i.e. making sure that the 'idea' in question is the same).
+
+----
+
+Rules for lighting:
+
+If a room is dark, all objects in that room should have the darkness rule
+applied to them, regardless (?) of how they are discovered.
+
+Right now this is enforced entirely by the CanSee retriever and the
+_PossiblyDark link annotation.
+
+However, this is overkill.  The link annotation is not used at any point during
+traversal.  Only the final annotation in any given path is used, and even that
+annotation is discarded if there is an effective "null annotation"; a link to a
+location with no lighting.  The way this is detected is (I think) suspect,
+because it doesn't use the path (path.of can't detect links which ).
+
+So we could implement the same rules by not annotating anything, and applying
+proxies only at the final link, inspecting its location, rather than applying
+an elaborate series of proxies as we go.
+
+Problems with the current implementation of lighting:
+
+    * applyLighting needs to know the interface that's being queried for so
+      that it can return a _DarkLocationProxy.  There's no good way to
+      determine this right now, because the way we know what interface is being
+      asked for has to do with the Retriever, which (in theory?) could be
+      something arbitrary.  We could make 'interface' a required attribute of
+      the navigator.  That seems a bit weird, since one could (theoretically)
+      want to be able to retrieve things by arbitrary sets of rules, but maybe
+      that's not a useful use-case?
+
+    * Limitations: these can be deferred for a different branch since I think
+      they're mostly just a SMOP, but worth thinking about:
+
+        * the proxy you get for a darkened object ought to be pluggable, so
+          that descriptions can change depending on light level.  This could be
+          a useful dramatic tool.
+

=== modified file 'Imaginary/imaginary/action.py'
--- Imaginary/imaginary/action.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/action.py	2011-08-16 01:57:22 +0000
@@ -15,6 +15,8 @@
 from imaginary import (iimaginary, eimaginary, iterutils, events,
                        objects, text as T, language, pyparsing)
 from imaginary.world import ImaginaryWorld
+from imaginary.idea import (
+    CanSee, Proximity, ProviderOf, Named, Traversability)
 
 ## Hacks because pyparsing doesn't have fantastic unicode support
 _quoteRemovingQuotedString = pyparsing.quotedString.copy()
@@ -44,9 +46,12 @@
 
 
     def parse(self, player, line):
-        for cls in self.actions:
+        """
+        Parse an action.
+        """
+        for eachActionType in self.actions:
             try:
-                match = cls.match(player, line)
+                match = eachActionType.match(player, line)
             except pyparsing.ParseException:
                 pass
             else:
@@ -56,85 +61,163 @@
                         if isinstance(v, pyparsing.ParseResults):
                             match[k] = v[0]
 
-                    return cls().runEventTransaction(player, line, match)
+                    return eachActionType().runEventTransaction(player, line, match)
         return defer.fail(eimaginary.NoSuchCommand(line))
 
 
 
 class Action(object):
+    """
+    An L{Action} represents an intention of a player to do something.
+    """
     __metaclass__ = _ActionType
     infrastructure = True
 
+    actorInterface = iimaginary.IActor
 
     def runEventTransaction(self, player, line, match):
         """
-        Take a player, line, and dictionary of parse results and execute the
-        actual Action implementation.
-
-        @param player: A provider of C{self.actorInterface}
+        Take a player, input, and dictionary of parse results, resolve those
+        parse results into implementations of appropriate interfaces in the
+        game world, and execute the actual Action implementation (contained in
+        the 'do' method) in an event transaction.
+
+        This is the top level of action invocation.
+
+        @param player: A L{Thing} representing the actor's body.
+
         @param line: A unicode string containing the original input
+
         @param match: A dictionary containing some parse results to pass
-        through to the C{run} method.
-
-        """
-        events.runEventTransaction(
-            player.store, self.run, player, line, **match)
-
-
+            through to this L{Action}'s C{do} method as keyword arguments.
+
+        @raise eimaginary.AmbiguousArgument: if multiple valid targets are
+            found for an argument.
+        """
+        def thunk():
+            begin = time.time()
+            try:
+                actor = self.actorInterface(player)
+                for (k, v) in match.items():
+                    try:
+                        objs = self.resolve(player, k, v)
+                    except NotImplementedError:
+                        pass
+                    else:
+                        if len(objs) == 1:
+                            match[k] = objs[0]
+                        elif len(objs) == 0:
+                            self.cantFind(player, actor, k, v)
+                        else:
+                            raise eimaginary.AmbiguousArgument(self, k, v, objs)
+                return self.do(actor, line, **match)
+            finally:
+                end = time.time()
+                log.msg(interface=iaxiom.IStatEvent,
+                        stat_actionDuration=end - begin,
+                        stat_actionExecuted=1)
+        events.runEventTransaction(player.store, thunk)
+
+
+    def cantFind(self, player, actor, slot, name):
+        """
+        This hook is invoked when a target cannot be found.
+
+        This will delegate to a method like C{self.cantFind_<slot>(actor,
+        name)} if one exists, to determine the error message to show to the
+        actor.  It will then raise L{eimaginary.ActionFailure} to stop
+        processing of this action.
+
+        @param player: The L{Thing} doing the searching.
+
+        @type player: L{IThing}
+
+        @param actor: The L{IActor} doing the searching.
+
+        @type actor: L{IActor}
+
+        @param slot: The slot in question.
+
+        @type slot: C{str}
+
+        @param name: The name of the object being searched for.
+
+        @type name: C{unicode}
+
+        @raise eimaginary.ActionFailure: always.
+        """
+        func = getattr(self, "cantFind_"+slot, None)
+        if func:
+            msg = func(actor, name)
+        else:
+            msg = "Who's that?"
+        raise eimaginary.ActionFailure(
+            events.ThatDoesntWork(
+                actorMessage=msg,
+                actor=player))
+
+
+    @classmethod
     def match(cls, player, line):
+        """
+        @return: a list of 2-tuples of all the results of parsing the given
+            C{line} using this L{Action} type's pyparsing C{expr} attribute, or
+            None if the expression does not match the given line.
+
+        @param line: a line of user input to be interpreted as an action.
+
+        @see: L{imaginary.pyparsing}
+        """
         return cls.expr.parseString(line)
-    match = classmethod(match)
-
-
-    def run(self, player, line, **kw):
-        begin = time.time()
-        try:
-            return self._reallyRun(player, line, kw)
-        finally:
-            end = time.time()
-            log.msg(
-                interface=iaxiom.IStatEvent,
-                stat_actionDuration=end - begin,
-                stat_actionExecuted=1,
-                )
-
-
-    def _reallyRun(self, player, line, kw):
-        for (k, v) in kw.items():
-            try:
-                objs = self.resolve(player, k, v)
-            except NotImplementedError:
-                pass
-            else:
-                if len(objs) != 1:
-                    raise eimaginary.AmbiguousArgument(self, k, v, objs)
-                else:
-                    kw[k] = objs[0]
-        return self.do(player, line, **kw)
+
+
+    def do(self, player, line, **slots):
+        """
+        Subclasses override this method to actually perform the action.
+
+        This method is performed in an event transaction, by 'run'.
+
+        NB: The suggested implementation strategy for a 'do' method is to do
+        action-specific setup but then delegate the bulk of the actual logic to
+        a method on a target/tool interface.  The 'do' method's job is to
+        select the appropriate methods to invoke.
+
+        @param player: a provider of this L{Action}'s C{actorInterface}.
+
+        @param line: the input string that created this action.
+
+        @param slots: The results of calling C{self.resolve} on each parsing
+        result (described by a setResultsName in C{self.expr}).
+        """
+        raise NotImplementedError("'do' method not implemented")
 
 
     def resolve(self, player, name, value):
-        raise NotImplementedError("Don't know how to resolve %r (%r)" % (name, value))
-
-
-
-class NoTargetAction(Action):
-    """
-    @cvar actorInterface: Interface that the actor must provide.
-    """
-    infrastructure = True
-
-    actorInterface = iimaginary.IActor
-
-    def match(cls, player, line):
-        actor = cls.actorInterface(player, None)
-        if actor is not None:
-            return super(NoTargetAction, cls).match(player, line)
-        return None
-    match = classmethod(match)
-
-    def run(self, player, line, **kw):
-        return super(NoTargetAction, self).run(self.actorInterface(player), line, **kw)
+        """
+        Resolve a given parsed value to a valid action parameter by calling a
+        'resolve_<name>' method on this L{Action} with the given C{player} and
+        C{value}.
+
+        @param player: the L{Thing} attempting to perform this action.
+
+        @type player: L{Thing}
+
+        @param name: the name of the slot being filled.  For example, 'target'.
+
+        @type name: L{str}
+
+        @param value: a string representing the value that was parsed.  For
+            example, if the user typed 'get fish', this would be 'fish'.
+
+        @return: a value which will be passed as the 'name' parameter to this
+            L{Action}'s C{do} method.
+        """
+        resolver = getattr(self, 'resolve_%s' % (name,), None)
+        if resolver is None:
+            raise NotImplementedError(
+                "Don't know how to resolve %r (%r)" % (name, value))
+        return resolver(player, value)
+
 
 
 def targetString(name):
@@ -144,7 +227,7 @@
 
 
 
-class TargetAction(NoTargetAction):
+class TargetAction(Action):
     """
     Subclass L{TargetAction} to implement an action that acts on a target, like
     'take foo' or 'eat foo' where 'foo' is the target.
@@ -160,10 +243,9 @@
     def targetRadius(self, player):
         return 2
 
-    def resolve(self, player, k, v):
-        if k == "target":
-            return list(player.thing.search(self.targetRadius(player), self.targetInterface, v))
-        return super(TargetAction, self).resolve(player, k, v)
+    def resolve_target(self, player, targetName):
+        return _getIt(player, targetName,
+                      self.targetInterface, self.targetRadius(player))
 
 
 
@@ -183,21 +265,29 @@
     def toolRadius(self, player):
         return 2
 
-    def resolve(self, player, k, v):
-        if k == "tool":
-            return list(player.thing.search(
-                    self.toolRadius(player), self.toolInterface, v))
-        return super(ToolAction, self).resolve(player, k, v)
-
-
-
-class LookAround(NoTargetAction):
+    def resolve_tool(self, player, toolName):
+        return _getIt(player, toolName,
+                      self.toolInterface, self.toolRadius(player))
+
+
+
+def _getIt(player, thingName, iface, radius):
+    return list(player.search(radius, iface, thingName))
+
+
+
+class LookAround(Action):
     actionName = "look"
     expr = pyparsing.Literal("look") + pyparsing.StringEnd()
 
     def do(self, player, line):
+        ultimateLocation = player.thing.location
+        while ultimateLocation.location is not None:
+            ultimateLocation = ultimateLocation.location
         for visible in player.thing.findProviders(iimaginary.IVisible, 1):
-            if player.thing.location is visible.thing:
+            # XXX what if my location is furniture?  I want to see '( Foo,
+            # sitting in the Bar )', not '( Bar )'.
+            if visible.isViewOf(ultimateLocation):
                 concept = visible.visualize()
                 break
         else:
@@ -217,7 +307,35 @@
 
     targetInterface = iimaginary.IVisible
 
-    def targetNotAvailable(self, player, exc):
+    def resolve_target(self, player, targetName):
+        """
+        Resolve the target to look at by looking for a named, visible object in
+        a proximity of 3 meters from the player.
+
+        @param player: The player doing the looking.
+
+        @type player: L{IThing}
+
+        @param targetName: The name of the object we are looking for.
+
+        @type targetName: C{unicode}
+
+        @return: A list of visible objects.
+
+        @rtype: C{list} of L{IVisible}
+
+        @raise eimaginary.ActionFailure: with an appropriate message if the
+            target cannot be resolved for an identifiable reason.  See
+            L{imaginary.objects.Thing.obtainOrReportWhyNot} for a description
+            of how such reasons may be identified.
+        """
+        return player.obtainOrReportWhyNot(
+            Proximity(3.0, Named(targetName,
+                                 CanSee(ProviderOf(iimaginary.IVisible)),
+                                 player)))
+
+
+    def cantFind_target(self, player, name):
         return "You don't see that."
 
     def targetRadius(self, player):
@@ -238,7 +356,7 @@
 
 
 
-class Illuminate(NoTargetAction):
+class Illuminate(Action):
     """
     Change the ambient light level at the location of the actor.  Since this is
     an administrative action that directly manipulates the environment, the
@@ -488,7 +606,7 @@
 
 
 
-class Equipment(NoTargetAction):
+class Equipment(Action):
     expr = pyparsing.Literal("equipment")
 
     actorInterface = iimaginary.IClothingWearer
@@ -528,9 +646,9 @@
             pyparsing.White() +
             targetString("tool"))
 
-    def targetNotAvailable(self, player, exc):
+    def cantFind_target(self, player, targetName):
         return "Nothing like that around here."
-    toolNotAvailable = targetNotAvailable
+    cantFind_tool = cantFind_target
 
     def do(self, player, line, target, tool):
         # XXX Make sure target is in tool
@@ -547,7 +665,7 @@
     toolInterface = iimaginary.IThing
     targetInterface = iimaginary.IContainer
 
-    def targetNotAvailable(self, player, exc):
+    def cantFind_target(self, player, targetName):
         return "That doesn't work."
 
     expr = (pyparsing.Literal("put") +
@@ -609,7 +727,7 @@
             pyparsing.White() +
             targetString("target"))
 
-    def targetNotAvailable(self, player, exc):
+    def cantFind_target(self, player, targetName):
         return u"Nothing like that around here."
 
     def targetRadius(self, player):
@@ -641,7 +759,7 @@
             pyparsing.White() +
             targetString("target"))
 
-    def targetNotAvailable(self, player, exc):
+    def cantFind_target(self, player, targetName):
         return "Nothing like that around here."
 
     def targetRadius(self, player):
@@ -678,7 +796,7 @@
 
 
 
-class Dig(NoTargetAction):
+class Dig(Action):
     expr = (pyparsing.Literal("dig") +
             pyparsing.White() +
             DIRECTION_LITERAL +
@@ -707,7 +825,7 @@
 
 
 
-class Bury(NoTargetAction):
+class Bury(Action):
     expr = (pyparsing.Literal("bury") +
             pyparsing.White() +
             DIRECTION_LITERAL)
@@ -738,47 +856,59 @@
 
 
 
-class Go(NoTargetAction):
-    expr = (pyparsing.Optional(pyparsing.Literal("go") + pyparsing.White()) +
-            DIRECTION_LITERAL)
+class Go(Action):
+    expr = (
+        DIRECTION_LITERAL |
+        (pyparsing.Literal("go") + pyparsing.White() +
+         targetString("direction")) |
+        (pyparsing.Literal("enter") + pyparsing.White() +
+         targetString("direction")) |
+        (pyparsing.Literal("exit") + pyparsing.White() +
+         targetString("direction")))
+
+    actorInterface = iimaginary.IThing
+
+    def resolve_direction(self, player, directionName):
+        """
+        Identify a direction by having the player search for L{IExit}
+        providers that they can see and reach.
+        """
+        return player.obtainOrReportWhyNot(
+            Proximity(
+                3.0,
+                Traversability(
+                    Named(directionName,
+                          CanSee(ProviderOf(iimaginary.IExit)), player))))
+
+
+    def cantFind_direction(self, actor, directionName):
+        """
+        Explain to the user that they can't go in a direction that they can't
+        locate.
+        """
+        return u"You can't go that way."
+
 
     def do(self, player, line, direction):
-        try:
-            exit = iimaginary.IContainer(player.thing.location).getExitNamed(direction)
-        except KeyError:
-            raise eimaginary.ActionFailure(events.ThatDoesntWork(
-                actor=player.thing,
-                actorMessage=u"You can't go that way."))
-
-        dest = exit.toLocation
-        location = player.thing.location
+        location = player.location
 
         evt = events.Success(
             location=location,
-            actor=player.thing,
-            otherMessage=(player.thing, " leaves ", direction, "."))
+            actor=player,
+            otherMessage=(player, " leaves ", direction.name, "."))
         evt.broadcast()
 
-        if exit.sibling is not None:
-            arriveDirection = exit.sibling.name
-        else:
-            arriveDirection = object.OPPOSITE_DIRECTIONS[exit.name]
-
         try:
-            player.thing.moveTo(
-                dest,
-                arrivalEventFactory=lambda player: events.MovementArrivalEvent(
-                    thing=player,
-                    origin=None,
-                    direction=arriveDirection))
+            direction.traverse(player)
         except eimaginary.DoesntFit:
             raise eimaginary.ActionFailure(events.ThatDoesntWork(
-                actor=player.thing,
-                actorMessage=language.ExpressString(u"There's no room for you there.")))
+                actor=player,
+                actorMessage=language.ExpressString(
+                        u"There's no room for you there.")))
 
-        # XXX A convention for programmatically invoked actions?
-        # None as the line?
-        LookAround().do(player, "look")
+        # This is subtly incorrect: see http://divmod.org/trac/ticket/2917
+        lookAroundActor = iimaginary.IActor(player)
+        LookAround().do(lookAroundActor, "look")
 
 
 
@@ -789,9 +919,11 @@
 
     targetInterface = iimaginary.IActor
 
-    def targetNotAvailable(self, player, exc):
-        for thing in player.search(self.targetRadius(player), iimaginary.IThing, exc.partValue):
-            return (language.Noun(thing).nounPhrase().plaintext(player), " cannot be restored.")
+    def cantFind_target(self, player, targetName):
+        for thing in player.thing.search(self.targetRadius(player),
+                                         iimaginary.IThing, targetName):
+            return (language.Noun(thing).nounPhrase().plaintext(player),
+                    " cannot be restored.")
         return "Who's that?"
 
     def targetRadius(self, player):
@@ -831,7 +963,6 @@
         return 3
 
     def do(self, player, line, target):
-        toBroadcast = []
         if target is player:
             raise eimaginary.ActionFailure(
                 events.ThatDoesntMakeSense(u"Hit yourself?  Stupid.",
@@ -866,7 +997,7 @@
 
 
 
-class Say(NoTargetAction):
+class Say(Action):
     expr = (((pyparsing.Literal("say") + pyparsing.White()) ^
              pyparsing.Literal("'")) +
             pyparsing.restOfLine.setResultsName("text"))
@@ -877,7 +1008,7 @@
 
 
 
-class Emote(NoTargetAction):
+class Emote(Action):
     expr = (((pyparsing.Literal("emote") + pyparsing.White()) ^
              pyparsing.Literal(":")) +
             pyparsing.restOfLine.setResultsName("text"))
@@ -890,7 +1021,7 @@
 
 
 
-class Actions(NoTargetAction):
+class Actions(Action):
     expr = pyparsing.Literal("actions")
 
     def do(self, player, line):
@@ -903,7 +1034,7 @@
 
 
 
-class Search(NoTargetAction):
+class Search(Action):
     expr = (pyparsing.Literal("search") +
             targetString("name"))
 
@@ -920,7 +1051,7 @@
 
 
 
-class Score(NoTargetAction):
+class Score(Action):
     expr = pyparsing.Literal("score")
 
     scoreFormat = (
@@ -951,7 +1082,7 @@
 
 
 
-class Who(NoTargetAction):
+class Who(Action):
     expr = pyparsing.Literal("who")
 
     def do(self, player, line):
@@ -1003,7 +1134,7 @@
 
 
 
-class Inventory(NoTargetAction):
+class Inventory(Action):
     expr = pyparsing.Literal("inventory")
 
     def do(self, player, line):
@@ -1113,7 +1244,7 @@
 
 
 
-class Help(NoTargetAction):
+class Help(Action):
     """
     A command for looking up help files.
 

=== modified file 'Imaginary/imaginary/creation.py'
--- Imaginary/imaginary/creation.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/creation.py	2011-08-16 01:57:22 +0000
@@ -16,7 +16,7 @@
 from imaginary.iimaginary import IThingType
 from imaginary.eimaginary import ActionFailure, DoesntFit
 
-from imaginary.action import NoTargetAction, insufficientSpace
+from imaginary.action import Action, insufficientSpace
 from imaginary.action import targetString
 
 from imaginary.pyparsing import Literal, White, Optional, restOfLine
@@ -109,7 +109,7 @@
         otherMessage=language.Sentence([player, " creates ", phrase, "."]))
 
 
-class Create(NoTargetAction):
+class Create(Action):
     """
     An action which can create items by looking at the L{IThingType} plugin
     registry.
@@ -163,7 +163,7 @@
 
 
 
-class ListThingTypes(NoTargetAction):
+class ListThingTypes(Action):
     """
     An action which tells the invoker what thing types exist to be created with
     the L{Create} command.

=== modified file 'Imaginary/imaginary/events.py'
--- Imaginary/imaginary/events.py	2009-01-14 05:21:23 +0000
+++ Imaginary/imaginary/events.py	2011-08-16 01:57:22 +0000
@@ -1,10 +1,11 @@
-# -*- test-case-name: imaginary.test -*-
+# -*- test-case-name: imaginary.test.test_actions.TargetActionTests.test_resolveTargetCaseInsensitively -*-
 
 from zope.interface import implements
 
 from twisted.python import context
 
 from imaginary import iimaginary, language, eimaginary
+from imaginary.idea import Proximity, ProviderOf
 
 
 class Event(language.BaseExpress):
@@ -35,8 +36,13 @@
 
 
     def conceptFor(self, observer):
-        # This can't be a dict because then the ordering when actor is target
-        # or target is tool or etc is non-deterministic.
+        """
+        Retrieve the appropriate L{IConcept} provider for a given observer.  If
+        the observer is this L{Event}'s C{actor}, it will return the
+        C{actorMessage} for this event, and so on for the tool and the target.
+        If it doesn't match a L{Thing} known to this event, it will return
+        C{otherMessage}.
+        """
         if observer is self.actor:
             msg = self.actorMessage
         elif observer is self.target:
@@ -65,13 +71,12 @@
             L{Event}'s location when this method, L{Event.reify}, was called.
         """
         L = []
-        for ob in iimaginary.IContainer(self.location).getContents():
-            observer = iimaginary.IEventObserver(ob, None)
-            if observer:
-                sender = observer.prepare(self)
-                if not callable(sender):
-                    raise TypeError("Senders must be callable", sender)
-                L.append(sender)
+        for observer in (self.location.idea.obtain(
+                Proximity(0.5, ProviderOf(iimaginary.IEventObserver)))):
+            sender = observer.prepare(self)
+            if not callable(sender):
+                raise TypeError("Senders must be callable", sender)
+            L.append(sender)
         return lambda: map(apply, L)
 
 
@@ -163,7 +168,7 @@
             raise
     try:
         result = store.transact(runHelper)
-    except eimaginary.ActionFailure, e:
+    except eimaginary.ActionFailure:
         broadcaster.broadcastRevertEvents()
         return None
     else:

=== modified file 'Imaginary/imaginary/garments.py'
--- Imaginary/imaginary/garments.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/garments.py	2011-08-16 01:57:22 +0000
@@ -11,6 +11,9 @@
 from axiom import item, attributes
 
 from imaginary import iimaginary, language, objects
+from imaginary.eimaginary import ActionFailure
+from imaginary.events import ThatDoesntWork
+from imaginary.idea import Link
 from imaginary.creation import createCreator
 from imaginary.enhancement import Enhancement
 
@@ -80,9 +83,17 @@
 
 
 class Garment(item.Item, Enhancement):
+    """
+    An enhancement for a L{Thing} representing its utility as an article of
+    clothing.
+    """
     implements(iimaginary.IClothing,
-               iimaginary.IDescriptionContributor)
-    powerupInterfaces = (iimaginary.IClothing, iimaginary.IDescriptionContributor)
+               iimaginary.IDescriptionContributor,
+               iimaginary.IMovementRestriction)
+
+    powerupInterfaces = (iimaginary.IClothing,
+                         iimaginary.IDescriptionContributor,
+                         iimaginary.IMovementRestriction)
 
     thing = attributes.reference()
 
@@ -113,6 +124,43 @@
         return self.garmentDescription
 
 
+    def nowWornBy(self, wearer):
+        """
+        This garment is now worn by the given wearer.  As this garment is now
+        on top, set its C{wearLevel} to be higher than any other L{Garment}
+        related to the new C{wearer}.
+        """
+        self.wearer = wearer
+        self.wearLevel = wearer.store.query(
+            Garment,
+            Garment.wearer == wearer).getColumn("wearLevel").max(default=0) + 1
+
+
+    def noLongerWorn(self):
+        """
+        This garment is no longer being worn by anyone.
+        """
+        self.wearer = None
+        self.wearLevel = None
+
+
+    def movementImminent(self, movee, destination):
+        """
+        Something is trying to move.  Don't allow it if I'm currently worn.
+        """
+        if self.wearer is not None and movee is self.thing:
+            raise ActionFailure(
+                ThatDoesntWork(
+                    # XXX I don't actually know who is performing the action
+                    # :-(.
+                    actor=self.wearer.thing,
+                    actorMessage=[
+                        "You can't move ",
+                        language.Noun(self.thing).definiteNounPhrase(),
+                        " without removing it first."]))
+
+
+
 def _orderTopClothingByGlobalSlotList(tempClothes):
     """
     This function orders a dict as returned by getGarmentDict in the order that
@@ -154,9 +202,15 @@
     person or mannequin.
     """
 
-    implements(iimaginary.IClothingWearer, iimaginary.IDescriptionContributor)
-    powerupInterfaces = (iimaginary.IClothingWearer, iimaginary.IDescriptionContributor,
-                         iimaginary.ILinkContributor)
+    _interfaces = (iimaginary.IClothingWearer,
+                   iimaginary.IDescriptionContributor,
+                   iimaginary.ILinkContributor,
+                   # iimaginary.ILinkAnnotator,
+                   )
+
+    implements(*_interfaces)
+
+    powerupInterfaces = _interfaces
 
 
     thing = attributes.reference()
@@ -172,27 +226,53 @@
 
 
     def putOn(self, newGarment):
+        """
+        Wear a new L{Garment} on this L{Wearer}, first moving it to this
+        L{Wearer}'s C{thing} if it is not already there.
+
+        @param newGarment: the article of clothing to wear.
+
+        @type newGarment: L{Garment}
+
+        @raise TooBulky: if the bulk of any of the slots occupied by
+            C{newGarment} is greater than the bulk of any other clothing
+            already in that slot.  (For example, if you tried to wear a T-shirt
+            over a heavy coat.)
+        """
         c = self.getGarmentDict()
         for garmentSlot in newGarment.garmentSlots:
             if garmentSlot in c:
-                # We don't want to be able to wear T-shirts over heavy coats;
-                # therefore, heavy coats have a high "bulk"
                 currentTopOfSlot = c[garmentSlot][-1]
                 if currentTopOfSlot.bulk >= newGarment.bulk:
                     raise TooBulky(currentTopOfSlot, newGarment)
 
         newGarment.thing.moveTo(None)
-        newGarment.wearer = self
-        newGarment.wearLevel = self.store.query(Garment, Garment.wearer == self).getColumn("wearLevel").max(default=0) + 1
+        newGarment.nowWornBy(self)
 
 
     def takeOff(self, garment):
+        """
+        Remove a garment which this player is wearing.
+
+        (Note: no error checking is currently performed to see if this garment
+        is actually already worn by this L{Wearer}.)
+
+        @param garment: the article of clothing to remove.
+
+        @type garment: L{Garment}
+
+        @raise InaccessibleGarment: if the garment is obscured by any other
+            clothing, and is therefore not in the top slot for any of the slots
+            it occupies.  For example, if you put on an undershirt, then a
+            turtleneck, you can't remove the undershirt without removing the
+            turtleneck first.
+        """
         gdict = self.getGarmentDict()
         for slot in garment.garmentSlots:
             if gdict[slot][-1] is not garment:
                 raise InaccessibleGarment(self, garment, gdict[slot][-1])
-        garment.thing.moveTo(garment.wearer.thing)
-        garment.wearer = garment.wearLevel = None
+        garment.noLongerWorn()
+        garment.thing.moveTo(self.thing)
 
 
     # IDescriptionContributor
@@ -205,11 +285,11 @@
 
     # ILinkContributor
     def links(self):
-        d = {}
-        for t in self.store.query(objects.Thing, attributes.AND(Garment.thing == objects.Thing.storeID,
-                                                                Garment.wearer == self)):
-            d.setdefault(t.name, []).append(t)
-        return d
+        for garmentThing in self.store.query(objects.Thing,
+                                  attributes.AND(
+                Garment.thing == objects.Thing.storeID,
+                Garment.wearer == self)):
+            yield Link(self.thing.idea, garmentThing.idea)
 
 
 

=== added file 'Imaginary/imaginary/idea.py'
--- Imaginary/imaginary/idea.py	1970-01-01 00:00:00 +0000
+++ Imaginary/imaginary/idea.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,625 @@
+# -*- test-case-name: imaginary -*-
+
+"""
+This module implements a highly abstract graph-traversal system for actions and
+events to locate the objects which can respond to them.  The top-level
+entry-point to this system is L{Idea.obtain}.
+
+It also implements several basic retrievers related to visibility and physical
+reachability.
+"""
+
+from zope.interface import implements
+from epsilon.structlike import record
+
+from imaginary.iimaginary import (
+    INameable, ILitLink, IThing, IObstruction, IElectromagneticMedium,
+    IDistance, IRetriever, IExit)
+
+
+
+class Link(record("source target")):
+    """
+    A L{Link} is a connection between two L{Idea}s in a L{Path}.
+
+    @ivar source: the idea that this L{Link} originated from.
+    @type source: L{Idea}
+
+    @ivar target: the idea that this L{Link} refers to.
+    @type target: L{Idea}
+    """
+
+    def __init__(self, *a, **k):
+        super(Link, self).__init__(*a, **k)
+        self.annotations = []
+
+
+    def annotate(self, annotations):
+        """
+        Annotate this link with a list of annotations.
+        """
+        self.annotations.extend(annotations)
+
+
+    def of(self, interface):
+        """
+        Yield all annotations on this link which provide the given interface.
+        """
+        for annotation in self.annotations:
+            provider = interface(annotation, None)
+            if provider is not None:
+                yield provider
+
+
+
+class Path(record('links')):
+    """
+    A list of L{Link}s.
+    """
+
+    def of(self, interface):
+        """
+        @return: an iterator of providers of interfaces, adapted from each link
+            in this path.
+        """
+        for link in self.links:
+            for annotation in link.of(interface):
+                yield annotation
+
+
+    def eachTargetAs(self, interface):
+        """
+        @return: an iterable of all non-None results of each L{Link.targetAs}
+            method in this L{Path}'s C{links} attribute.
+        """
+        for link in self.links:
+            provider = interface(link.target.delegate, None)
+            if provider is not None:
+                yield provider
+
+
+    def targetAs(self, interface):
+        """
+        Retrieve the target of the last link of this path, its final
+        destination, as a given interface.
+
+        @param interface: the interface to retrieve.
+        @type interface: L{zope.interface.interfaces.IInterface}
+
+        @return: the last link's target, adapted to the given interface, or
+            C{None} if no appropriate adapter or component exists.
+        @rtype: C{interface} or C{NoneType}
+        """
+        return interface(self.links[-1].target.delegate, None)
+
+
+    def isCyclic(self):
+        """
+        Determine if this path is cyclic, to avoid descending down infinite
+        loops.
+
+        @return: a boolean indicating whether this L{Path} is cyclic or not,
+            i.e. whether the L{Idea} its last link points at is the source of
+            any of its links.
+        """
+        if len(self.links) < 2:
+            return False
+        return (self.links[-1].target in (x.source for x in self.links))
+
+
+    def to(self, link):
+        """
+        Create a new path, extending this one by one new link.
+        """
+        return Path(self.links + [link])
+
+
+    def __repr__(self):
+        """
+        @return: an expanded pretty-printed representation of this Path,
+        suitable for debugging.
+        """
+        s = 'Path('
+        for link in self.links:
+            dlgt = link.target.delegate
+            src = link.source.delegate
+            s += "\n\t"
+            s += repr(getattr(src, 'name', src))
+            s += " => "
+            s += repr(getattr(dlgt, 'name', dlgt))
+            s += " "
+            s += repr(link.annotations)
+        s += ')'
+        return s
+
+
+
+class Idea(record("delegate linkers annotators")):
+    """
+    Consider a person's activities with the world around them as having two
+    layers.  One is a physical layer, out in the world, composed of matter and
+    energy. The other is a cognitive layer, internal to the person, composed
+    of ideas about that matter and energy.
+
+    For example, when a person wants to sit in a wooden chair, they must first
+    visually locate the arrangement of wood in question, make the determination
+    of that it is a "chair" based on its properties, and then perform the
+    appropriate actions to sit upon it.
+
+    However, a person may also interact with symbolic abstractions rather than
+    physical objects.  They may read a word, or point at a window on a computer
+    screen.  An L{Idea} is a representation of the common unit that can be
+    referred to in this way.
+
+    Both physical and cognitive layers are present in Imaginary.  The cognitive
+    layer is modeled by L{imaginary.idea}.  The physical layer is modeled by a
+    rudimentary point-of-interest simulation in L{imaginary.objects}.  An
+    L{imaginary.thing.Thing} is a physical object; an L{Idea} is a node in a
+    non-physical graph, related by links that are annotated to describe the
+    nature of the relationship between it and other L{Idea}s.
+
+    L{Idea} is the most abstract unit of simulation.  It does not have any
+    behavior or simulation semantics of its own; it merely ties together
+    different related systems.
+
+    An L{Idea} is composed of a C{delegate}, which is an object that implements
+    simulation-defined interfaces; a list of L{ILinkContributor}s, which
+    produce L{Link}s to other L{Idea}s, an a set of C{ILinkAnnotator}s, which
+    apply annotations (which themselves implement simulation-defined
+    link-annotation interfaces) to those links.
+
+    Each L{imaginary.thing.Thing} has a corresponding L{Idea} to represent it
+    in the simulation.  The physical simulation defines only a few types of
+    links: objects have links to their containers, containers have links to
+    their contents, rooms have links to their exits, exits have links to their
+    destinations.  Any L{imaginary.thing.Thing} can have a powerup applied to
+    it which adds to the list of linkers or annotators for its L{Idea},
+    however, which allows users to create arbitrary objects.
+
+    For example, the target of the "look" action must implement
+    L{imaginary.iimaginary.IVisible}, but need not be a
+    L{iimaginary.objects.Thing}.  A simulation might want to provide a piece of
+    graffiti that you could look at, but would not be a physical object, in the
+    sense that you couldn't pick it up, weigh it, push it, etc.  Such an object
+    could be implemented as a powerup for both
+    L{imaginary.iimaginary.IDescriptionContributor}, which would impart some
+    short flavor text to the room, and L{imaginary.iimaginary.IVisible}, which
+    would be an acceptable target of 'look'.  The
+    L{imaginary.iimaginary.IVisible} implementation could even be an in-memory
+    object, not stored in the database at all; and there could be different
+    implementations for different observers, depending on their level of
+    knowledge about the in-world graffiti.
+
+    @ivar delegate: this object is the object which may be adaptable to a set
+        of interfaces.  This L{Idea} delegates all adaptation to its delegate.
+        In many cases (when referring to a physical object), this will be an
+        L{imaginary.thing.Thing}, but not necessarily.
+
+    @ivar linkers: a L{list} of L{ILinkContributor}s which are used to gather
+        L{Link}s from this L{Idea} during L{Idea.obtain} traversal.
+
+    @ivar annotators: a L{list} of L{ILinkAnnotator}s which are used to annotate
+        L{Link}s gathered from this L{Idea} via the C{linkers} list.
+    """
+
+    def __init__(self, delegate):
+        super(Idea, self).__init__(delegate, [], [])
+
+
+    def _allLinks(self):
+        """
+        Return an iterator of all L{Links} away from this idea.
+        """
+        for linker in self.linkers:
+            for link in linker.links():
+                yield link
+
+
+    def _applyAnnotators(self, linkiter):
+        """
+        Apply my list of annotators to each link in the given iterable.
+        """
+        for link in linkiter:
+            self._annotateOneLink(link)
+            yield link
+
+
+    def _annotateOneLink(self, link):
+        """
+        Apply all L{ILinkAnnotator}s in this L{Idea}'s C{annotators} list.
+        """
+        allAnnotations = []
+        for annotator in self.annotators:
+            # XXX important to test: annotators shouldn't mutate the links.
+            # The annotators show up in a non-deterministic order, so in order
+            # to facilitate a consistent view of the link in annotationsFor(),
+            # all annotations are applied at the end.
+            allAnnotations.extend(annotator.annotationsFor(link, self))
+        link.annotate(allAnnotations)
+
+
+    def obtain(self, retriever):
+        """
+        Traverse the graph of L{Idea}s, starting with C{self}, looking for
+        objects which the given L{IRetriever} can retrieve.
+
+        The graph will be traversed by looking at all the links generated by
+        this L{Idea}'s C{linkers}, only continuing down those links for which
+        the given L{IRetriever}'s C{shouldKeepGoing} returns L{True}.
+
+        @param retriever: an object which will be passed each L{Path} in turn,
+            discovered during traversal of the L{Idea} graph.  If any
+            invocation of L{IRetriever.retrieve} on this parameter should
+            succeed, that will be yielded as a result from this method.
+        @type retriever: L{IRetriever}
+
+        @return: a generator which yields the results of C{retriever.retrieve}
+            which are not L{None}.
+        """
+        return ObtainResult(self, retriever)
+
+
+    def _doObtain(self, retriever, path, reasonsWhyNot):
+        """
+        A generator that implements the logic for obtain()
+        """
+        if path is None:
+            # Special case: we only get a self->self link if we are the
+            # beginning _and_ the end.
+            path = Path([])
+            selfLink = Link(self, self)
+            self._annotateOneLink(selfLink)
+            finalPath = path.to(selfLink)
+        else:
+            finalPath = Path(path.links[:])
+            self._annotateOneLink(finalPath.links[-1])
+
+        result = retriever.retrieve(finalPath)
+        objections = set(retriever.objectionsTo(finalPath, result))
+        reasonsWhyNot |= objections
+        if result is not None:
+            if not objections:
+                yield result
+
+        for link in self._applyAnnotators(self._allLinks()):
+            subpath = path.to(link)
+            if subpath.isCyclic():
+                continue
+            if retriever.shouldKeepGoing(subpath):
+                for obtained in link.target._doObtain(retriever, subpath, reasonsWhyNot):
+                    yield obtained
+
+
+
+class ObtainResult(record("idea retriever")):
+    """
+    The result of L{Idea.obtain}, this provides an iterable of results.
+
+    @ivar reasonsWhyNot: If this iterator has already been exhausted, this will
+        be a C{set} of L{IWhyNot} objects explaining possible reasons why there
+        were no results.  For example, if the room where the player attempted
+        to obtain targets is dark, this may contain an L{IWhyNot} provider.
+        However, until this iterator has been exhausted, it will be C{None}.
+    @type reasonsWhyNot: C{set} of L{IWhyNot}, or C{NoneType}
+
+    @ivar idea: the L{Idea} that L{Idea.obtain} was invoked on.
+    @type idea: L{Idea}
+
+    @ivar retriever: The L{IRetriever} that L{Idea.obtain} was invoked with.
+    @type retriever: L{IRetriever}
+    """
+
+    reasonsWhyNot = None
+
+    def __iter__(self):
+        """
+        A generator which yields each result of the query, then sets
+        C{reasonsWhyNot}.
+        """
+        reasonsWhyNot = set()
+        for result in self.idea._doObtain(self.retriever, None, reasonsWhyNot):
+            yield result
+        self.reasonsWhyNot = reasonsWhyNot
+
+
+
+class DelegatingRetriever(object):
+    """
+    A delegating retriever, so that retrievers can be easily composed.
+
+    See the various methods marked for overriding.
+
+    @ivar retriever: A retriever to delegate most operations to.
+    @type retriever: L{IRetriever}
+    """
+
+    implements(IRetriever)
+
+    def __init__(self, retriever):
+        """
+        Create a delegator with a retriever to delegate to.
+        """
+        self.retriever = retriever
+
+
+    def moreObjectionsTo(self, path, result):
+        """
+        Override in subclasses to yield objections to add to this
+        L{DelegatingRetriever}'s C{retriever}'s C{objectionsTo}.
+
+        By default, offer no additional objections.
+        """
+        return []
+
+
+    def objectionsTo(self, path, result):
+        """
+        Concatenate C{self.moreObjectionsTo} with C{self.moreObjectionsTo}.
+        """
+        for objection in self.retriever.objectionsTo(path, result):
+            yield objection
+        for objection in self.moreObjectionsTo(path, result):
+            yield objection
+
+
+    def shouldStillKeepGoing(self, path):
+        """
+        Override in subclasses to halt traversal via a C{False} return value for
+        C{shouldKeepGoing} if this L{DelegatingRetriever}'s C{retriever}'s
+        C{shouldKeepGoing} returns C{True}.
+
+        By default, return C{True} to keep going.
+        """
+        return True
+
+
+    def shouldKeepGoing(self, path):
+        """
+        If this L{DelegatingRetriever}'s C{retriever}'s C{shouldKeepGoing}
+        returns C{False} for the given path, return C{False} and stop
+        traversing.  Otherwise, delegate to C{shouldStillKeepGoing}.
+        """
+        return (self.retriever.shouldKeepGoing(path) and
+                self.shouldStillKeepGoing(path))
+
+
+    def resultRetrieved(self, path, retrievedResult):
+        """
+        A result was retrieved.  Post-process it if desired.
+
+        Override this in subclasses to modify (non-None) results returned from
+        this L{DelegatingRetriever}'s C{retriever}'s C{retrieve} method.
+
+        By default, simply return the result retrieved.
+        """
+        return retrievedResult
+
+
+    def retrieve(self, path):
+        """
+        Delegate to this L{DelegatingRetriever}'s C{retriever}'s C{retrieve}
+        method, then post-process it with C{resultRetrieved}.
+        """
+        subResult = self.retriever.retrieve(path)
+        if subResult is None:
+            return None
+        return self.resultRetrieved(path, subResult)
+
+
+
+class Proximity(DelegatingRetriever):
+    """
+    L{Proximity} is a retriever which will continue traversing any path which
+    is shorter than its proscribed distance, but not any longer.
+
+    @ivar distance: the distance, in meters, to query for.
+
+    @type distance: L{float}
+    """
+
+    def __init__(self, distance, retriever):
+        DelegatingRetriever.__init__(self, retriever)
+        self.distance = distance
+
+
+    def shouldStillKeepGoing(self, path):
+        """
+        Implement L{IRetriever.shouldKeepGoing} to stop for paths whose sum of
+        L{IDistance} annotations is greater than L{Proximity.distance}.
+        """
+        dist = sum(vector.distance for vector in path.of(IDistance))
+        ok = (self.distance >= dist)
+        return ok
+
+
+
+class Reachable(DelegatingRetriever):
+    """
+    L{Reachable} is a navivator which will object to any path with an
+    L{IObstruction} annotation on it.
+    """
+
+    def moreObjectionsTo(self, path, result):
+        """
+        Yield an objection from each L{IObstruction.whyNot} method annotating
+        the given path.
+        """
+        if result is not None:
+            for obstruction in path.of(IObstruction):
+                yield obstruction.whyNot()
+
+
+
+class Traversability(DelegatingRetriever):
+    """
+    A path is only traversible if it terminates in *one* exit.  Once you've
+    gotten to an exit, you have to stop, because the player needs to go through
+    that exit to get to the next one.
+    """
+
+    def shouldStillKeepGoing(self, path):
+        """
+        Stop at the first exit that you find.
+        """
+        for index, target in enumerate(path.eachTargetAs(IExit)):
+            if index > 0:
+                return False
+        return True
+
+
+
+class Vector(record('distance direction')):
+    """
+    A L{Vector} is a link annotation which remembers a distance and a
+    direction; for example, a link through a 'north' exit between rooms will
+    have a direction of 'north' and a distance specified by that
+    L{imaginary.objects.Exit} (defaulting to 1 meter).
+    """
+
+    implements(IDistance)
+
+
+
+class ProviderOf(record("interface")):
+    """
+    L{ProviderOf} is a retriever which will retrieve the facet which provides
+    its C{interface}, if any exists at the terminus of the path.
+
+    @ivar interface: The interface which defines the type of values returned by
+        the C{retrieve} method.
+    @type interface: L{zope.interface.interfaces.IInterface}
+    """
+
+    implements(IRetriever)
+
+    def retrieve(self, path):
+        """
+        Retrieve the target of the path, as it provides the interface specified
+        by this L{ProviderOf}.
+
+        @return: the target of the path, adapted to this retriever's interface,
+            as defined by L{Path.targetAs}.
+
+        @rtype: L{ProviderOf.interface}
+        """
+        return path.targetAs(self.interface)
+
+
+    def objectionsTo(self, path, result):
+        """
+        Implement L{IRetriever.objectionsTo} to yield no objections.
+        """
+        return []
+
+
+    def shouldKeepGoing(self, path):
+        """
+        Implement L{IRetriever.shouldKeepGoing} to always return C{True}.
+        """
+        return True
+
+
+
+class AlsoKnownAs(record('name')):
+    """
+    L{AlsoKnownAs} is an annotation that indicates that the link it annotates
+    is known as a particular name.
+
+    @ivar name: The name that this L{AlsoKnownAs}'s link's target is also known
+        as.
+    @type name: C{unicode}
+    """
+
+    implements(INameable)
+
+    def knownTo(self, observer, name):
+        """
+        An L{AlsoKnownAs} is known to all observers as its C{name} attribute.
+        """
+        return (self.name == name)
+
+
+
+class Named(DelegatingRetriever):
+    """
+    A retriever which wraps another retriever, but yields only results known to
+    a particular observer by a particular name.
+
+    @ivar name: the name to search for.
+
+    @ivar observer: the observer who should identify the target by the name
+        this L{Named} is searching for.
+    @type observer: L{Thing}
+    """
+
+    def __init__(self, name, retriever, observer):
+        DelegatingRetriever.__init__(self, retriever)
+        self.name = name
+        self.observer = observer
+
+
+    def resultRetrieved(self, path, subResult):
+        """
+        Invoke C{retrieve} on the L{IRetriever} which we wrap, but only return
+        it if the L{INameable} target of the given path is known as this
+        L{Named}'s C{name}.
+        """
+        named = path.targetAs(INameable)
+        allAliases = list(path.links[-1].of(INameable))
+        if named is not None:
+            allAliases += [named]
+        for alias in allAliases:
+            if alias.knownTo(self.observer, self.name):
+                return subResult
+        return None
+
+
+
+class CanSee(DelegatingRetriever):
+    """
+    Wrap a L{ProviderOf}, yielding the results that it would yield, but
+    applying lighting to the ultimate target based on the last L{IThing} the
+    path.
+
+    @ivar retriever: The lowest-level retriever being wrapped.
+
+    @type retriever: L{ProviderOf} (Note: it might be a good idea to add an
+        'interface' attribute to L{IRetriever} so this no longer depends on a
+        more specific type than other L{DelegatingRetriever}s, to make the
+        order of composition more flexible.)
+    """
+
+    def resultRetrieved(self, path, subResult):
+        """
+        Post-process retrieved results by determining if lighting applies to
+        them.
+        """
+        litlinks = list(path.of(ILitLink))
+        if not litlinks:
+            return subResult
+        # XXX what if there aren't any IThings on the path?
+        litThing = list(path.eachTargetAs(IThing))[-1]
+        # you need to be able to look from a light room to a dark room, so only
+        # apply the most "recent" lighting properties.
+        return litlinks[-1].applyLighting(
+            litThing, subResult, self.retriever.interface)
+
+
+    def shouldStillKeepGoing(self, path):
+        """
+        Don't keep going through links that are opaque to the observer.
+        """
+        for opacity in path.of(IElectromagneticMedium):
+            if opacity.isOpaque():
+                return False
+        return True
+
+
+    def moreObjectionsTo(self, path, result):
+        """
+        Object to paths which have L{ILitLink} annotations which are not lit.
+        """
+        for lighting in path.of(ILitLink):
+            if not lighting.isItLit(path, result):
+                tmwn = lighting.whyNotLit()
+                yield tmwn

=== modified file 'Imaginary/imaginary/iimaginary.py'
--- Imaginary/imaginary/iimaginary.py	2009-01-14 05:21:23 +0000
+++ Imaginary/imaginary/iimaginary.py	2011-08-16 01:57:22 +0000
@@ -40,16 +40,18 @@
     A powerup interface which can add more connections between objects in the
     world graph.
 
-    All ILinkContributors which are powered up on a particular Thing will be
-    given a chance to add to the L{IThing.link} method's return value.
+    All L{ILinkContributors} which are powered up on a particular
+    L{imaginary.objects.Thing} will be appended to that
+    L{imaginary.objects.Thing}'s value.
     """
 
     def links():
         """
-        Return a C{dict} mapping names of connections to C{IThings}.
+        @return: an iterable of L{imaginary.idea.Link}s.
         """
 
 
+
 class IDescriptionContributor(Interface):
     """
     A powerup interface which can add text to the description of an object.
@@ -64,6 +66,70 @@
         """
 
 
+class INameable(Interface):
+    """
+    A provider of L{INameable} is an object which can be identified by an
+    imaginary actor by a name.
+    """
+
+    def knownTo(observer, name):
+        """
+        Is this L{INameable} known to the given C{observer} by the given
+        C{name}?
+
+        @param name: the name to test for
+
+        @type name: L{unicode}
+
+        @param observer: the thing which is observing this namable.
+
+        @type observer: L{IThing}
+
+        @rtype: L{bool}
+
+        @return: L{True} if C{name} identifies this L{INameable}, L{False}
+            otherwise.
+        """
+
+
+class ILitLink(Interface):
+    """
+    This interface is an annotation interface for L{imaginary.idea.Link}
+    objects, for indicating that the link can apply lighting.
+    """
+
+    def applyLighting(litThing, eventualTarget, requestedInterface):
+        """
+        Apply a transformation to an object that an
+        L{imaginary.idea.Idea.obtain} is requesting, based on the light level
+        of this link and its surroundings.
+
+        @param litThing: The L{IThing} to apply lighting to.
+
+        @type litThing: L{IThing}
+
+        @param eventualTarget: The eventual, ultimate target of the path in
+            question.
+
+        @type eventualTarget: C{requestedInterface}
+
+        @param requestedInterface: The interface requested by the query that
+            resulted in this path; this is the interface which
+            C{eventualTarget} should implement.
+
+        @type requestedInterface: L{Interface}
+
+        @return: C{eventualTarget}, or, if this L{ILitLink} knows how to deal
+            with lighting specifically for C{requestedInterface}, a modified
+            version thereof which still implements C{requestedInterface}.  If
+            insufficient lighting results in the player being unable to access
+            the desired object at all, C{None} will be returned.
+
+        @rtype: C{NoneType}, or C{requestedInterface}
+        """
+
+
+
 
 class IThing(Interface):
     """
@@ -71,6 +137,12 @@
     """
     location = Attribute("An IThing which contains this IThing")
 
+    proper = Attribute(
+        "A boolean indicating the definiteness of this thing's pronoun.")
+
+    name = Attribute(
+        "A unicode string, the name of this Thing.")
+
 
     def moveTo(where, arrivalEventFactory=None):
         """
@@ -78,7 +150,7 @@
 
         @type where: L{IThing} provider.
         @param where: The new location to be moved to.
-        
+
         @type arrivalEventFactory: A callable which takes a single
         argument, the thing being moved, and returns an event.
         @param arrivalEventFactory: Will be called to produce the
@@ -86,7 +158,6 @@
         thing. If not specified (or None), no event will be broadcast.
         """
 
-
     def findProviders(interface, distance):
         """
         Retrieve all game objects which provide C{interface} within C{distance}.
@@ -95,19 +166,31 @@
         """
 
 
-    def proxiedThing(thing, interface, distance):
-        """
-        Given an L{IThing} provider, return a provider of L{interface} as it is
-        accessible from C{self}.  Any necessary proxies will be applied.
-        """
-
-
-    def knownAs(name):
-        """
-        Return a boolean indicating whether this thing might reasonably be
-        called C{name}.
-
-        @type name: C{unicode}
+
+class IMovementRestriction(Interface):
+    """
+    A L{MovementRestriction} is a powerup that can respond to a L{Thing}'s
+    movement before it occurs, and thereby restrict it.
+
+    Powerups of this type are consulted on L{Thing} before movement is allowed
+    to complete.
+    """
+
+    def movementImminent(movee, destination):
+        """
+        An object is about to move.  Implementations can raise an exception if
+        they wish to to prevent it.
+
+        @param movee: the object that is moving.
+
+        @type movee: L{Thing}
+
+        @param destination: The L{Thing} of the container that C{movee} will be
+            moving to.
+
+        @type destination: L{IThing}
+
+        @raise Exception: if the movement is to be prevented.
         """
 
 
@@ -116,6 +199,7 @@
     hitpoints = Attribute("L{Points} instance representing hit points")
     experience = Attribute("C{int} representing experience")
     level = Attribute("C{int} representing player's level")
+    thing = Attribute("L{IThing} which represents the actor's physical body.")
 
     def send(event):
         """Describe something to the actor.
@@ -224,22 +308,68 @@
 
 
 
+class IExit(Interface):
+    """
+    An interface representing one direction that a player may move in.  While
+    L{IExit} only represents one half of a passageway, it is not necessarily
+    one-way; in most cases, a parallel exit will exist on the other side.
+    (However, it I{may} be one-way; there is no guarantee that you will be able
+    to traverse it backwards, or even indeed that it will take you somewhere at
+    all!)
+    """
+
+    name = Attribute(
+        """
+        The name of this exit.  This must be something adaptable to
+        L{IConcept}, to display to players.
+        """)
+
+    def traverse(thing):
+        """
+        Attempt to move the given L{IThing} through this L{IExit} to the other
+        side.  (Note that this may not necessarily result in actual movement,
+        if the exit does something tricky like disorienting you or hurting
+        you.)
+
+        @param thing: Something which is passing through this exit.
+
+        @type thing: L{IThing}
+        """
+
+
+
+
+class IObstruction(Interface):
+    """
+    An L{IObstruction} is a link annotation indicating that there is a physical
+    obstruction preventing solid objects from reaching between the two ends of
+    the link.  For example, a closed door might annotate its link to its
+    destination with an L{IObstruction}.
+    """
+
+    def whyNot():
+        """
+        @return: a reason why this is obstructed.
+
+        @rtype: L{IWhyNot}
+        """
+
+
+
 class IContainer(Interface):
     """
     An object which can contain other objects.
     """
-    capacity = Attribute("""
-    The maximum weight this container is capable of holding.
-    """)
-
-#     lid = Attribute("""
-#     A reference to an L{IThing} which serves as this containers lid, or
-#     C{None} if there is no lid.
-#     """)
-
-    closed = Attribute("""
-    A boolean indicating whether this container is closed.
-    """)
+    capacity = Attribute(
+        """
+        The maximum weight this container is capable of holding.
+        """)
+
+    closed = Attribute(
+        """
+        A boolean indicating whether this container is closed.
+        """)
+
 
     def add(object):
         """
@@ -331,63 +461,84 @@
 
 
 
-class IProxy(Interface):
-    """
-        | > look
-        | [ Nuclear Reactor Core ]
-        | High-energy particles are wizzing around here at a fantastic rate.  You can
-        | feel the molecules in your body splitting apart as neutrons bombard the
-        | nuclei of their constituent atoms.  In a few moments you will be dead.
-        | There is a radiation suit on the floor.
-        | > take radiation suit
-        | You take the radiation suit.
-        | Your internal organs hurt a lot.
-        | > wear radiation suit
-        | You wear the radiation suit.
-        | You start to feel better.
-
-    That is to say, a factory for objects which take the place of elements in
-    the result of L{IThing.findProviders} for the purpose of altering their
-    behavior in some manner due to a particular property of the path in the
-    game object graph through which the original element would have been found.
-
-    Another example to consider is that of a pair of sunglasses worn by a
-    player: these might power up that player for IProxy so as to be able to
-    proxy IVisible in such a way as to reduce glaring light.
-    """
-    # XXX: Perhaps add 'distance' here, so Fog can be implemented as an
-    # IVisibility proxy which reduces the distance a observer can see.
-    def proxy(iface, facet):
-        """
-        Proxy C{facet} which provides C{iface}.
-
-        @param facet: A candidate for inclusion in the set of objects returned
-        by findProviders.
-
-        @return: Either a provider of C{iface} or C{None}. If C{None} is
-        returned, then the object will not be returned from findProviders.
-        """
-
-
-
-class ILocationProxy(Interface):
-    """
-    Similar to L{IProxy}, except the pathway between the observer and the
-    target is not considered: instead, all targets are wrapped by all
-    ILocationProxy providers on their location.
-    """
-
-    def proxy(iface, facet):
-        """
-        Proxy C{facet} which provides C{iface}.
-
-        @param facet: A candidate B{contained by the location on which this is
-        a powerup} for inclusion in the set of objects returned by
-        findProviders.
-
-        @return: Either a provider of C{iface} or C{None}. If C{None} is
-        returned, then the object will not be returned from findProviders.
-        """
+class ILinkAnnotator(Interface):
+    """
+    An L{ILinkAnnotator} provides annotations for links from one
+    L{imaginary.idea.Idea} to another.
+    """
+
+    def annotationsFor(link, idea):
+        """
+        Produce an iterator of annotations to be applied to a link whose source
+        or target is the L{Idea} that this L{ILinkAnnotator} has been applied
+        to.
+        """
+
+
+
+class ILocationLinkAnnotator(Interface):
+    """
+    L{ILocationLinkAnnotator} is a powerup interface to allow powerups for a
+    L{Thing} to act as L{ILinkAnnotator}s for every L{Thing} contained within
+    it.  This allows area-effect link annotators to be implemented simply,
+    without needing to monitor movement.
+    """
+
+    def annotationsFor(link, idea):
+        """
+        Produce an iterator of annotations to be applied to a link whose source
+        or target is an L{Idea} of a L{Thing} contained in the L{Thing} that
+        this L{ILocationLinkAnnotator} has been applied to.
+        """
+
+
+
+class IRetriever(Interface):
+    """
+    An L{IRetriever} examines a L{Path} and retrieves a desirable object from
+    it to yield from L{Idea.obtain}, if the L{Path} is suitable.
+
+    Every L{IRetriever} has a different definition of suitability; you should
+    examine some of their implementations for more detail.
+    """
+
+    def retrieve(path):
+        """
+        Return the suitable object described by C{path}, or None if the path is
+        unsuitable for this retriever's purposes.
+        """
+
+    def shouldKeepGoing(path):
+        """
+        Inspect a L{Path}. True if it should be searched, False if not.
+        """
+
+
+    def objectionsTo(path, result):
+        """
+        @return: an iterator of IWhyNot, if you object to this result being
+        yielded.
+        """
+
+
+
+class IContainmentRelationship(Interface):
+    """
+    Indicate the containment of one idea within another, via a link.
+
+    This is an annotation interface, used to annotate L{iimaginary.idea.Link}s
+    to specify that the relationship between linked objects is one of
+    containment.  In other words, the presence of an
+    L{IContainmentRelationship} annotation on a L{iimaginary.idea.Link}
+    indicates that the target of that link is contained by the source of that
+    link.
+    """
+
+    containedBy = Attribute(
+        """
+        A reference to the L{IContainer} which contains the target of the link
+        that this L{IContainmentRelationship} annotates.
+        """)
 
 
 
@@ -395,6 +546,7 @@
     """
     A thing which can be seen.
     """
+
     def visualize():
         """
         Return an IConcept which represents the visible aspects of this
@@ -402,6 +554,15 @@
         """
 
 
+    def isViewOf(thing):
+        """
+        Is this L{IVisible} a view of a given L{Thing}?
+
+        @rtype: L{bool}
+        """
+
+
+
 
 class ILightSource(Interface):
     """
@@ -478,6 +639,36 @@
         """)
 
 
+    def nowWornBy(self, wearer):
+        """
+        This article of clothing is now being worn by C{wearer}.
+
+        @param wearer: The wearer of the clothing.
+
+        @type wearer: L{IClothingWearer}
+        """
+
+
+    def noLongerWorn(self):
+        """
+        This article of clothing is no longer being worn.
+        """
+
+
+
+class ISittable(Interface):
+    """
+    Something you can sit on.
+    """
+
+    def seat(sitterThing):
+        """
+        @param sitterThing: The person sitting down on this sittable surface.
+
+        @type sitterThing: L{imaginary.objects.Thing}
+        """
+
+
 
 class IDescriptor(IThingPowerUp):
     """
@@ -494,4 +685,39 @@
         """
 
 
+class IWhyNot(Interface):
+    """
+    This interface is an idea link annotation interface, designed to be applied
+    by L{ILinkAnnotator}s, that indicates a reason why a given path cannot
+    yield a provider.  This is respected by L{imaginary.idea.ProviderOf}.
+    """
+
+    def tellMeWhyNot():
+        """
+        Return something adaptable to L{IConcept}, that explains why this link
+        is unsuitable for producing results.  For example, the string "It's too
+        dark in here."
+        """
+
+
+
+class IDistance(Interface):
+    """
+    A link annotation that provides a distance.
+    """
+
+    distance = Attribute("floating point, distance in meters")
+
+
+
+class IElectromagneticMedium(Interface):
+    """
+    A medium through which electromagnetic radiation may or may not pass; used
+    as a link annotation.
+    """
+
+    def isOpaque():
+        """
+        Will this propagate radiation the visible spectrum?
+        """
 

=== modified file 'Imaginary/imaginary/language.py'
--- Imaginary/imaginary/language.py	2008-05-04 21:35:09 +0000
+++ Imaginary/imaginary/language.py	2011-08-16 01:57:22 +0000
@@ -136,6 +136,17 @@
     Concepts will be ordered by the C{preferredOrder} class attribute.
     Concepts not named in this list will appear last in an unpredictable
     order.
+
+    @ivar name: The name of the thing being described.
+
+    @ivar description: A basic description of the thing being described, the
+        first thing to show up.
+
+    @ivar exits: An iterable of L{IExit}, to be listed as exits in the
+        description.
+
+    @ivar others: An iterable of L{IDescriptionContributor} that will
+        supplement the description.
     """
     implements(iimaginary.IConcept)
 
@@ -167,6 +178,7 @@
             description = (T.fg.green, self.description, u'\n')
 
         descriptionConcepts = []
+
         for pup in self.others:
             descriptionConcepts.append(pup.conceptualize())
 

=== modified file 'Imaginary/imaginary/objects.py'
--- Imaginary/imaginary/objects.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/objects.py	2011-08-16 01:57:22 +0000
@@ -1,4 +1,12 @@
-# -*- test-case-name: imaginary.test.test_objects -*-
+# -*- test-case-name: imaginary.test.test_objects,imaginary.test.test_actions -*-
+
+"""
+This module contains the core, basic objects in Imaginary.
+
+L{imaginary.objects} contains the physical simulation (L{Thing}), objects
+associated with scoring (L{Points}), and the basic actor interface which allows
+the user to perform simple actions (L{Actor}).
+"""
 
 from __future__ import division
 
@@ -9,6 +17,7 @@
 from twisted.python import reflect, components
 
 from epsilon import structlike
+from epsilon.remember import remembered
 
 from axiom import item, attributes
 
@@ -16,17 +25,9 @@
 
 from imaginary.enhancement import Enhancement as _Enhancement
 
-def merge(d1, *dn):
-    """
-    da = {a: [1, 2]}
-    db = {b: [3]}
-    dc = {b: [5], c: [2, 4]}
-    merge(da, db, dc)
-    da == {a: [1, 2], b: [3, 5], c: [2, 4]}
-    """
-    for d in dn:
-        for (k, v) in d.iteritems():
-            d1.setdefault(k, []).extend(v)
+from imaginary.idea import (
+    Idea, Link, Proximity, Reachable, ProviderOf, Named, AlsoKnownAs, CanSee,
+    Vector, DelegatingRetriever)
 
 
 class Points(item.Item):
@@ -66,8 +67,58 @@
         return self.current
 
 
+
 class Thing(item.Item):
-    implements(iimaginary.IThing, iimaginary.IVisible)
+    """
+    A L{Thing} is a physically located object in the game world.
+
+    While a game object in Imaginary is composed of many different Python
+    objects, the L{Thing} is the central that most game objects will share.
+    It's central for several reasons.
+
+    First, a L{Thing} is connected to the point-of-interest simulation that
+    makes up the environment of an Imaginary game.  A L{Thing} has a location,
+    and a L{Container} can list the L{Thing}s located within it, which is how
+    you can see the objects in your surroundings or a container.
+
+    Each L{Thing} has an associated L{Idea}, which provides the graph that can
+    be traversed to find other L{Thing}s to be the target for actions or
+    events.
+
+    A L{Thing} also the object which serves as the persistent nexus of powerups
+    that define behavior.  An L{_Enhancement} is a powerup for a L{Thing}.
+    L{Thing}s can be powered up for a number of different interfaces:
+
+        - L{iimaginary.IMovementRestriction}, for preventing the L{Thing} for
+          moving around,
+
+        - L{iimaginary.ILinkContributor}, which can provide links from the
+          L{Thing}'s L{Idea} to other L{Idea}s,
+
+        - L{iimaginary.ILinkAnnotator}, which can provide annotations on links
+          incoming or outgoing to the L{Thing}'s L{Idea},
+
+        - L{iimaginary.ILocationLinkAnnotator}, which can provide annotations on
+          links to or from any L{Thing}'s L{Idea} which is ultimately located
+          within the powered-up L{Thing}.
+
+        - L{iimaginary.IDescriptionContributor}, which provide components of
+          the L{Thing}'s description when viewed with the L{Look}.
+
+        - and finally, any interface used as a target for an action or event.
+
+    The way this all fits together are as follows: if you wanted to make a
+    shirt, for example, you would make a L{Thing}, give it an appropriate name
+    and description, make a new L{Enhancement} class which implements
+    L{IMovementRestriction} to prevent the shirt from moving around unless it
+    is correctly in the un-worn state, and then power up that L{Enhancement} on
+    the L{Thing}.  This particular example is implemented in
+    L{imaginary.garments}, but almost any game-logic implementation will follow
+    this general pattern.
+    """
+
+    implements(iimaginary.IThing, iimaginary.IVisible, iimaginary.INameable,
+               iimaginary.ILinkAnnotator, iimaginary.ILinkContributor)
 
     weight = attributes.integer(doc="""
     Units of weight of this object.
@@ -106,117 +157,113 @@
 
 
     def links(self):
-        d = {self.name.lower(): [self]}
-        if self.location is not None:
-            merge(d, {self.location.name: [self.location]})
+        """
+        Implement L{ILinkContributor.links()} by offering a link to this
+        L{Thing}'s C{location} (if it has one).
+        """
+        # since my link contribution is to go up (out), put this last, since
+        # containment (i.e. going down (in)) is a powerup.  we want to explore
+        # contained items first.
         for pup in self.powerupsFor(iimaginary.ILinkContributor):
-            merge(d, pup.links())
-        return d
-
-
-    thing = property(lambda self: self)
-
-    _ProviderStackElement = structlike.record('distance stability target proxies')
+            for link in pup.links():
+                # wooo composition
+                yield link
+        if self.location is not None:
+            l = Link(self.idea, self.location.idea)
+            # XXX this incorrectly identifies any container with an object in
+            # it as 'here', since it doesn't distinguish the observer; however,
+            # cycle detection will prevent these links from being considered in
+            # any case I can think of.  However, 'here' is ambiguous in the
+            # case where you are present inside a container, and that should
+            # probably be dealt with.
+            l.annotate([AlsoKnownAs('here')])
+            yield l
+
+
+    def allAnnotators(self):
+        """
+        A generator which yields all L{iimaginary.ILinkAnnotator} providers
+        that should affect this L{Thing}'s L{Idea}.  This includes:
+
+            - all L{iimaginary.ILocationLinkAnnotator} powerups on all
+              L{Thing}s which contain this L{Thing} (the container it's in, the
+              room its container is in, etc)
+
+            - all L{iimaginary.ILinkAnnotator} powerups on this L{Thing}.
+        """
+        loc = self
+        while loc is not None:
+            if loc is not None:
+                for pup in loc.powerupsFor(iimaginary.ILocationLinkAnnotator):
+                    yield pup
+            loc = loc.location
+        for pup in self.powerupsFor(iimaginary.ILinkAnnotator):
+            yield pup
+
+
+    def annotationsFor(self, link, idea):
+        """
+        Implement L{ILinkAnnotator.annotationsFor} to consult each
+        L{ILinkAnnotator} for this L{Thing}, as defined by
+        L{Thing.allAnnotators}, and yield each annotation for the given L{Link}
+        and L{Idea}.
+        """
+        for annotator in self.allAnnotators():
+            for annotation in annotator.annotationsFor(link, idea):
+                yield annotation
+
+
+    @remembered
+    def idea(self):
+        """
+        An L{Idea} which represents this L{Thing}.
+        """
+        idea = Idea(self)
+        idea.linkers.append(self)
+        idea.annotators.append(self)
+        return idea
+
 
     def findProviders(self, interface, distance):
-
-        # Dictionary keyed on Thing instances used to ensure any particular
-        # Thing is yielded at most once.
-        seen = {}
-
-        # Dictionary keyed on Thing instances used to ensure any particular
-        # Thing only has its links inspected at most once.
-        visited = {self: True}
-
-        # Load proxies that are installed directly on this Thing as well as
-        # location proxies on this Thing's location: if self is adaptable to
-        # interface, use them as arguments to _applyProxies and yield a proxied
-        # and adapted facet of self.
-        facet = interface(self, None)
-        initialProxies = list(self.powerupsFor(iimaginary.IProxy))
-        locationProxies = set()
-        if self.location is not None:
-            locationProxies.update(set(self.location.powerupsFor(iimaginary.ILocationProxy)))
-        if facet is not None:
-            seen[self] = True
-            proxiedFacet = self._applyProxies(locationProxies, initialProxies, facet, interface)
-            if proxiedFacet is not None:
-                yield proxiedFacet
-
-        # Toss in for the _ProviderStackElement list/stack.  Ensures ordering
-        # in the descendTo list remains consistent with a breadth-first
-        # traversal of links (there is probably a better way to do this).
-        stabilityHelper = 1
-
-        # Set up a stack of Things to ask for links to visit - start with just
-        # ourself and the proxies we have found already.
-        descendTo = [self._ProviderStackElement(distance, 0, self, initialProxies)]
-
-        while descendTo:
-            element = descendTo.pop()
-            distance, target, proxies = (element.distance, element.target,
-                                         element.proxies)
-            links = target.links().items()
-            links.sort()
-            for (linkName, linkedThings) in links:
-                for linkedThing in linkedThings:
-                    if distance:
-                        if linkedThing not in visited:
-                            # A Thing which was linked and has not yet been
-                            # visited.  Create a new list of proxies from the
-                            # current list and any which it has and push this
-                            # state onto the stack.  Also extend the total list
-                            # of location proxies with any location proxies it
-                            # has.
-                            visited[linkedThing] = True
-                            stabilityHelper += 1
-                            locationProxies.update(set(linkedThing.powerupsFor(iimaginary.ILocationProxy)))
-                            proxies = proxies + list(
-                                linkedThing.powerupsFor(iimaginary.IProxy))
-                            descendTo.append(self._ProviderStackElement(
-                                distance - 1, stabilityHelper,
-                                linkedThing, proxies))
-
-                    # If the linked Thing hasn't been yielded before and is
-                    # adaptable to the desired interface, wrap it in the
-                    # appropriate proxies and yield it.
-                    facet = interface(linkedThing, None)
-                    if facet is not None and linkedThing not in seen:
-                        seen[linkedThing] = True
-                        proxiedFacet = self._applyProxies(locationProxies, proxies, facet, interface)
-                        if proxiedFacet is not None:
-                            yield proxiedFacet
-
-            # Re-order anything we've appended so that we visit it in the right
-            # order.
-            descendTo.sort()
-
-
-    def _applyProxies(self, locationProxies, proxies, obj, interface):
-        # Extremely pathetic algorithm - loop over all location proxies we have
-        # seen and apply any which belong to the location of the target object.
-        # This could do with some serious optimization.
-        for proxy in locationProxies:
-            if iimaginary.IContainer(proxy.thing).contains(obj.thing) or proxy.thing is obj.thing:
-                obj = proxy.proxy(obj, interface)
-                if obj is None:
-                    return None
-
-        # Loop over the other proxies and simply apply them in turn, giving up
-        # as soon as one eliminates the object entirely.
-        for proxy in proxies:
-            obj = proxy.proxy(obj, interface)
-            if obj is None:
-                return None
-
-        return obj
-
-
-    def proxiedThing(self, thing, interface, distance):
-        for prospectiveFacet in self.findProviders(interface, distance):
-            if prospectiveFacet.thing is thing:
-                return prospectiveFacet
-        raise eimaginary.ThingNotFound(thing)
+        """
+        Temporary emulation of the old way of doing things so that I can
+        surgically replace findProviders.
+        """
+        return self.idea.obtain(
+            Proximity(distance, CanSee(ProviderOf(interface))))
+
+
+    def obtainOrReportWhyNot(self, retriever):
+        """
+        Invoke L{Idea.obtain} on C{self.idea} with the given C{retriever}.
+
+        If no results are yielded, then investigate the reasons why no results
+        have been yielded, and raise an exception describing one of them.
+
+        Objections may be registered by:
+
+            - an L{iimaginary.IWhyNot} annotation on any link traversed in the
+              attempt to discover results, or,
+
+            - an L{iimaginary.IWhyNot} yielded by the given C{retriever}'s
+              L{iimaginary.IRetriever.objectionsTo} method.
+
+        @return: a list of objects returned by C{retriever.retrieve}
+
+        @rtype: C{list}
+
+        @raise eimaginary.ActionFailure: if no results are available, and an
+            objection has been registered.
+        """
+        obt = self.idea.obtain(retriever)
+        results = list(obt)
+        if not results:
+            reasons = list(obt.reasonsWhyNot)
+            if reasons:
+                raise eimaginary.ActionFailure(events.ThatDoesntWork(
+                        actor=self,
+                        actorMessage=reasons[0].tellMeWhyNot()))
+        return results
 
 
     def search(self, distance, interface, name):
@@ -224,59 +271,59 @@
         Retrieve game objects answering to the given name which provide the
         given interface and are within the given distance.
 
-        @type distance: C{int}
         @param distance: How many steps to traverse (note: this is wrong, it
-        will become a real distance-y thing with real game-meaning someday).
+            will become a real distance-y thing with real game-meaning
+            someday).
+        @type distance: C{float}
 
         @param interface: The interface which objects within the required range
-        must be adaptable to in order to be returned.
+            must be adaptable to in order to be returned.
 
+        @param name: The name of the stuff.
         @type name: C{str}
-        @param name: The name of the stuff.
 
         @return: An iterable of L{iimaginary.IThing} providers which are found.
         """
-        # TODO - Move this into the action system.  It is about finding things
-        # using strings, which isn't what the action system is all about, but
-        # the action system is where we do that sort of thing now. -exarkun
-        extras = []
-
-        container = iimaginary.IContainer(self.location, None)
-        if container is not None:
-            potentialExit = container.getExitNamed(name, None)
-            if potentialExit is not None:
-                try:
-                    potentialThing = self.proxiedThing(
-                        potentialExit.toLocation, interface, distance)
-                except eimaginary.ThingNotFound:
-                    pass
-                else:
-                    yield potentialThing
-
-        if name == "me" or name == "self":
-            facet = interface(self, None)
-            if facet is not None:
-                extras.append(self)
-
-        if name == "here" and self.location is not None:
-            facet = interface(self.location, None)
-            if facet is not None:
-                extras.append(self.location)
-
-        for res in self.findProviders(interface, distance):
-            if res.thing in extras:
-                yield res
-            elif res.thing.knownAs(name):
-                yield res
+        return self.obtainOrReportWhyNot(
+            Proximity(
+                distance,
+                Reachable(Named(name, CanSee(ProviderOf(interface)), self))))
 
 
     def moveTo(self, where, arrivalEventFactory=None):
         """
-        @see: L{iimaginary.IThing.moveTo}.
+        Implement L{iimaginary.IThing.moveTo} to change the C{location} of this
+        L{Thing} to a new L{Thing}, broadcasting an L{events.DepartureEvent} to
+        note this object's departure from its current C{location}.
+
+        Before moving it, invoke each L{IMovementRestriction} powerup on this
+        L{Thing} to allow them to prevent this movement.
         """
-        if where is self.location:
+        whereContainer = iimaginary.IContainer(where, None)
+        if (whereContainer is
+            iimaginary.IContainer(self.location, None)):
+            # Early out if I'm being moved to the same location that I was
+            # already in.
             return
+        if whereContainer is None:
+            whereThing = None
+        else:
+            whereThing = whereContainer.thing
+        if whereThing is not None and whereThing.location is self:
+            # XXX should be checked against _all_ locations of whereThing, not
+            # just the proximate one.
+
+            # XXX actor= here is wrong, who knows who is moving this thing.
+            raise eimaginary.ActionFailure(events.ThatDoesntWork(
+                    actor=self,
+                    actorMessage=[
+                        language.Noun(where.thing).definiteNounPhrase()
+                        .capitalizeConcept(),
+                        " won't fit inside itself."]))
+
         oldLocation = self.location
+        for restriction in self.powerupsFor(iimaginary.IMovementRestriction):
+            restriction.movementImminent(self, where)
         if oldLocation is not None:
             events.DepartureEvent(oldLocation, self).broadcast()
         if where is not None:
@@ -290,20 +337,33 @@
             iimaginary.IContainer(oldLocation).remove(self)
 
 
-    def knownAs(self, name):
-        """
-        Return C{True} if C{name} might refer to this L{Thing}, C{False} otherwise.
-
-        XXX - See #2604.
-        """
+    def knownTo(self, observer, name):
+        """
+        Implement L{INameable.knownTo} to compare the name to L{Thing.name} as
+        well as few constant values based on the relationship of the observer
+        to this L{Thing}, such as 'me', 'self', and 'here'.
+
+        @param observer: an L{IThing} provider.
+        """
+
+        mine = self.name.lower()
         name = name.lower()
-        mine = self.name.lower()
-        return name == mine or name in mine.split()
+        if name == mine or name in mine.split():
+            return True
+        if observer == self:
+            if name in ('me', 'self'):
+                return True
+        return False
 
 
     # IVisible
     def visualize(self):
-        container = iimaginary.IContainer(self.thing, None)
+        """
+        Implement L{IVisible.visualize} to return a
+        L{language.DescriptionConcept} that describes this L{Thing}, including
+        all its L{iimaginary.IDescriptionContributor} powerups.
+        """
+        container = iimaginary.IContainer(self, None)
         if container is not None:
             exits = list(container.getExits())
         else:
@@ -313,8 +373,41 @@
             self.name,
             self.description,
             exits,
+            # Maybe we should listify this or something; see
+            # http://divmod.org/trac/ticket/2905
             self.powerupsFor(iimaginary.IDescriptionContributor))
-components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(), Thing, iimaginary.IConcept)
+
+
+    def isViewOf(self, thing):
+        """
+        Implement L{IVisible.isViewOf} to return C{True} if its argument is
+        C{self}.  In other words, this L{Thing} is only a view of itself.
+        """
+        return (thing is self)
+
+components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(),
+                           Thing,
+                           iimaginary.IConcept)
+
+
+def _eventuallyContains(containerThing, containeeThing):
+    """
+    Does a container, or any containers within it (or any containers within any
+    of those, etc etc) contain some object?
+
+    @param containeeThing: The L{Thing} which may be contained.
+
+    @param containerThing: The L{Thing} which may have a L{Container} that
+    contains C{containeeThing}.
+
+    @return: L{True} if the containee is contained by the container.
+    """
+    while containeeThing is not None:
+        if containeeThing is containerThing:
+            return True
+        containeeThing = containeeThing.location
+    return False
+
 
 
 
@@ -323,18 +416,41 @@
     u"west": u"east",
     u"northwest": u"southeast",
     u"northeast": u"southwest"}
-for (k, v) in OPPOSITE_DIRECTIONS.items():
-    OPPOSITE_DIRECTIONS[v] = k
+
+
+def _populateOpposite():
+    """
+    Populate L{OPPOSITE_DIRECTIONS} with inverse directions.
+
+    (Without leaking any loop locals into the global scope, thank you very
+    much.)
+    """
+    for (k, v) in OPPOSITE_DIRECTIONS.items():
+        OPPOSITE_DIRECTIONS[v] = k
+
+_populateOpposite()
+
 
 
 class Exit(item.Item):
-    fromLocation = attributes.reference(doc="""
-    Where this exit leads from.
-    """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing)
-
-    toLocation = attributes.reference(doc="""
-    Where this exit leads to.
-    """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing)
+    """
+    An L{Exit} is an oriented pathway between two L{Thing}s which each
+    represent a room.
+    """
+
+    implements(iimaginary.INameable, iimaginary.IExit)
+
+    fromLocation = attributes.reference(
+        doc="""
+        Where this exit leads from.
+        """, allowNone=False,
+        whenDeleted=attributes.reference.CASCADE, reftype=Thing)
+
+    toLocation = attributes.reference(
+        doc="""
+        Where this exit leads to.
+        """, allowNone=False,
+        whenDeleted=attributes.reference.CASCADE, reftype=Thing)
 
     name = attributes.text(doc="""
     What this exit is called/which direction it is in.
@@ -344,15 +460,69 @@
     The reverse exit object, if one exists.
     """)
 
-
-    def link(cls, a, b, forwardName, backwardName=None):
+    distance = attributes.ieee754_double(
+        doc="""
+        How far, in meters, does a user have to travel to traverse this exit?
+        """, allowNone=False, default=1.0)
+
+    def knownTo(self, observer, name):
+        """
+        Implement L{iimaginary.INameable.knownTo} to identify this L{Exit} as
+        its C{name} attribute.
+        """
+        return name == self.name
+
+
+    def traverse(self, thing):
+        """
+        Implement L{iimaginary.IExit} to move the given L{Thing} to this
+        L{Exit}'s C{toLocation}.
+        """
+        if self.sibling is not None:
+            arriveDirection = self.sibling.name
+        else:
+            arriveDirection = OPPOSITE_DIRECTIONS.get(self.name)
+
+        thing.moveTo(
+            self.toLocation,
+            arrivalEventFactory=lambda player: events.MovementArrivalEvent(
+                thing=thing,
+                origin=None,
+                direction=arriveDirection))
+
+
+    @classmethod
+    def link(cls, a, b, forwardName, backwardName=None, distance=1.0):
+        """
+        Create two L{Exit}s connecting two rooms.
+
+        @param a: The first room.
+
+        @type a: L{Thing}
+
+        @param b: The second room.
+
+        @type b: L{Thing}
+
+        @param forwardName: The name of the link going from C{a} to C{b}.  For
+            example, u'east'.
+
+        @type forwardName: L{unicode}
+
+        @param backwardName: the name of the link going from C{b} to C{a}.  For
+            example, u'west'.  If not provided or L{None}, this will be
+            computed based on L{OPPOSITE_DIRECTIONS}.
+
+        @type backwardName: L{unicode}
+        """
         if backwardName is None:
             backwardName = OPPOSITE_DIRECTIONS[forwardName]
-        me = cls(store=a.store, fromLocation=a, toLocation=b, name=forwardName)
-        him = cls(store=b.store, fromLocation=b, toLocation=a, name=backwardName)
-        me.sibling = him
-        him.sibling = me
-    link = classmethod(link)
+        forward = cls(store=a.store, fromLocation=a, toLocation=b,
+                 name=forwardName, distance=distance)
+        backward = cls(store=b.store, fromLocation=b, toLocation=a,
+                  name=backwardName, distance=distance)
+        forward.sibling = backward
+        backward.sibling = forward
 
 
     def destroy(self):
@@ -361,29 +531,79 @@
         self.deleteFromStore()
 
 
-    # NOTHING
+    @remembered
+    def exitIdea(self):
+        """
+        This property is the L{Idea} representing this L{Exit}; this is a
+        fairly simple L{Idea} that will link only to the L{Exit.toLocation}
+        pointed to by this L{Exit}, with a distance annotation indicating the
+        distance traversed to go through this L{Exit}.
+        """
+        x = Idea(self)
+        x.linkers.append(self)
+        return x
+
+
+    def links(self):
+        """
+        Generate a link to the location that this exit points at.
+
+        @return: an iterator which yields a single L{Link}, annotated with a
+            L{Vector} that indicates a distance of 1.0 (a temporary measure,
+            since L{Exit}s don't have distances yet) and a direction of this
+            exit's C{name}.
+        """
+        l = Link(self.exitIdea, self.toLocation.idea)
+        l.annotate([Vector(self.distance, self.name),
+                    # We annotate this link with ourselves because the 'Named'
+                    # retriever will use the last link in the path to determine
+                    # if an object has any aliases.  We want this direction
+                    # name to be an alias for the room itself as well as the
+                    # exit, so we want to annotate the link with an INameable.
+                    # This also has an effect of annotating the link with an
+                    # IExit, and possibly one day an IItem as well (if such a
+                    # thing ever comes to exist), so perhaps we eventually want
+                    # a wrapper which elides all references here except
+                    # INameable since that's what we want.  proxyForInterface
+                    # perhaps?  However, for the moment, the extra annotations
+                    # do no harm, so we'll leave them there.
+                    self])
+        yield l
+
+
     def conceptualize(self):
-        return language.ExpressList([u'the exit to ', language.Noun(self.toLocation).nounPhrase()])
-components.registerAdapter(lambda exit: exit.conceptualize(), Exit, iimaginary.IConcept)
+        return language.ExpressList(
+            [u'the exit to ', language.Noun(self.toLocation).nounPhrase()])
+
+components.registerAdapter(lambda exit: exit.conceptualize(),
+                           Exit, iimaginary.IConcept)
+
+
+
+class ContainmentRelationship(structlike.record("containedBy")):
+    """
+    Implementation of L{iimaginary.IContainmentRelationship}.  The interface
+    specifies no methods or attributes.  See its documentation for more
+    information.
+    """
+    implements(iimaginary.IContainmentRelationship)
 
 
 
 class Containment(object):
-    """Functionality for containment to be used as a mixin in Powerups.
+    """
+    Functionality for containment to be used as a mixin in Powerups.
     """
 
     implements(iimaginary.IContainer, iimaginary.IDescriptionContributor,
                iimaginary.ILinkContributor)
-    powerupInterfaces = (iimaginary.IContainer, iimaginary.ILinkContributor,
+    powerupInterfaces = (iimaginary.IContainer,
+                         iimaginary.ILinkContributor,
                          iimaginary.IDescriptionContributor)
 
     # Units of weight which can be contained
     capacity = None
 
-    # Reference to another object which serves as this container's lid.
-    # If None, this container cannot be opened or closed.
-    # lid = None
-
     # Boolean indicating whether the container is currently closed or open.
     closed = False
 
@@ -443,18 +663,164 @@
 
     # ILinkContributor
     def links(self):
-        d = {}
+        """
+        Implement L{ILinkContributor} to contribute L{Link}s to all contents of
+        this container, as well as all of its exits, and its entrance from its
+        location.
+        """
         if not self.closed:
             for ob in self.getContents():
-                merge(d, ob.links())
+                content = Link(self.thing.idea, ob.idea)
+                content.annotate([ContainmentRelationship(self)])
+                yield content
+        yield Link(self.thing.idea, self._entranceIdea)
+        yield Link(self.thing.idea, self._exitIdea)
         for exit in self.getExits():
-            merge(d, {exit.name: [exit.toLocation]})
-        return d
+            yield Link(self.thing.idea, exit.exitIdea)
+
+
+    @remembered
+    def _entranceIdea(self):
+        """
+        Return an L{Idea} that reflects the implicit entrance from this
+        container's location to the interior of the container.
+        """
+        return Idea(delegate=_ContainerEntrance(self))
+
+
+    @remembered
+    def _exitIdea(self):
+        """
+        Return an L{Idea} that reflects the implicit exit from this container
+        to its location.
+        """
+        return Idea(delegate=_ContainerExit(self))
 
 
     # IDescriptionContributor
     def conceptualize(self):
-        return ExpressSurroundings(self.getContents())
+        """
+        Implement L{IDescriptionContributor} to enumerate the contents of this
+        containment.
+
+        @return: an L{ExpressSurroundings} with an iterable of all visible
+        contents of this container.
+        """
+        return ExpressSurroundings(
+            self.thing.idea.obtain(
+                _ContainedBy(CanSee(ProviderOf(iimaginary.IThing)), self)))
+
+
+
+class _ContainedBy(DelegatingRetriever):
+    """
+    An L{iimaginary.IRetriever} which discovers only things present in a given
+    container.  Currently used only for discovering the list of things to list
+    in a container's description.
+
+    @ivar retriever: a retriever to delegate to.
+
+    @type retriever: L{iimaginary.IRetriever}
+
+    @ivar container: the container to test containment by
+
+    @type container: L{IThing}
+    """
+
+    implements(iimaginary.IRetriever)
+
+    def __init__(self, retriever, container):
+        DelegatingRetriever.__init__(self, retriever)
+        self.container = container
+
+
+    def resultRetrieved(self, path, result):
+        """
+        If this L{_ContainedBy}'s container contains the last L{IThing} target
+        of the given path, return the result of this L{_ContainedBy}'s
+        retriever retrieving from the given C{path}, otherwise C{None}.
+        """
+        containments = list(path.of(iimaginary.IContainmentRelationship))
+        if containments:
+            if containments[-1].containedBy is self.container:
+                return result
+
+
+
+class _ContainerEntrance(structlike.record('container')):
+    """
+    A L{_ContainerEntrance} is the implicit entrance to a container from its
+    location.  If a container is open, and big enough, it can be entered.
+
+    @ivar container: the container that this L{_ContainerEntrance} points to.
+
+    @type container: L{Containment}
+    """
+
+    implements(iimaginary.IExit, iimaginary.INameable)
+
+    @property
+    def name(self):
+        """
+        Implement L{iimaginary.IExit.name} to return a descriptive name for the
+        inward exit of this specific container.
+        """
+        return 'into ', language.Noun(self.container.thing).definiteNounPhrase()
+
+
+    def traverse(self, thing):
+        """
+        Implement L{iimaginary.IExit.traverse} to move the thing in transit to
+        the container specified.
+        """
+        thing.moveTo(self.container)
+
+
+    def knownTo(self, observer, name):
+        """
+        Delegate L{iimaginary.INameable.knownTo} to this
+        L{_ContainerEntrance}'s container's thing.
+        """
+        return self.container.thing.knownTo(observer, name)
+
+
+
+class _ContainerExit(structlike.record('container')):
+    """
+    A L{_ContainerExit} is the exit from a container, or specifically, a
+    L{Containment}; an exit by which actors may move to the container's
+    container.
+
+    @ivar container: the container that this L{_ContainerExit} points out from.
+
+    @type container: L{Containment}
+    """
+
+    implements(iimaginary.IExit, iimaginary.INameable)
+
+    @property
+    def name(self):
+        """
+        Implement L{iimaginary.IExit.name} to return a descriptive name for the
+        outward exit of this specific container.
+        """
+        return 'out of ', language.Noun(self.container.thing).definiteNounPhrase()
+
+
+    def traverse(self, thing):
+        """
+        Implement L{iimaginary.IExit.traverse} to move the thing in transit to
+        the container specified.
+        """
+        thing.moveTo(self.container.thing.location)
+
+
+    def knownTo(self, observer, name):
+        """
+        This L{_ContainerExit} is known to observers inside it as 'out'
+        (i.e. 'go out', 'look out'), but otherwise it has no known description.
+        """
+        return (observer.location == self.container.thing) and (name == 'out')
 
 
 
@@ -467,19 +833,24 @@
 
 
 class Container(item.Item, Containment, _Enhancement):
-    """A generic powerup that implements containment."""
-
-    capacity = attributes.integer(doc="""
-    Units of weight which can be contained.
-    """, allowNone=False, default=1)
-
-    closed = attributes.boolean(doc="""
-    Indicates whether the container is currently closed or open.
-    """, allowNone=False, default=False)
-
-    thing = attributes.reference(doc="""
-    The object this container powers up.
-    """)
+    """
+    A generic L{_Enhancement} that implements containment.
+    """
+
+    capacity = attributes.integer(
+        doc="""
+        Units of weight which can be contained.
+        """, allowNone=False, default=1)
+
+    closed = attributes.boolean(
+        doc="""
+        Indicates whether the container is currently closed or open.
+        """, allowNone=False, default=False)
+
+    thing = attributes.reference(
+        doc="""
+        The object this container powers up.
+        """)
 
 
 
@@ -488,14 +859,17 @@
 
     def vt102(self, observer):
         return [
-            [T.bold, T.fg.yellow, language.Noun(self.original.thing).shortName().plaintext(observer)],
+            [T.bold, T.fg.yellow, language.Noun(
+                    self.original.thing).shortName().plaintext(observer)],
             u" is ",
             [T.bold, T.fg.red, self.original._condition(), u"."]]
 
 
 class Actable(object):
     implements(iimaginary.IActor, iimaginary.IEventObserver)
-    powerupInterfaces = (iimaginary.IActor, iimaginary.IEventObserver, iimaginary.IDescriptionContributor)
+
+    powerupInterfaces = (iimaginary.IActor, iimaginary.IEventObserver,
+                         iimaginary.IDescriptionContributor)
 
     # Yay, experience!
     experience = 0
@@ -514,8 +888,6 @@
         'great')
 
 
-
-
     # IDescriptionContributor
     def conceptualize(self):
         return ExpressCondition(self)
@@ -651,62 +1023,182 @@
 
 
 class LocationLighting(item.Item, _Enhancement):
-    implements(iimaginary.ILocationProxy)
-    powerupInterfaces = (iimaginary.ILocationProxy,)
-
-    candelas = attributes.integer(doc="""
-    The luminous intensity in candelas.
-
-    See U{http://en.wikipedia.org/wiki/Candela}.
-    """, default=100, allowNone=False)
-
-    thing = attributes.reference()
-
+    """
+    A L{LocationLighting} is an enhancement for a location which allows the
+    location's description and behavior to depend on its lighting.  While
+    L{LocationLighting} includes its own ambient lighting number, it is not
+    really a light source, it's just a location which is I{affected by} light
+    sources; for that, you should use L{LightSource}.
+
+    By default, in Imaginary, rooms are considered by to be lit to an
+    acceptable level that actors can see and interact with both the room and
+    everything in it without worrying about light.  By contrast, any room that
+    can be dark needs to have a L{LocationLighting} installed.  A room affected
+    by a L{LocationLighting} which is lit will behave like a normal room, but a
+    room affected by a L{LocationLighting} with no available light sources will
+    prevent players from performing actions which require targets that need to
+    be seen, and seeing the room's description.
+    """
+
+    implements(iimaginary.ILocationLinkAnnotator)
+    powerupInterfaces = (iimaginary.ILocationLinkAnnotator,)
+
+    candelas = attributes.integer(
+        doc="""
+        The ambient luminous intensity in candelas.
+
+        See U{http://en.wikipedia.org/wiki/Candela}.
+        """, default=100, allowNone=False)
+
+    thing = attributes.reference(
+        doc="""
+        The location being affected by lighting.
+        """,
+        reftype=Thing,
+        allowNone=False,
+        whenDeleted=attributes.reference.CASCADE)
 
     def getCandelas(self):
         """
         Sum the candelas of all light sources within a limited distance from
         the location this is installed on and return the result.
         """
-        sum = 0
-        for candle in self.thing.findProviders(iimaginary.ILightSource, 1):
+        sum = self.candelas
+        for candle in self.thing.idea.obtain(
+            Proximity(1, ProviderOf(iimaginary.ILightSource))):
             sum += candle.candelas
         return sum
 
 
-    def proxy(self, facet, interface):
-        if interface is iimaginary.IVisible:
-            if self.getCandelas():
-                return facet
-            elif facet.thing is self.thing:
-                return _DarkLocationProxy(self.thing)
-            else:
-                return None
-        return facet
+    def annotationsFor(self, link, idea):
+        """
+        Yield a L{_PossiblyDark} annotation for all links pointing to objects
+        located in the C{thing} attribute of this L{LocationLighting}.
+        """
+        if link.target is idea:
+            yield _PossiblyDark(self)
 
 
 
 class _DarkLocationProxy(structlike.record('thing')):
+    """
+    An L{IVisible} implementation for darkened locations.
+    """
+
     implements(iimaginary.IVisible)
 
     def visualize(self):
+        """
+        Return a L{DescriptionConcept} that tells the player they can't see.
+        """
         return language.DescriptionConcept(
             u"Blackness",
             u"You cannot see anything because it is very dark.")
 
 
+    def isViewOf(self, thing):
+        """
+        Implement L{IVisible.isViewOf} to delegate to this
+        L{_DarkLocationProxy}'s L{Thing}'s L{IVisible.isViewOf}.
+
+        In other words, this L{_DarkLocationProxy} C{isViewOf} its C{thing}.
+        """
+        return self.thing.isViewOf(thing)
+
+
 
 class LightSource(item.Item, _Enhancement):
+    """
+    A simple implementation of L{ILightSource} which provides a fixed number of
+    candelas of luminous intensity, assumed to be emitted uniformly in all
+    directions.
+    """
+
     implements(iimaginary.ILightSource)
     powerupInterfaces = (iimaginary.ILightSource,)
 
-    candelas = attributes.integer(doc="""
-    The luminous intensity in candelas.
-
-    See U{http://en.wikipedia.org/wiki/Candela}.
-    """, default=1, allowNone=False)
-
-    thing = attributes.reference()
-
-
-
+    candelas = attributes.integer(
+        doc="""
+        The luminous intensity in candelas.
+
+        See U{http://en.wikipedia.org/wiki/Candela}.
+        """, default=1, allowNone=False)
+
+    thing = attributes.reference(
+        doc="""
+        The physical body emitting the light.
+        """,
+        reftype=Thing,
+        allowNone=False,
+        whenDeleted=attributes.reference.CASCADE)
+
+
+
+class _PossiblyDark(structlike.record("lighting")):
+    """
+    A L{_PossiblyDark} is a link annotation which specifies that the target of
+    the link may be affected by lighting.
+
+    @ivar lighting: the lighting for a particular location.
+
+    @type lighting: L{LocationLighting}
+    """
+
+    implements(iimaginary.IWhyNot, iimaginary.ILitLink)
+
+    def tellMeWhyNot(self):
+        """
+        Return a helpful message explaining why something may not be accessible
+        due to poor lighting.
+        """
+        return "It's too dark to see."
+
+
+    def isItLit(self, path, result):
+        """
+        Determine if the given result, viewed via the given path, appears to be
+        lit.
+
+        @return: L{True} if the result should be lit, L{False} if it is dark.
+
+        @rtype: C{bool}
+        """
+        # XXX wrong, we need to examine this exactly the same way applyLighting
+        # does.  CanSee and Visibility *are* the same object now so it is
+        # possible to do.
+        if self.lighting.getCandelas():
+            return True
+        litThing = list(path.eachTargetAs(iimaginary.IThing))[-1]
+        if _eventuallyContains(self.lighting.thing, litThing):
+            val = litThing is self.lighting.thing
+            #print 'checking if', litThing, 'is lit:', val
+            return val
+        else:
+            return True
+
+
+    def whyNotLit(self):
+        """
+        Return an L{iimaginary.IWhyNot} provider explaining why the target of
+        this link is not lit.  (Return 'self', since L{_PossiblyDark} is an
+        L{iimaginary.IWhyNot} provider itself.)
+        """
+        return self
+
+
+    def applyLighting(self, litThing, eventualTarget, requestedInterface):
+        """
+        Implement L{iimaginary.ILitLink.applyLighting} to return a
+        L{_DarkLocationProxy} for the room lit by this
+        L{_PossiblyDark.lighting}, C{None} for any items in that room, or
+        C{eventualTarget} if the target is in a different place.
+        """
+        if self.lighting.getCandelas():
+            return eventualTarget
+        elif (eventualTarget is self.lighting.thing and
+              requestedInterface is iimaginary.IVisible):
+            return _DarkLocationProxy(self.lighting.thing)
+        elif _eventuallyContains(self.lighting.thing, litThing):
+            return None
+        else:
+            return eventualTarget

=== modified file 'Imaginary/imaginary/resources/motd'
--- Imaginary/imaginary/resources/motd	2006-04-12 02:41:46 +0000
+++ Imaginary/imaginary/resources/motd	2011-08-16 01:57:22 +0000
@@ -2,4 +2,4 @@
 TWISTED %(twistedVersion)s
 PYTHON %(pythonVersion)s
 
-Written by Jp Calderone (exarkun@xxxxxxxxxxxxxxxxx)
+Created by IMAGINARY TEAM

=== modified file 'Imaginary/imaginary/test/commandutils.py'
--- Imaginary/imaginary/test/commandutils.py	2009-06-29 12:25:10 +0000
+++ Imaginary/imaginary/test/commandutils.py	2011-08-16 01:57:22 +0000
@@ -26,6 +26,17 @@
     """
     A mixin for TestCase classes which provides support for testing Imaginary
     environments via command-line transcripts.
+
+    @ivar store: the L{store.Store} containing all the relevant game objects.
+
+    @ivar location: The location where the test is taking place.
+
+    @ivar world: The L{ImaginaryWorld} that created the player.
+
+    @ivar player: The L{Thing} representing the main player.
+
+    @ivar observer: The L{Thing} representing the observer who sees the main
+        player's actions.
     """
 
     def setUp(self):

=== modified file 'Imaginary/imaginary/test/test_actions.py'
--- Imaginary/imaginary/test/test_actions.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/test/test_actions.py	2011-08-16 01:57:22 +0000
@@ -500,6 +500,49 @@
             ["Test Player arrives from the west."])
 
 
+    def test_goThroughOneWayExit(self):
+        """
+        Going through a one-way exit with a known direction will announce that
+        the player arrived from that direction; with an unknown direction it
+        will simply announce that they have arrived.
+        """
+        secretRoom = objects.Thing(store=self.store, name=u'Secret Room!')
+        objects.Container.createFor(secretRoom, capacity=1000)
+        myExit = objects.Exit(store=self.store, fromLocation=secretRoom,
+                              toLocation=self.location, name=u'north')
+        self.player.moveTo(secretRoom)
+        self._test(
+            "north",
+            [E("[ Test Location ]"),
+             "Location for testing.",
+             "Observer Player"],
+            ["Test Player arrives from the south."])
+        self.player.moveTo(secretRoom)
+        myExit.name = u'elsewhere'
+        self.assertCommandOutput(
+            "go elsewhere",
+            [E("[ Test Location ]"),
+             "Location for testing.",
+             "Observer Player"],
+            ["Test Player arrives."])
+
+
+    def test_goDoesntJumpOverExits(self):
+        """
+        You can't go through an exit without passing through exits which lead
+        to it.  Going through an exit named 'east' will only work if it is east
+        of your I{present} location, even if it is easily reachable from where
+        you stand.
+        """
+        northRoom = objects.Thing(store=self.store, name=u'Northerly')
+        eastRoom = objects.Thing(store=self.store, name=u'Easterly')
+        for room in northRoom, eastRoom:
+            objects.Container.createFor(room, capacity=1000)
+        objects.Exit.link(self.location, northRoom, u'north', distance=0.1)
+        objects.Exit.link(northRoom, eastRoom, u'east', distance=0.1)
+        self.assertCommandOutput("go east", [E("You can't go that way.")], [])
+
+
     def testDirectionalMovement(self):
         # A couple tweaks to state to make the test simpler
         self.observer.location = None

=== modified file 'Imaginary/imaginary/test/test_container.py'
--- Imaginary/imaginary/test/test_container.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/test/test_container.py	2011-08-16 01:57:22 +0000
@@ -5,6 +5,7 @@
 from axiom import store
 
 from imaginary import eimaginary, objects
+from imaginary.test.commandutils import CommandTestCaseMixin, E
 
 class ContainerTestCase(unittest.TestCase):
     def setUp(self):
@@ -65,3 +66,57 @@
         self.assertRaises(eimaginary.Closed, self.container.remove, self.object)
         self.assertEquals(list(self.container.getContents()), [self.object])
         self.assertIdentical(self.object.location, self.containmentCore)
+
+
+
+class IngressAndEgressTestCase(CommandTestCaseMixin, unittest.TestCase):
+    """
+    I should be able to enter and exit containers that are sufficiently big.
+    """
+
+    def setUp(self):
+        """
+        Create a container, C{self.box} that is large enough to stand in.
+        """
+        CommandTestCaseMixin.setUp(self)
+        self.box = objects.Thing(store=self.store, name=u'box')
+        self.container = objects.Container.createFor(self.box, capacity=1000)
+        self.box.moveTo(self.location)
+
+
+    def test_enterBox(self):
+        """
+        I should be able to enter the box.
+        """
+        self.assertCommandOutput(
+            'enter box',
+            [E('[ Test Location ]'),
+             'Location for testing.',
+             'Observer Player and a box'],
+            ['Test Player leaves into the box.'])
+
+
+    def test_exitBox(self):
+        """
+        I should be able to exit the box.
+        """
+        self.player.moveTo(self.container)
+        self.assertCommandOutput(
+            'exit out',
+            [E('[ Test Location ]'),
+             'Location for testing.',
+             'Observer Player and a box'],
+            ['Test Player leaves out of the box.'])
+        self.assertEquals(self.player.location,
+                          self.location)
+
+
+    def test_enterWhileHoldingBox(self):
+        """
+        When I'm holding a container, I shouldn't be able to enter it.
+        """
+        self.container.thing.moveTo(self.player)
+        self.assertCommandOutput('enter box',
+                                 ["The box won't fit inside itself."],
+                                 [])
+

=== modified file 'Imaginary/imaginary/test/test_garments.py'
--- Imaginary/imaginary/test/test_garments.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/test/test_garments.py	2011-08-16 01:57:22 +0000
@@ -31,8 +31,7 @@
 
     def testWearing(self):
         self.wearer.putOn(self.shirtGarment)
-
-        self.assertEquals(self.shirt.location, None)
+        self.assertIdentical(self.shirt.location, None)
 
 
 
@@ -115,6 +114,26 @@
         self.assertIdentical(self.dukes.location, self.daisy)
 
 
+    def test_cantDropSomethingYouAreWearing(self):
+        """
+        If you're wearing an article of clothing, you should not be able to
+        drop it until you first take it off.  After taking it off, however, you
+        can move it around just fine.
+        """
+        wearer = iimaginary.IClothingWearer(self.daisy)
+        wearer.putOn(iimaginary.IClothing(self.undies))
+        af = self.assertRaises(ActionFailure, self.undies.moveTo,
+                               self.daisy.location)
+        self.assertEquals(
+            u''.join(af.event.plaintext(self.daisy)),
+            u"You can't move the pair of lacy underwear "
+            u"without removing it first.\n")
+
+        wearer.takeOff(iimaginary.IClothing(self.undies))
+        self.undies.moveTo(self.daisy.location)
+        self.assertEquals(self.daisy.location, self.undies.location)
+
+
     def testTakeOffUnderwearBeforePants(self):
         # TODO - underwear removal skill
         wearer = iimaginary.IClothingWearer(self.daisy)
@@ -174,10 +193,19 @@
 
 
 class FunSimulationStuff(commandutils.CommandTestCaseMixin, unittest.TestCase):
+
+    def createPants(self):
+        """
+        Create a pair of Daisy Dukes for the test player to wear.
+        """
+        self._test("create pants named 'pair of daisy dukes'",
+                   ["You create a pair of daisy dukes."],
+                   ["Test Player creates a pair of daisy dukes."])
+
+
+
     def testWearIt(self):
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("wear 'pair of daisy dukes'",
                    ["You put on the pair of daisy dukes."],
                    ["Test Player puts on a pair of daisy dukes."])
@@ -188,9 +216,7 @@
         A garment can be removed with the I{take off} action or the
         I{remove} action.
         """
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("wear 'pair of daisy dukes'",
                    ["You put on the pair of daisy dukes."],
                    ["Test Player puts on a pair of daisy dukes."])
@@ -207,9 +233,7 @@
 
 
     def testProperlyDressed(self):
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("create underwear named 'pair of lace panties'",
                    ["You create a pair of lace panties."],
                    ["Test Player creates a pair of lace panties."])
@@ -227,9 +251,7 @@
 
 
     def testTooBulky(self):
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("create pants named 'pair of overalls'",
                    ["You create a pair of overalls."],
                    ["Test Player creates a pair of overalls."])
@@ -248,9 +270,7 @@
 
 
     def testInaccessibleGarment(self):
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("create underwear named 'pair of lace panties'",
                    ["You create a pair of lace panties."],
                    ["Test Player creates a pair of lace panties."])
@@ -266,9 +286,7 @@
 
 
     def testEquipment(self):
-        self._test("create pants named 'pair of daisy dukes'",
-                   ["You create a pair of daisy dukes."],
-                   ["Test Player creates a pair of daisy dukes."])
+        self.createPants()
         self._test("create underwear named 'pair of lace panties'",
                    ["You create a pair of lace panties."],
                    ["Test Player creates a pair of lace panties."])

=== added file 'Imaginary/imaginary/test/test_idea.py'
--- Imaginary/imaginary/test/test_idea.py	1970-01-01 00:00:00 +0000
+++ Imaginary/imaginary/test/test_idea.py	2011-08-16 01:57:22 +0000
@@ -0,0 +1,241 @@
+
+"""
+Some basic unit tests for L{imaginary.idea} (but many tests for this code are in
+other modules instead).
+"""
+
+from zope.interface import implements
+
+from twisted.trial.unittest import TestCase
+
+from epsilon.structlike import record
+
+from imaginary.iimaginary import (
+    IWhyNot, INameable, ILinkContributor, IObstruction, ILinkAnnotator,
+    IElectromagneticMedium)
+from imaginary.language import ExpressString
+from imaginary.idea import (
+    Idea, Link, Path, AlsoKnownAs, ProviderOf, Named, DelegatingRetriever,
+    Reachable, CanSee)
+
+
+class Reprable(record('repr')):
+    def __repr__(self):
+        return self.repr
+
+
+class PathTests(TestCase):
+    """
+    Tests for L{imaginary.idea.Path}.
+    """
+    def test_repr(self):
+        """
+        A L{Path} instance can be rendered into a string by C{repr}.
+        """
+        key = Idea(AlsoKnownAs("key"))
+        table = Idea(AlsoKnownAs("table"))
+        hall = Idea(AlsoKnownAs("hall"))
+        path = Path([Link(hall, table), Link(table, key)])
+        self.assertEquals(
+            repr(path),
+            "Path(\n"
+            "\t'hall' => 'table' []\n"
+            "\t'table' => 'key' [])")
+
+
+    def test_unnamedDelegate(self):
+        """
+        The I{repr} of a L{Path} containing delegates without names includes the
+        I{repr} of the delegates.
+        """
+        key = Idea(Reprable("key"))
+        table = Idea(Reprable("table"))
+        hall = Idea(Reprable("hall"))
+        path = Path([Link(hall, table), Link(table, key)])
+        self.assertEquals(
+            repr(path),
+            "Path(\n"
+            "\thall => table []\n"
+            "\ttable => key [])")
+
+
+
+class OneLink(record('link')):
+    implements(ILinkContributor)
+
+    def links(self):
+        return [self.link]
+
+
+class TooHigh(object):
+    implements(IWhyNot)
+
+    def tellMeWhyNot(self):
+        return ExpressString("the table is too high")
+
+
+class ArmsReach(DelegatingRetriever):
+    """
+    Restrict retrievable to things within arm's reach.
+
+        alas for poor Alice! when she got to the door, she found he had
+        forgotten the little golden key, and when she went back to the table for
+        it, she found she could not possibly reach it:
+    """
+    def moreObjectionsTo(self, path, result):
+        """
+        Object to finding the key.
+        """
+        # This isn't a very good implementation of ArmsReach.  It doesn't
+        # actually check distances or paths or anything.  It just knows the
+        # key is on the table, and Alice is too short.
+        named = path.targetAs(INameable)
+        if named.knownTo(None, "key"):
+            return [TooHigh()]
+        return []
+
+
+class WonderlandSetupMixin:
+    """
+    A test case mixin which sets up a graph based on a scene from Alice in
+    Wonderland.
+    """
+    def setUp(self):
+        garden = Idea(AlsoKnownAs("garden"))
+        door = Idea(AlsoKnownAs("door"))
+        hall = Idea(AlsoKnownAs("hall"))
+        alice = Idea(AlsoKnownAs("alice"))
+        key = Idea(AlsoKnownAs("key"))
+        table = Idea(AlsoKnownAs("table"))
+
+        alice.linkers.append(OneLink(Link(alice, hall)))
+        hall.linkers.append(OneLink(Link(hall, door)))
+        hall.linkers.append(OneLink(Link(hall, table)))
+        table.linkers.append(OneLink(Link(table, key)))
+        door.linkers.append(OneLink(Link(door, garden)))
+
+        self.alice = alice
+        self.hall = hall
+        self.door = door
+        self.garden = garden
+        self.table = table
+        self.key = key
+
+
+
+class IdeaTests(WonderlandSetupMixin, TestCase):
+    """
+    Tests for L{imaginary.idea.Idea}.
+    """
+    def test_objections(self):
+        """
+        The L{IRetriver} passed to L{Idea.obtain} can object to certain results.
+        This excludes them from the result returned by L{Idea.obtain}.
+        """
+        # XXX The last argument is the observer, and is supposed to be an
+        # IThing.
+        retriever = Named("key", ProviderOf(INameable), self.alice)
+
+        # Sanity check.  Alice should be able to reach the key if we don't
+        # restrict things based on her height.
+        self.assertEquals(
+            list(self.alice.obtain(retriever)), [self.key.delegate])
+
+        # But when we consider how short she is, she should not be able to reach
+        # it.
+        results = self.alice.obtain(ArmsReach(retriever))
+        self.assertEquals(list(results), [])
+
+
+class Closed(object):
+    implements(IObstruction)
+
+    def whyNot(self):
+        return ExpressString("the door is closed")
+
+
+
+class ConstantAnnotation(record('annotation')):
+    implements(ILinkAnnotator)
+
+    def annotationsFor(self, link, idea):
+        return [self.annotation]
+
+
+
+class ReachableTests(WonderlandSetupMixin, TestCase):
+    """
+    Tests for L{imaginary.idea.Reachable}.
+    """
+    def setUp(self):
+        WonderlandSetupMixin.setUp(self)
+        # XXX The last argument is the observer, and is supposed to be an
+        # IThing.
+        self.retriever = Reachable(
+            Named("garden", ProviderOf(INameable), self.alice))
+
+
+    def test_anyObstruction(self):
+        """
+        If there are any obstructions in the path traversed by the retriever
+        wrapped by L{Reachable}, L{Reachable} objects to them and they are not
+        returned by L{Idea.obtain}.
+        """
+        # Make the door closed..  Now Alice cannot reach the garden.
+        self.door.annotators.append(ConstantAnnotation(Closed()))
+        self.assertEquals(list(self.alice.obtain(self.retriever)), [])
+
+
+    def test_noObstruction(self):
+        """
+        If there are no obstructions in the path traversed by the retriever
+        wrapped by L{Reachable}, all results are returned by L{Idea.obtain}.
+        """
+        self.assertEquals(
+            list(self.alice.obtain(self.retriever)),
+            [self.garden.delegate])
+
+
+class Wood(object):
+    implements(IElectromagneticMedium)
+
+    def isOpaque(self):
+        return True
+
+
+
+class Glass(object):
+    implements(IElectromagneticMedium)
+
+    def isOpaque(self):
+        return False
+
+
+class CanSeeTests(WonderlandSetupMixin, TestCase):
+    """
+    Tests for L{imaginary.idea.CanSee}.
+    """
+    def setUp(self):
+        WonderlandSetupMixin.setUp(self)
+        self.retriever = CanSee(
+            Named("garden", ProviderOf(INameable), self.alice))
+
+
+    def test_throughTransparent(self):
+        """
+        L{Idea.obtain} continues past an L{IElectromagneticMedium} which returns
+        C{False} from its C{isOpaque} method.
+        """
+        self.door.annotators.append(ConstantAnnotation(Glass()))
+        self.assertEquals(
+            list(self.alice.obtain(self.retriever)), [self.garden.delegate])
+
+
+    def test_notThroughOpaque(self):
+        """
+        L{Idea.obtain} does not continue past an L{IElectromagneticMedium} which
+        returns C{True} from its C{isOpaque} method.
+        """
+        # Make the door opaque.  Now Alice cannot see the garden.
+        self.door.annotators.append(ConstantAnnotation(Wood()))
+        self.assertEquals(list(self.alice.obtain(self.retriever)), [])

=== modified file 'Imaginary/imaginary/test/test_illumination.py'
--- Imaginary/imaginary/test/test_illumination.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/test/test_illumination.py	2011-08-16 01:57:22 +0000
@@ -1,8 +1,13 @@
+
+from zope.interface import implements
+
 from twisted.trial import unittest
 
-from axiom import store
+from axiom import store, item, attributes
 
-from imaginary import iimaginary, objects
+from imaginary.enhancement import Enhancement
+from imaginary import iimaginary, objects, idea
+from imaginary.language import ExpressString
 from imaginary.manipulation import Manipulator
 
 from imaginary.test import commandutils
@@ -67,14 +72,27 @@
         self.assertEquals(len(found), 3)
 
 
-    def testNonVisibilityUnaffected(self):
+    def test_nonVisibilityAffected(self):
         """
-        Test that the LocationLightning thingy doesn't block out non-IVisible
-        stuff.
+        L{LocationLightning} blocks out non-IVisible stuff from
+        L{Thing.findProviders} by default.
         """
         self.assertEquals(
             list(self.observer.findProviders(iimaginary.IThing, 3)),
-            [self.observer, self.location, self.rock])
+            [])
+        # XXX need another test: not blocked out from ...
+
+
+    def test_nonVisibilityUnaffected(self):
+        """
+        L{LocationLightning} should not block out non-IVisible stuff from a
+        plain L{Idea.obtain} query.
+        """
+        self.assertEquals(
+            list(self.observer.idea.obtain(
+                    idea.Proximity(3, idea.ProviderOf(iimaginary.IThing)))),
+            [self.observer, self.location, self.rock]
+            )
 
 
     def testLightSourceInLocation(self):
@@ -104,7 +122,7 @@
 
         self.assertEquals(
             list(self.observer.findProviders(iimaginary.IVisible, 1)),
-            [self.observer, self.location, torch, self.rock])
+            [self.observer, torch, self.location, self.rock])
 
 
     def testOccultedLightSource(self):
@@ -216,3 +234,137 @@
         self.assertEquals(self.store.findUnique(
                 objects.LocationLighting,
                 objects.LocationLighting.thing == self.location).candelas, 100)
+
+
+class ActionsInDarkRoomTestCase(commandutils.CommandTestCaseMixin,
+                                unittest.TestCase):
+    """
+    Darkness interferes with other commands.
+    """
+
+    def setUp(self):
+        """
+        There's a room which is dark, where the player is trying to do things.
+        """
+        commandutils.CommandTestCaseMixin.setUp(self)
+        self.lighting = objects.LocationLighting.createFor(
+            self.location, candelas=0)
+
+
+    def test_actionWithTargetInDarkRoom(self):
+        """
+        By default, actions which require objects in a darkened room should
+        fail, because it's too dark.
+        """
+        self.assertCommandOutput(
+            "create pants named 'pair of pants'",
+            ["You create a pair of pants."],
+            ["Test Player creates a pair of pants."])
+
+        # The action is going to try to locate its target.  During the graph
+        # traversal it shouldn't find _any_ pants.  Whether or not we find any
+        # pants, we want the message to note that it's too dark.  The reason is
+        # actually a property of a link (or perhaps a set of links: i.e. the
+        # me->me link, the me->chair link, the chair->room link) so the
+        # retriever is going to need to keep a list of those (Refusals) as it
+        # retrieves each one.
+        #
+        # resolve calls search
+        # search calls findProviders
+        # findProviders constructs a thingy, calls obtain()
+
+        self.test_actionWithNoTargetInDarkRoom()
+
+
+    def test_actionWithTargetInAdjacentDarkRoom(self):
+        """
+        If a player is standing I{next} to a dark room, they should not be able
+        to locate targets in the dark room, but the reporting in this case
+        should be normal, not the "It's too dark to see" that would result if
+        they were in the dark room themselves.
+        """
+        self.otherRoom = objects.Thing(store=self.store, name=u'Elsewhere')
+        objects.Container.createFor(self.otherRoom, capacity=1000)
+        objects.Exit.link(self.location, self.otherRoom, u'west')
+        self.player.moveTo(self.otherRoom)
+        self.observer.moveTo(self.otherRoom)
+        self.assertCommandOutput(
+            "wear pants",
+            [commandutils.E(u"Who's that?")],
+            [])
+
+
+    def test_actionWithNoTargetInDarkRoom(self):
+        """
+        By default, actions which require objects in a darkened room should
+        fail because it's too dark, even if there is actually no target to be
+        picked up.
+        """
+        self._test(
+            "wear pants",
+            ["It's too dark to see."], # to dark to see... the pants?  any pants?
+            [])
+
+
+    def test_examiningNonThing(self):
+        """
+        When examining an L{IVisible} which is not also an L{IThing}, it should
+        be dark.
+        """
+        t = objects.Thing(name=u"magic stone", store=self.store)
+        t.powerUp(MagicStone(thing=t, store=self.store))
+        t.moveTo(self.location)
+
+        self.assertCommandOutput(
+            "look at rune",
+            ["It's too dark to see."],
+            [])
+        self.lighting.candelas = 100
+        self.assertCommandOutput(
+            "look at rune",
+            ["A totally mystical rune."],
+            [])
+
+
+
+class Rune(object):
+    """
+    This is an example provider of L{iimaginary.IVisible} which is not an
+    L{iimaginary.IThing}.
+    """
+
+    implements(iimaginary.IVisible, iimaginary.INameable)
+
+    def visualize(self):
+        """
+        Return an L{ExpressString} with a sample string that can be tested
+        against.
+        """
+        return ExpressString("A totally mystical rune.")
+
+
+    def knownTo(self, observer, asName):
+        """
+        Implement L{iimaginary.INameable.knownTo} to respond to the word 'rune'
+        and nothing else, so that this object may be found by
+        L{imaginary.idea.Idea.obtain}.
+        """
+        return (asName == "rune")
+
+
+
+class MagicStone(item.Item, Enhancement):
+    """
+    This is a magic stone that has a rune on it which you can examine.
+    """
+
+    implements(iimaginary.ILinkContributor)
+    powerupInterfaces = [iimaginary.ILinkContributor]
+    thing = attributes.reference()
+
+    def links(self):
+        """
+        Implement L{ILinkContributor} to yield a single link to a L{Rune}.
+        """
+        runeIdea = idea.Idea(Rune())
+        yield idea.Link(self.thing.idea, runeIdea)

=== modified file 'Imaginary/imaginary/test/test_objects.py'
--- Imaginary/imaginary/test/test_objects.py	2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/test/test_objects.py	2011-08-16 01:57:22 +0000
@@ -1,13 +1,12 @@
 
-from zope.interface import Interface, implements
+from zope.interface import Interface
 
 from twisted.trial import unittest
 from twisted.python import components
 
-from axiom import store, item, attributes
+from axiom import store
 
 from imaginary import iimaginary, eimaginary, objects, events
-from imaginary.enhancement import Enhancement
 from imaginary.test import commandutils
 
 
@@ -236,37 +235,6 @@
 components.registerAdapter(lambda o: (unexpected, o), objects.Thing, IFoo)
 
 
-class Proxy(item.Item, Enhancement):
-    implements(iimaginary.IProxy)
-
-    thing = attributes.reference()
-
-    provider = attributes.inmemory()
-
-    priority = attributes.integer(default=0)
-
-    proxiedObjects = attributes.inmemory()
-
-    def __getPowerupInterfaces__(self, other):
-        yield (iimaginary.IProxy, self.priority)
-
-    # IProxy
-    def proxy(self, facet, iface):
-        getattr(self, 'proxiedObjects', []).append((facet, iface))
-        return self.provider
-
-
-
-class StubLocationProxy(item.Item, Enhancement):
-    implements(iimaginary.ILocationProxy)
-
-    thing = attributes.reference()
-    powerupInterfaces = (iimaginary.ILocationProxy,)
-
-    def proxy(self, facet, interface):
-        return (facet,)
-
-
 
 class FindProvidersTestCase(unittest.TestCase):
     def setUp(self):
@@ -370,236 +338,42 @@
             [(unexpected, self.obj), (unexpected, self.room)])
 
 
-    def testProxyRestrictsResults(self):
-        """
-        If we put a proxy between the object and the room, and the proxy
-        returns None, then no facets should be returned when searching for
-        providers of IFoo.
-        """
-        self.retain(Proxy.createFor(self.obj, provider=None))
-
-        self.assertEquals(
-            list(self.obj.findProviders(IFoo, 1)),
-            [])
-
-
-    def testProxyReturnsAlternate(self):
-        """
-        Similar to testProxyReturnsAlternate, but using a proxy which returns
-        an alternative provider. The provider should be in the result of
-        findProviders.
-        """
-        expected = u"expected"
-        self.retain(Proxy.createFor(self.obj, provider=expected))
-
-        self.assertEquals(
-            list(self.obj.findProviders(IFoo, 1)),
-            [expected, expected])
-
-
-    def testProxyNoneWins(self):
-        """
-        If the first proxy found returns None, and the second proxy found
-        returns an object, then nothing should be returned from findProviders.
-        """
-        expected = u"zoom"
-        self.retain(Proxy.createFor(self.obj, priority=1, provider=None))
-        self.retain(Proxy.createFor(self.obj, priority=2, provider=expected))
-
-        self.assertEquals(
-            list(self.obj.findProviders(IFoo, 1)),
-            [])
-
-
-    def testProxyApplicability(self):
-        """
-        Test that an observer sees a room through a proxy on the room, but sees
-        himself unproxied.
-        """
-        expected = u"frotz"
-        p = Proxy.createFor(self.room, provider=expected)
-        p.proxiedObjects = []
-
-        self.assertEquals(
-            list(self.obj.findProviders(IFoo, 1)),
-            [(unexpected, self.obj), expected])
-
-        self.assertEquals(
-            p.proxiedObjects,
-            [((unexpected, self.room), IFoo)])
-
-
-    # TODO: test similar to testProxyApplicability only obj -> proxy1 -> obj2 -> proxy2 -> obj3.
-
-    def testLocationProxy(self):
-        """
-        Test that ILocationProxy powerups on a location are asked to proxy for
-        all objects within location.
-
-        Also test that an ILocationProxy will get the location on which it is
-        powered up passed to its proxy method.
-        """
-        StubLocationProxy.createFor(self.room)
-
-        self.assertEquals(list(self.obj.findProviders(iimaginary.IThing, 1)),
-                          [(self.obj,), (self.room,)])
-
-
-    def testLocationProxyProxiesIndirectContents(self):
-        """
-        Similar to testLocationProxy, but also ensure that objects which are
-        indirectly contained by the location are also proxied.
-        """
-        StubLocationProxy.createFor(self.room)
-        objects.Container.createFor(self.obj, capacity=9999)
-        rock = objects.Thing(store=self.store, name=u"rock")
-        rock.moveTo(self.obj)
-
-        self.assertEquals(
-            list(self.obj.findProviders(iimaginary.IThing, 1)),
-            [(self.obj,), (rock,), (self.room,)])
-
-
-    def testLocationProxyOnlyAppliesToContainedObjects(self):
-        """
-        Test Location Proxy Only Applies To Contained Objects.
-        """
-        StubLocationProxy.createFor(self.room)
-
-        nearby = objects.Thing(store=self.store, name=u"other room")
-        objects.Container.createFor(nearby, capacity=1000)
-        ball = objects.Thing(store=self.store, name=u"ball")
-        ball.moveTo(nearby)
-
-        objects.Exit.link(self.room, nearby, u"west")
-
-        self.assertEquals(list(self.obj.findProviders(iimaginary.IThing, 2)),
-                          [(self.obj,), (self.room,), nearby, ball])
-
-
-
-    def testRemoteLocationProxies(self):
-        """
-        Test that location proxies apply to their contents, even when the
-        findProviders call is originated from a different location.
-        """
-
-        nearby = objects.Thing(store=self.store, name=u"other room")
-        objects.Container.createFor(nearby, capacity=1000)
-        ball = objects.Thing(store=self.store, name=u"ball")
-        ball.moveTo(nearby)
-
-        StubLocationProxy.createFor(nearby)
-
-        objects.Exit.link(self.room, nearby, u"west")
-
-
-        self.assertEquals(list(self.obj.findProviders(iimaginary.IThing, 2)),
-                          [self.obj, self.room, (nearby,), (ball,)])
-
-
-
     def test_exactlyKnownAs(self):
         """
-        L{Thing.knownAs} returns C{True} when called with exactly the things
+        L{Thing.knownTo} returns C{True} when called with exactly the things
         own name.
         """
-        self.assertTrue(self.obj.knownAs(self.obj.name))
+        self.assertTrue(self.obj.knownTo(self.obj, self.obj.name))
 
 
     def test_caseInsensitivelyKnownAs(self):
         """
-        L{Thing.knownAs} returns C{True} when called with a string which
+        L{Thing.knownTo} returns C{True} when called with a string which
         differs from its name only in case.
         """
-        self.assertTrue(self.obj.knownAs(self.obj.name.upper()))
-        self.assertTrue(self.obj.knownAs(self.obj.name.title()))
+        self.assertTrue(self.obj.knownTo(self.obj, self.obj.name.upper()))
+        self.assertTrue(self.obj.knownTo(self.obj, self.obj.name.title()))
 
 
     def test_wholeWordSubstringKnownAs(self):
         """
-        L{Thing.knownAs} returns C{True} when called with a string which
+        L{Thing.knownTo} returns C{True} when called with a string which
         appears in the thing's name delimited by spaces.
         """
         self.obj.name = u"one two three"
-        self.assertTrue(self.obj.knownAs(u"one"))
-        self.assertTrue(self.obj.knownAs(u"two"))
-        self.assertTrue(self.obj.knownAs(u"three"))
+        self.assertTrue(self.obj.knownTo(self.obj, u"one"))
+        self.assertTrue(self.obj.knownTo(self.obj, u"two"))
+        self.assertTrue(self.obj.knownTo(self.obj, u"three"))
 
 
     def test_notKnownAs(self):
         """
-        L{Thing.knownAs} returns C{False} when called with a string which
+        L{Thing.knownTo} returns C{False} when called with a string which
         doesn't satisfy one of the above positive cases.
         """
-        self.assertFalse(self.obj.knownAs(u"gunk" + self.obj.name))
+        self.assertFalse(self.obj.knownTo(self.obj, u"gunk" + self.obj.name))
         self.obj.name = u"one two three"
-        self.assertFalse(self.obj.knownAs(u"ne tw"))
-
-
-    def testNotReallyProxiedSelfThing(self):
-        """
-        Test that an unwrapped Thing can be found from itself through the
-        proxy-resolving method L{IThing.proxiedThing}.
-        """
-        self.assertIdentical(
-            self.obj.proxiedThing(self.obj, iimaginary.IThing, 0),
-            self.obj)
-
-
-    def testNotReallyProxiedOtherThing(self):
-        """
-        Like testNotReallyProxiedSelfThing, but find an object other than the
-        finder.
-        """
-        self.assertIdentical(
-            self.obj.proxiedThing(self.room, iimaginary.IThing, 0),
-            self.room)
-
-
-
-    def testCannotFindProxiedThing(self):
-        """
-        Test that L{IThing.proxiedThing} raises the appropriate exception when
-        the searched-for thing cannot be found.
-        """
-        self.assertRaises(
-            eimaginary.ThingNotFound,
-            self.obj.proxiedThing,
-            objects.Thing(store=self.store, name=u"nonexistent"),
-            iimaginary.IThing,
-            0)
-
-
-    def testActuallyProxiedSelfThing(self):
-        """
-        Test that if a proxy gets in the way, it is properly respected by
-        L{IThing.proxiedThing}.
-        """
-        class result(object):
-            thing = self.obj
-
-        self.retain(Proxy.createFor(self.obj, provider=result))
-
-        self.assertIdentical(
-            self.obj.proxiedThing(self.obj, iimaginary.IThing, 0),
-            result)
-
-
-    def testActuallyProxiedOtherThing(self):
-        """
-        Just like testActuallyProxiedSelfThing, but look for a Thing other than
-        the finder.
-        """
-        class result(object):
-            thing = self.room
-
-        self.retain(Proxy.createFor(self.obj, provider=result))
-
-        self.assertIdentical(
-            self.obj.proxiedThing(self.room, iimaginary.IThing, 0),
-            result)
-
+        self.assertFalse(self.obj.knownTo(self.obj, u"ne tw"))
 
 
     def test_searchForThings(self):
@@ -634,3 +408,32 @@
             [room])
 
 
+    def test_searchFindsRelativeExit(self):
+        """
+        L{Thing.search} should only find the exit known relative to the player
+        who is asking.  In other words, the room where the player is standing
+        may be north from I{somewhere}, but it should not be known as 'north'
+        to the player.
+        """
+        def mkroom(n):
+            room = objects.Thing(store=self.store, name=n)
+            room.powerUp(objects.Container(store=self.store, capacity=1000,
+                                           thing=room))
+            return room
+        north = mkroom(u"Northerly")
+        middle = self.room
+        south = mkroom(u"Southerly")
+        objects.Exit.link(south, middle, u"north")
+        objects.Exit.link(middle, north, u"north")
+
+        self.assertEquals(
+            list(self.obj.search(100, iimaginary.IThing, u"north")),
+            [north])
+        self.assertEquals(
+            list(self.obj.search(100, iimaginary.IThing, u"south")),
+            [south])
+
+
+    # XXX Test: me
+    # XXX Test: here
+    # XXX Test: self

=== modified file 'Imaginary/imaginary/test/test_player.py'
--- Imaginary/imaginary/test/test_player.py	2009-06-29 18:32:07 +0000
+++ Imaginary/imaginary/test/test_player.py	2011-08-16 01:57:22 +0000
@@ -56,25 +56,14 @@
         When the player refers to something ambiguously, the error message
         should enumerate the objects in question.
         """
-        def newThing(color):
+        for color in [u'red', u'green', u'blue']:
             it = objects.Thing(store=self.store, name=u'%s thing' % (color,))
             it.moveTo(self.room)
 
-        for color in [u'red', u'green']:
-            newThing(color)
-
-        self.player.parse(u"take thing")
-
-        self.assertEquals(self.transport.value(),
-                          '> take thing\n'
-                          'Could you be more specific?  When you said "thing", '
-                          'did you mean a green thing or a red thing?\r\n')
-
-        self.transport.clear()
-        newThing(u'blue')
-        self.player.parse(u"take thing")
-        self.assertEquals(self.transport.value(),
-                          '> take thing\n'
-                          'Could you be more specific?  When you said "thing", '
-                          'did you mean a blue thing, a green thing, or a red '
-                          'thing?\r\n')
+        self.player.parse("take thing")
+
+        self.assertEquals(self.transport.value(),
+                          "> take thing\n"
+                          "Could you be more specific?  When you said 'thing', "
+                          "did you mean: a red thing, a green thing, "
+                          "or a blue thing?\r\n")

=== modified file 'Imaginary/imaginary/wiring/player.py'
--- Imaginary/imaginary/wiring/player.py	2009-06-29 18:32:07 +0000
+++ Imaginary/imaginary/wiring/player.py	2011-08-16 01:57:22 +0000
@@ -45,28 +45,14 @@
         def ebAmbiguity(err):
             err.trap(eimaginary.AmbiguousArgument)
             exc = err.value
-            if len(exc.objects) == 0:
-                func = getattr(err.value.action, err.value.part + "NotAvailable", None)
-                if func:
-                    msg = func(self.actor, exc)
-                else:
-                    msg = "Who's that?"
-            else:
-                msg = ('Could you be more specific?  When you said "' +
-                       exc.partValue + '", did you mean ')
-                formatted = [
-                    ''.join(iimaginary.IConcept(
-                            potentialTarget).vt102(self.player))
-                    for potentialTarget in exc.objects]
-                formatted.sort()
-                for astring in formatted[:-1]:
-                    msg += astring
-                    if len(formatted) > 2:
-                        msg += ","
-                    msg += " "
-                msg += "or "
-                msg += formatted[-1]
-                msg += "?"
+            msg = "Could you be more specific?  When you said '" + exc.partValue + "', did you mean: "
+            format = lambda potentialTarget: ''.join(iimaginary.IConcept(potentialTarget).vt102(self.player))
+            for obj in exc.objects[:-1]:
+                msg += format(obj)
+                msg += ", "
+            msg += "or "
+            msg += format(exc.objects[-1])
+            msg += "?"
             self.send((msg, "\r\n"))
 
         def ebUnexpected(err):


Follow ups