divmod-dev team mailing list archive
-
divmod-dev team
-
Mailing list archive
-
Message #00079
[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