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

Proposed by Glyph Lefkowitz on 2011-08-16
Status: Merged
Merged at revision: 2683
Proposed branch: lp:~divmod-dev/divmod.org/obtain-2824-6
Merge into: lp:divmod.org
Diff against target: 5589 lines (+3826/-806)
31 files modified
Epsilon/epsilon/remember.py (+39/-0)
Epsilon/epsilon/test/test_remember.py (+90/-0)
Imaginary/COMPATIBILITY.txt (+6/-0)
Imaginary/ExampleGame/examplegame/furniture.py (+139/-0)
Imaginary/ExampleGame/examplegame/glass.py (+72/-0)
Imaginary/ExampleGame/examplegame/mice.py (+1/-1)
Imaginary/ExampleGame/examplegame/squeaky.py (+42/-0)
Imaginary/ExampleGame/examplegame/test/test_furniture.py (+107/-0)
Imaginary/ExampleGame/examplegame/test/test_glass.py (+95/-0)
Imaginary/ExampleGame/examplegame/test/test_squeaky.py (+51/-0)
Imaginary/ExampleGame/examplegame/test/test_tether.py (+96/-0)
Imaginary/ExampleGame/examplegame/tether.py (+121/-0)
Imaginary/TODO.txt (+258/-0)
Imaginary/imaginary/action.py (+261/-130)
Imaginary/imaginary/creation.py (+3/-3)
Imaginary/imaginary/events.py (+16/-11)
Imaginary/imaginary/garments.py (+130/-17)
Imaginary/imaginary/idea.py (+625/-0)
Imaginary/imaginary/iimaginary.py (+313/-87)
Imaginary/imaginary/language.py (+12/-0)
Imaginary/imaginary/objects.py (+742/-248)
Imaginary/imaginary/resources/motd (+1/-1)
Imaginary/imaginary/test/commandutils.py (+11/-0)
Imaginary/imaginary/test/test_actions.py (+43/-0)
Imaginary/imaginary/test/test_container.py (+55/-0)
Imaginary/imaginary/test/test_garments.py (+38/-20)
Imaginary/imaginary/test/test_idea.py (+241/-0)
Imaginary/imaginary/test/test_illumination.py (+159/-7)
Imaginary/imaginary/test/test_objects.py (+43/-240)
Imaginary/imaginary/test/test_player.py (+8/-19)
Imaginary/imaginary/wiring/player.py (+8/-22)
To merge this branch: bzr merge lp:~divmod-dev/divmod.org/obtain-2824-6
Reviewer Review Type Date Requested Status
Jean-Paul Calderone 2011-08-16 Approve on 2011-09-16
Review via email: mp+71631@code.launchpad.net

Description of the change

It makes obtain() better. this is basically what should be Imaginary trunk anyway; it changes a bunch of semantics, and improves a bunch of documentation.

To post a comment you must log in.
2679. By Jean-Paul Calderone on 2011-09-16

Put the _DisregardYourWearingIt hack back in place. It is required for the tethering features in ExampleGame. This means worn clothing once again has a location of the wearer.

2680. By Jean-Paul Calderone on 2011-09-16

There is no self

2681. By Jean-Paul Calderone on 2011-09-16

note about some uncovered code

2682. By Jean-Paul Calderone on 2011-09-16

Some minor doc fixes, and a note about Exit.link being confusing.

Jean-Paul Calderone (exarkun) wrote :

Okay. Branch is too large. It is a challenge to understand (either the change or just the new code on its own). Too large partly because it does too many things - adds better naming features, updates unrelated documentation, changes how actions are dispatched, changes the action class hierarchy naming, etc. Hard to understand because the simulation core now hinges on a number of distinct concepts - links, annotations, obstructions, retrievers, maybe some others - but lacks any documentation about how they relate to each other (Thing.__doc__ is a noble effort, but gives up about 1/6th of the way though).

I think near future efforts on Imaginary need to focus on documentation, examples, and test coverage. Once the branch is merged, I hope efforts will pick up on those fronts.

review: Approve
Glyph Lefkowitz (glyph) wrote :

Once again, I apologize for the size of the branch. I'll try never to do that again :). Thanks for taking the time to attempt to comprehend this on multiple occasions, and thanks for landing it!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'Epsilon/epsilon/remember.py'
--- Epsilon/epsilon/remember.py 1970-01-01 00:00:00 +0000
+++ Epsilon/epsilon/remember.py 2011-09-16 20:42:26 +0000
@@ -0,0 +1,39 @@
1# -*- test-case-name: epsilon.test.test_remember -*-
2
3"""
4This module implements a utility for managing the lifecycle of attributes
5related to a particular object.
6"""
7
8from epsilon.structlike import record
9
10class remembered(record('creationFunction')):
11 """
12 This descriptor decorator is applied to a function to create an attribute
13 which will be created on-demand, but remembered for the lifetime of the
14 instance to which it is attached. Subsequent accesses of the attribute
15 will return the remembered value.
16
17 @ivar creationFunction: the decorated function, to be called to create the
18 value. This should be a 1-argument callable, that takes only a 'self'
19 parameter, like a method.
20 """
21
22 value = None
23
24 def __get__(self, oself, type):
25 """
26 Retrieve the value if already cached, otherwise, call the
27 C{creationFunction} to create it.
28 """
29 remembername = "_remembered_" + self.creationFunction.func_name
30 rememberedval = oself.__dict__.get(remembername, None)
31 if rememberedval is not None:
32 return rememberedval
33 rememberme = self.creationFunction(oself)
34 oself.__dict__[remembername] = rememberme
35 return rememberme
36
37
38
39__all__ = ['remembered']
040
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,90 @@
1
2from twisted.trial.unittest import TestCase
3
4from epsilon.remember import remembered
5from epsilon.structlike import record
6
7class Rememberee(record("rememberer whichValue")):
8 """
9 A sample value that holds on to its L{Rememberer}.
10 """
11
12
13class Rememberer(object):
14 """
15 Sample application code which uses epsilon.remember.
16
17 @ivar invocations: The number of times that it is invoked.
18 """
19
20 invocations = 0
21 otherInvocations = 0
22
23 @remembered
24 def value1(self):
25 """
26 I remember a value.
27 """
28 self.invocations += 1
29 return Rememberee(self, 1)
30
31
32 @remembered
33 def value2(self):
34 """
35 A separate value.
36 """
37 self.otherInvocations += 1
38 return Rememberee(self, 2)
39
40
41class RememberedTests(TestCase):
42 """
43 The "remembered" decorator allows you to lazily create an attribute and
44 remember it.
45 """
46
47 def setUp(self):
48 """
49 Create a L{Rememberer} for use with the tests.
50 """
51 self.rememberer = Rememberer()
52
53
54 def test_selfArgument(self):
55 """
56 The "self" argument to the decorated creation function will be the
57 instance the property is accessed upon.
58 """
59 value = self.rememberer.value1
60 self.assertIdentical(value.rememberer, self.rememberer)
61
62
63 def test_onlyOneInvocation(self):
64 """
65 The callable wrapped by C{@remembered} will only be invoked once,
66 regardless of how many times the attribute is accessed.
67 """
68 self.assertEquals(self.rememberer.invocations, 0)
69 firstTime = self.rememberer.value1
70 self.assertEquals(self.rememberer.invocations, 1)
71 secondTime = self.rememberer.value1
72 self.assertEquals(self.rememberer.invocations, 1)
73 self.assertIdentical(firstTime, secondTime)
74
75
76 def test_twoValues(self):
77 """
78 If the L{@remembered} decorator is used more than once, each one will
79 be an attribute with its own identity.
80 """
81 self.assertEquals(self.rememberer.invocations, 0)
82 self.assertEquals(self.rememberer.otherInvocations, 0)
83 firstValue1 = self.rememberer.value1
84 self.assertEquals(self.rememberer.invocations, 1)
85 self.assertEquals(self.rememberer.otherInvocations, 0)
86 firstValue2 = self.rememberer.value2
87 self.assertEquals(self.rememberer.otherInvocations, 1)
88 self.assertNotIdentical(firstValue1, firstValue2)
89 secondValue2 = self.rememberer.value2
90 self.assertIdentical(firstValue2, secondValue2)
091
=== added file 'Imaginary/COMPATIBILITY.txt'
--- Imaginary/COMPATIBILITY.txt 1970-01-01 00:00:00 +0000
+++ Imaginary/COMPATIBILITY.txt 2011-09-16 20:42:26 +0000
@@ -0,0 +1,6 @@
1Imaginary provides _no_ source-level compatibility from one release to the next.
2
3Efforts will be made to provide database level compatibility (i.e. data from
4one release can be loaded with the next). However, although we will try to
5ensure that data will load, there is no guarantee that it will be meaningfully
6upgraded yet.
07
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,139 @@
1# -*- test-case-name: examplegame.test.test_furniture -*-
2
3"""
4
5 Furniture is the mass noun for the movable objects which may support the
6 human body (seating furniture and beds), provide storage, or hold objects
7 on horizontal surfaces above the ground.
8
9 -- Wikipedia, http://en.wikipedia.org/wiki/Furniture
10
11L{imaginary.furniture} contains L{Action}s which allow players to interact with
12household objects such as chairs and tables, and L{Enhancement}s which allow
13L{Thing}s to behave as same.
14
15This has the same implementation weakness as L{examplegame.tether}, in that it
16needs to make some assumptions about who is moving what in its restrictions of
17movement; it should be moved into imaginary proper when that can be properly
18addressed. It's also a bit too draconian in terms of preventing the player
19from moving for any reason just because they're seated. However, it's a
20workable approximation for some uses, and thus useful as an example.
21"""
22
23from zope.interface import implements
24
25from axiom.item import Item
26from axiom.attributes import reference
27
28from imaginary.iimaginary import ISittable, IContainer, IMovementRestriction
29from imaginary.eimaginary import ActionFailure
30from imaginary.events import ThatDoesntWork
31from imaginary.language import Noun
32from imaginary.action import Action, TargetAction
33from imaginary.events import Success
34from imaginary.enhancement import Enhancement
35from imaginary.objects import Container
36from imaginary.pyparsing import Literal, Optional, restOfLine
37
38class Sit(TargetAction):
39 """
40 An action allowing a player to sit down in a chair.
41 """
42 expr = (Literal("sit") + Optional(Literal("on")) +
43 restOfLine.setResultsName("target"))
44
45 targetInterface = ISittable
46
47 def do(self, player, line, target):
48 """
49 Do the action; sit down.
50 """
51 target.seat(player)
52
53 actorMessage=["You sit in ",
54 Noun(target.thing).definiteNounPhrase(),"."]
55 otherMessage=[player.thing, " sits in ",
56 Noun(target.thing).definiteNounPhrase(),"."]
57 Success(actor=player.thing, location=player.thing.location,
58 actorMessage=actorMessage,
59 otherMessage=otherMessage).broadcast()
60
61
62class Stand(Action):
63 """
64 Stand up from a sitting position.
65 """
66 expr = (Literal("stand") + Optional(Literal("up")))
67
68 def do(self, player, line):
69 """
70 Do the action; stand up.
71 """
72 # XXX This is wrong. I should be issuing an obtain() query to find
73 # something that qualifies as "my location" or "the thing I'm already
74 # sitting in".
75 chair = ISittable(player.thing.location, None)
76 if chair is None:
77 raise ActionFailure(ThatDoesntWork(
78 actor=player.thing,
79 actorMessage=["You're already standing."]))
80 chair.unseat(player)
81 Success(actor=player.thing, location=player.thing.location,
82 actorMessage=["You stand up."],
83 otherMessage=[player.thing, " stands up."]).broadcast()
84
85
86
87class Chair(Enhancement, Item):
88 """
89 A chair is a thing you can sit in.
90 """
91
92 implements(ISittable, IMovementRestriction)
93
94 powerupInterfaces = [ISittable]
95
96 thing = reference()
97 container = reference()
98
99
100 def movementImminent(self, movee, destination):
101 """
102 A player tried to move while they were seated. Prevent them from doing
103 so, noting that they must stand first.
104
105 (Assume the player was trying to move themselves, although there's no
106 way to know currently.)
107 """
108 raise ActionFailure(ThatDoesntWork(
109 actor=movee,
110 actorMessage=u"You can't do that while sitting down."))
111
112
113 def applyEnhancement(self):
114 """
115 Apply this enhancement to this L{Chair}'s thing, creating a
116 L{Container} to hold the seated player, if necessary.
117 """
118 super(Chair, self).applyEnhancement()
119 container = IContainer(self.thing, None)
120 if container is None:
121 container = Container.createFor(self.thing, capacity=300)
122 self.container = container
123
124
125 def seat(self, player):
126 """
127 The player sat down on this chair; place them into it and prevent them
128 from moving elsewhere until they stand up.
129 """
130 player.thing.moveTo(self.container)
131 player.thing.powerUp(self, IMovementRestriction)
132
133
134 def unseat(self, player):
135 """
136 The player stood up; remove them from this chair.
137 """
138 player.thing.powerDown(self, IMovementRestriction)
139 player.thing.moveTo(self.container.thing.location)
0140
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,72 @@
1# -*- test-case-name: examplegame.test.test_glass -*-
2"""
3This example implements a transparent container that you can see, but not
4reach, the contents of.
5"""
6from zope.interface import implements
7
8from axiom.item import Item
9from axiom.attributes import reference
10
11from imaginary.iimaginary import (
12 ILinkContributor, IWhyNot, IObstruction, IContainer)
13from imaginary.enhancement import Enhancement
14from imaginary.objects import ContainmentRelationship
15from imaginary.idea import Link
16
17class _CantReachThroughGlassBox(object):
18 """
19 This object provides an explanation for why the user cannot access a target
20 that is inside a L{imaginary.objects.Thing} enhanced with a L{GlassBox}.
21 """
22 implements(IWhyNot)
23
24 def tellMeWhyNot(self):
25 """
26 Return a simple message explaining that the user can't reach through
27 the glass box.
28 """
29 return "You can't reach through the glass box."
30
31
32
33class _ObstructedByGlass(object):
34 """
35 This is an annotation on a link between two objects which represents a
36 physical obstruction between them. It is used to annotate between a
37 L{GlassBox} and its contents, so you can see them without reaching them.
38 """
39 implements(IObstruction)
40
41 def whyNot(self):
42 """
43 @return: an object which explains why you can't reach through the glass
44 box.
45 """
46 return _CantReachThroughGlassBox()
47
48
49
50class GlassBox(Item, Enhancement):
51 """
52 L{GlassBox} is an L{Enhancement} which modifies a container such that it is
53 contained.
54 """
55
56 powerupInterfaces = (ILinkContributor,)
57
58 thing = reference()
59
60 def links(self):
61 """
62 If the container attached to this L{GlassBox}'s C{thing} is closed,
63 yield its list of contents with each link annotated with
64 L{_ObstructedByGlass}, indicating that the object cannot be reached.
65 """
66 container = IContainer(self.thing)
67 if container.closed:
68 for content in container.getContents():
69 link = Link(self.thing.idea, content.idea)
70 link.annotate([_ObstructedByGlass(),
71 ContainmentRelationship(container)])
72 yield link
073
=== 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-09-16 20:42:26 +0000
@@ -242,7 +242,7 @@
242 character = random.choice(japanese.hiragana.keys())242 character = random.choice(japanese.hiragana.keys())
243 self._currentChallenge = character243 self._currentChallenge = character
244 actor = self._actor()244 actor = self._actor()
245 action.Say().do(actor.thing, None, character)245 action.Say().do(actor, None, character)
246246
247247
248248
249249
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,42 @@
1# -*- test-case-name: examplegame.test.test_squeaky -*-
2
3"""
4This module implements an L{ILinkAnnotator} which causes an object to squeak
5when it is moved. It should serve as a simple example for overriding what
6happens when an action is executed (in this case, 'take' and 'drop').
7"""
8
9from zope.interface import implements
10
11from axiom.item import Item
12from axiom.attributes import reference
13
14from imaginary.iimaginary import IMovementRestriction, IConcept
15from imaginary.events import Success
16from imaginary.enhancement import Enhancement
17from imaginary.objects import Thing
18
19
20class Squeaker(Item, Enhancement):
21 """
22 This is an L{Enhancement} which, when installed on a L{Thing}, causes that
23 L{Thing} to squeak when you pick it up.
24 """
25
26 implements(IMovementRestriction)
27
28 powerupInterfaces = [IMovementRestriction]
29
30 thing = reference(allowNone=False,
31 whenDeleted=reference.CASCADE,
32 reftype=Thing)
33
34
35 def movementImminent(self, movee, destination):
36 """
37 The object enhanced by this L{Squeaker} is about to move - emit a
38 L{Success} event which describes its squeak.
39 """
40 Success(otherMessage=(IConcept(self.thing).capitalizeConcept(),
41 " emits a faint squeak."),
42 location=self.thing.location).broadcast()
043
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,107 @@
1
2"""
3This module contains tests for the examplegame.furniture module.
4"""
5
6from twisted.trial.unittest import TestCase
7
8from imaginary.test.commandutils import CommandTestCaseMixin, E
9
10from imaginary.objects import Thing, Container, Exit
11from examplegame.furniture import Chair
12
13class SitAndStandTests(CommandTestCaseMixin, TestCase):
14 """
15 Tests for the 'sit' and 'stand' actions.
16 """
17
18 def setUp(self):
19 """
20 Create a room, with a dude in it, and a chair he can sit in.
21 """
22 CommandTestCaseMixin.setUp(self)
23 self.chairThing = Thing(store=self.store, name=u"chair")
24 self.chairThing.moveTo(self.location)
25 self.chair = Chair.createFor(self.chairThing)
26
27
28 def test_sitDown(self):
29 """
30 Sitting in a chair should move your location to that chair.
31 """
32 self.assertCommandOutput(
33 "sit chair",
34 ["You sit in the chair."],
35 ["Test Player sits in the chair."])
36 self.assertEquals(self.player.location, self.chair.thing)
37
38
39 def test_standWhenStanding(self):
40 """
41 You can't stand up - you're already standing up.
42 """
43 self.assertCommandOutput(
44 "stand up",
45 ["You're already standing."])
46
47
48 def test_standWhenSitting(self):
49 """
50 If a player stands up when sitting in a chair, they should be seen to
51 stand up, and they should be placed back into the room where the chair
52 is located.
53 """
54 self.test_sitDown()
55 self.assertCommandOutput(
56 "stand up",
57 ["You stand up."],
58 ["Test Player stands up."])
59 self.assertEquals(self.player.location, self.location)
60
61
62 def test_takeWhenSitting(self):
63 """
64 When a player is seated, they should still be able to take objects on
65 the floor around them.
66 """
67 self.test_sitDown()
68 self.ball = Thing(store=self.store, name=u'ball')
69 self.ball.moveTo(self.location)
70 self.assertCommandOutput(
71 "take ball",
72 ["You take a ball."],
73 ["Test Player takes a ball."])
74
75
76 def test_moveWhenSitting(self):
77 """
78 A player who is sitting shouldn't be able to move without standing up
79 first.
80 """
81 self.test_sitDown()
82 otherRoom = Thing(store=self.store, name=u'elsewhere')
83 Container.createFor(otherRoom, capacity=1000)
84 Exit.link(self.location, otherRoom, u'north')
85 self.assertCommandOutput(
86 "go north",
87 ["You can't do that while sitting down."])
88 self.assertCommandOutput(
89 "go south",
90 ["You can't go that way."])
91
92
93 def test_lookWhenSitting(self):
94 """
95 Looking around when sitting should display the description of the room.
96 """
97 self.test_sitDown()
98 self.assertCommandOutput(
99 "look",
100 # I'd like to add ', in the chair' to this test, but there's
101 # currently no way to modify the name of the object being looked
102 # at.
103 [E("[ Test Location ]"),
104 "Location for testing.",
105 "Observer Player and a chair"])
106
107
0108
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,95 @@
1
2"""
3Tests for L{examplegame.glass}
4"""
5
6from twisted.trial.unittest import TestCase
7
8from imaginary.test.commandutils import CommandTestCaseMixin, E
9
10from imaginary.objects import Thing, Container
11
12from examplegame.glass import GlassBox
13
14class GlassBoxTests(CommandTestCaseMixin, TestCase):
15 """
16 Tests for L{GlassBox}
17 """
18
19 def setUp(self):
20 """
21 Create a room with a L{GlassBox} in it, which itself contains a ball.
22 """
23 CommandTestCaseMixin.setUp(self)
24 self.box = Thing(store=self.store, name=u'box',
25 description=u'The system under test.')
26 self.ball = Thing(store=self.store, name=u'ball',
27 description=u'an interesting object')
28 self.container = Container.createFor(self.box)
29 GlassBox.createFor(self.box)
30 self.ball.moveTo(self.box)
31 self.box.moveTo(self.location)
32 self.container.closed = True
33
34
35 def test_lookThrough(self):
36 """
37 You can see items within a glass box by looking at them directly.
38 """
39 self.assertCommandOutput(
40 "look at ball",
41 [E("[ ball ]"),
42 "an interesting object"])
43
44
45 def test_lookAt(self):
46 """
47 You can see the contents within a glass box by looking at the box.
48 """
49 self.assertCommandOutput(
50 "look at box",
51 [E("[ box ]"),
52 "The system under test.",
53 "a ball"])
54
55
56 def test_take(self):
57 """
58 You can't take items within a glass box.
59 """
60 self.assertCommandOutput(
61 "get ball",
62 ["You can't reach through the glass box."])
63
64
65 def test_openTake(self):
66 """
67 Taking items from a glass box should work if it's open.
68 """
69 self.container.closed = False
70 self.assertCommandOutput(
71 "get ball",
72 ["You take a ball."],
73 ["Test Player takes a ball."])
74
75
76 def test_put(self):
77 """
78 You can't put items into a glass box.
79 """
80 self.container.closed = False
81 self.ball.moveTo(self.location)
82 self.container.closed = True
83 self.assertCommandOutput(
84 "put ball in box",
85 ["The box is closed."])
86
87
88 def test_whyNot(self):
89 """
90 A regression test; there was a bug where glass boxes would interfere
91 with normal target-acquisition error reporting.
92 """
93 self.assertCommandOutput(
94 "get foobar",
95 ["Nothing like that around here."])
096
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,51 @@
1
2from twisted.trial.unittest import TestCase
3
4from imaginary.test.commandutils import CommandTestCaseMixin
5
6from imaginary.objects import Thing, Container
7
8from examplegame.squeaky import Squeaker
9
10class SqueakTest(CommandTestCaseMixin, TestCase):
11 """
12 Squeak Test.
13 """
14
15 def setUp(self):
16 """
17 Set Up.
18 """
19 CommandTestCaseMixin.setUp(self)
20 self.squeaker = Thing(store=self.store, name=u"squeaker")
21 self.squeaker.moveTo(self.location)
22 self.squeakification = Squeaker.createFor(self.squeaker)
23
24
25 def test_itSqueaks(self):
26 """
27 Picking up a squeaky thing makes it emit a squeak.
28 """
29 self.assertCommandOutput(
30 "take squeaker",
31 ["You take a squeaker.",
32 "A squeaker emits a faint squeak."],
33 ["Test Player takes a squeaker.",
34 "A squeaker emits a faint squeak."])
35
36
37 def test_squeakyContainer(self):
38 """
39 If a container is squeaky, that shouldn't interfere with its function
40 as a container. (i.e. let's make sure that links keep working even
41 though we're using an annotator here.)
42 """
43 cont = Container.createFor(self.squeaker)
44
45 mcguffin = Thing(store=self.store, name=u"mcguffin")
46 mcguffin.moveTo(cont)
47
48 self.assertCommandOutput(
49 "take mcguffin from squeaker",
50 ["You take a mcguffin from the squeaker."],
51 ["Test Player takes a mcguffin from the squeaker."])
052
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,96 @@
1
2from twisted.trial.unittest import TestCase
3
4from imaginary.test.commandutils import CommandTestCaseMixin, E
5
6from imaginary.objects import Thing, Container, Exit
7from imaginary.garments import Garment
8
9from examplegame.furniture import Chair
10from examplegame.tether import Tether
11
12class TetherTest(CommandTestCaseMixin, TestCase):
13 """
14 A test for tethering an item to its location, such that a player who picks
15 it up can't leave until they drop it.
16 """
17
18 def setUp(self):
19 """
20 Tether a ball to the room.
21 """
22 CommandTestCaseMixin.setUp(self)
23 self.ball = Thing(store=self.store, name=u'ball')
24 self.ball.moveTo(self.location)
25 self.tether = Tether.createFor(self.ball, to=self.location)
26 self.otherPlace = Thing(store=self.store, name=u'elsewhere')
27 Container.createFor(self.otherPlace, capacity=1000)
28 Exit.link(self.location, self.otherPlace, u'north')
29
30
31 def test_takeAndLeave(self):
32 """
33 You can't leave the room if you're holding the ball that's tied to it.
34 """
35 self.assertCommandOutput(
36 "take ball",
37 ["You take a ball."],
38 ["Test Player takes a ball."])
39 self.assertCommandOutput(
40 "go north",
41 ["You can't move, you're still holding a ball."],
42 ["Test Player struggles with a ball."])
43 self.assertCommandOutput(
44 "drop ball",
45 ["You drop the ball."],
46 ["Test Player drops a ball."])
47 self.assertCommandOutput(
48 "go north",
49 [E("[ elsewhere ]"),
50 E("( south )"),
51 ""],
52 ["Test Player leaves north."])
53
54
55 def test_allTiedUp(self):
56 """
57 If you're tied to a chair, you can't leave.
58 """
59 chairThing = Thing(store=self.store, name=u'chair')
60 chairThing.moveTo(self.location)
61 chair = Chair.createFor(chairThing)
62 self.assertCommandOutput("sit chair",
63 ["You sit in the chair."],
64 ["Test Player sits in the chair."])
65 Tether.createFor(self.player, to=chairThing)
66 self.assertCommandOutput(
67 "stand up",
68 ["You can't move, you're tied to a chair."],
69 ["Test Player struggles."])
70
71
72 def test_tetheredClothing(self):
73 """
74 Clothing that is tethered will also prevent movement if you wear it.
75
76 This isn't just simply a test for clothing; it's an example of
77 integrating with a foreign system which doesn't know about tethering,
78 but can move objects itself.
79
80 Tethering should I{not} have any custom logic related to clothing to
81 make this test pass; if it does get custom clothing code for some
82 reason, more tests should be added to deal with other systems that do
83 not take tethering into account (and vice versa).
84 """
85 Garment.createFor(self.ball, garmentDescription=u"A lovely ball.",
86 garmentSlots=[u"head"])
87 self.assertCommandOutput(
88 "wear ball",
89 ["You put on the ball."],
90 ["Test Player puts on a ball."])
91 self.assertCommandOutput(
92 "go north",
93 ["You can't move, you're still holding a ball."],
94 ["Test Player struggles with a ball."])
95
96
097
=== 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-09-16 20:42:26 +0000
@@ -0,0 +1,121 @@
1# -*- test-case-name: examplegame.test.test_tether -*-
2
3"""
4A simplistic implementation of tethering, which demonstrates how to prevent
5someone from moving around.
6
7This implementation is somewhat limited, as it assumes that tethered objects
8can only be located in players' inventories and on the ground. It also makes
9several assumptions about who is actually doing the moving in moveTo; in order
10to be really correct, the implementation of movement needs to relay more
11information about what is moving and how.
12"""
13
14from zope.interface import implements
15
16from axiom.item import Item
17from axiom.attributes import reference
18
19from imaginary.iimaginary import IMovementRestriction, IActor
20from imaginary.eimaginary import ActionFailure
21from imaginary.events import ThatDoesntWork
22from imaginary.enhancement import Enhancement
23from imaginary.objects import Thing
24
25
26class Tether(Item, Enhancement):
27 """
28 I am a force that binds two objects together.
29
30 Right now this force isn't symmetric; the idea is that the thing that we
31 are tethered 'to' is immovable for some other reason. This is why we're in
32 the example rather than a real robust piece of game-library functionality
33 in imaginary proper.
34
35 The C{thing} that we are installed on is prevented from moving more than a
36 certain distance away from the thing it is tethered C{to}.
37
38 This is accomplished by preventing movement of the object's container;
39 i.e. if you pick up a ball that is tied to the ground, you can't move until
40 you drop it.
41 """
42
43 thing = reference(reftype=Thing,
44 whenDeleted=reference.CASCADE,
45 allowNone=False)
46
47 # XXX 'thing' and 'to' should be treated more consistently, or at least the
48 # differences between them explained officially.
49 to = reference(reftype=Thing,
50 whenDeleted=reference.CASCADE,
51 allowNone=False)
52
53 implements(IMovementRestriction)
54
55 powerupInterfaces = [IMovementRestriction]
56
57 def movementImminent(self, movee, destination):
58 """
59 The object which is tethered is trying to move somewhere. If it has an
60 IActor, assume that it's a player trying to move on its own, and emit
61 an appropriate message.
62
63 Otherwise, assume that it is moving *to* an actor, and install a
64 L{MovementBlocker} on that actor.
65 """
66 # There isn't enough information provided to moveTo just yet; we need
67 # to know who is doing the moving. In the meanwhile, if you have an
68 # actor, we'll assume you're a player.
69 if IActor(movee, None) is not None:
70 raise ActionFailure(
71 ThatDoesntWork(
72 actor=self.thing,
73 actorMessage=[u"You can't move, you're tied to ",
74 self.to,
75 "."],
76 otherMessage=[self.thing, u' struggles.']))
77 MovementBlocker.destroyFor(self.thing.location)
78 if self.to != destination:
79 MovementBlocker.createFor(destination, tether=self)
80
81 return False
82
83
84class MovementBlocker(Item, Enhancement):
85 """
86 A L{MovementBlocker} is an L{Enhancement} which prevents the movement of a
87 player holding a tethered object.
88 """
89 implements(IMovementRestriction)
90
91 powerupInterfaces = [IMovementRestriction]
92
93 thing = reference(
94 doc="""
95 The L{Thing} whose movement is blocked.
96 """, reftype=Thing, allowNone=False,
97 whenDeleted=reference.CASCADE)
98
99 tether = reference(
100 doc="""
101 The L{Tether} ultimely responsible for blocking movement.
102 """,
103 reftype=Tether, allowNone=False,
104 whenDeleted=reference.CASCADE)
105
106
107 def movementImminent(self, movee, destination):
108 """
109 The player this blocker is installed on is trying to move. Assume that
110 they are trying to move themselves (via a 'go' action) and prevent it
111 by raising an L{ActionFailure} with an appropriate error message for
112 the player.
113 """
114 raise ActionFailure(
115 ThatDoesntWork(
116 actor=self.thing,
117 actorMessage=
118 [u"You can't move, you're still holding ",
119 self.tether.thing,u'.'],
120 otherMessage=
121 [self.thing, u' struggles with ', self.tether.thing,u'.']))
0122
=== added file 'Imaginary/TODO.txt'
--- Imaginary/TODO.txt 1970-01-01 00:00:00 +0000
+++ Imaginary/TODO.txt 2011-09-16 20:42:26 +0000
@@ -0,0 +1,258 @@
1
2(This list of tasks is for the #2824 branch, it shouldn't be merged to trunk.)
3
4self-review:
5
6 (Since this is already a large branch, I am going to clean up the obvious
7 stuff before putting it into review for someone else)
8
9 * test coverage:
10
11 * there need to be direct tests for imaginary.idea. This is obviously
12 the biggest issue.
13
14 * lifecycle testing (testing that if these are not identical on
15 subsequent method calls, bad stuff happens):
16 * Thing.idea
17 * Exit.exitIdea
18 * Containment._exitIdea
19 * Containment._entranceIdea
20
21 * direct tests for IContainmentRelationship and
22 ContainmentRelationship.
23
24-------------------------------------------------------------------------------
25
26I think everything below this line is probably for a separate branch, but I
27need to clean it up and file some additional tickets before deleting it.
28
29-------------------------------------------------------------------------------
30
31General high-level structural issues:
32
33 * the "sensory" system could address multiple issues. It would be
34 worthwhile to have a rudiment of it, if only to remove duplication
35 between "findProviders" and "search" and the thing that computes the list
36 for ExpressSurroundings, so we can have a consistent way to construct
37 that thing.
38
39 * movement restrictions want to raise ActionFailure for pretty error
40 handling, but don't know who the actor is. This should be dealt with in
41 more than one way:
42
43 * There should be an error-handling path which allows actions to fail
44 with feedback only to the actor. "You can't do that because..."
45
46 * moveTo should receive more information, specifically the actor who
47 initiated the movement. There should probably be a TON of extra
48 information, like the number of joules used in support of the
49 movement etc.
50
51 * moveTo should not be raising ActionFailure directly. There should be
52 a conventional exception type to raise specifically for saying "not
53 movable", and its callers should catch it.
54
55 * Navigators and retrievers need to be reconciled. Specifically, CanSee
56 and Visibility need to be smashed into the same object somehow.
57
58Some use-cases that should be implemented / tested:
59
60 * container travel:
61
62 * I should *not* be able to get out of a closed container that I'm in.
63
64 * Bugfix, sort of: If I'm inside a closed container, I should be able
65 to see and access the stuff around me.
66
67 * containment fixes
68
69 * the obtain() call used to pass to ExpressSurroundings is wrong; it's
70 asking "what can you, the location, see from here"; whereas it should
71 be asking "what can you, the player, see in this location". If it
72 were to be really smart / correct, it would be passing an initial
73 path to obtain(), since we know the relationship between these two
74 things. Dumb implementation of that could simply re-evaluate the
75 path up to that point and ignore all the links in it to validate that
76 it's a valid path.
77
78 * ranged actions
79
80 * 'get coolant rod with claw'
81
82 * 'shoot target'
83
84 * A shooting range. There are two rooms: one with targets in it,
85 one with the player and their gun.
86
87 * 'look' and 'shoot' should work, although ideally 'go' should
88 not: it should say something like "that's live fire over
89 there!"
90
91 * the gun should be implicitly located, since it's the only valid
92 tool.
93
94 * I should not be able to see *or* reach objects that are around corners.
95
96 * I should be able to exit a darkened room, perhaps with some modifiers.
97 Some effects that some games might want, should these be default?:
98
99 * Stumbling around in the dark will occasionally send you in a random
100 direction.
101
102 * You can always exit in the direction of an exit where the target of
103 the exit is itself lit.
104
105 * Definitely some games will want this, some not: You are likely to be
106 eaten by a lurking grue.
107
108 * I shouldn't be able to see an event that transpires in a dark room.
109
110 * I should be able to pick up a shirt in a dark room if I have
111 something that lets only me see in the dark (night-vision goggles?)
112 but others should not be able to see that.
113
114Some restructuring:
115
116 * What paramters should findProviders and search take? We're starting with
117 'distance' and 'interface'. Let's enumerate some verbs:
118
119 * take: something you can physically reach without walking
120
121 * drop: something you are holding
122
123 * wear: something you are holding
124
125 * sit: something in the room, something you are *NOT* holding
126
127 * stand: something *which is your location*. (something which is
128 holding you?)
129
130 * unwear/remove: something you are *wearing*? something you're holding
131 would be good enough.
132
133 * look: something you can see
134
135 * we need a 'near look' and a 'far look'. When the user types
136 'look' they only want to see items in their immediate vicinity,
137 but 'look around' or 'look north' or whatever should still allow
138 them to see things that are close enough.
139
140 * shoot: something you can see? If there's something in a glass box,
141 you should be able to shoot it.
142
143 * eat: something you're holding
144
145 defaults:
146
147 * actor -> "you" (still not sure what this means)
148
149 a thought: the self-link of the Idea starting an obtain()
150 search should apply annotationsForIncoming, but it should not
151 apply annotationsForOutgoing. Then the actor idea can always
152 apply an annotationsForOutgoing that says "this isn't you any
153 more"? then you can have a (retriever? sense?)
154
155 * target -> something you're holding
156
157 * tool -> something you're holding
158
159 None of these verbs really know anything about distance, except
160 possibly "shoot" - which really cares about the distance to the target
161 *in the hit-probability calculation*; i.e. it wants to know about the
162 path during the execution of the action, not during the location of the
163 target.
164
165 * Rather than an ad-hoc construction of a Navigator and Retriever in
166 findProviders() and search(), there should be a (pluggable, eventually:
167 this is how one would implement night-vision goggles) way to get objects
168 representing "sensory" inputs. (although "reachability" is a bit of a
169 stretch for a sense, it does make sense as 'touch'.) Practically
170 speaking, that means the logic in the "CanSee" retriever ought to be in
171 Visibility somehow. Options for implementing this:
172
173 * smash the Retriever and the Navigator into one object, and compose
174 them. Then build each one (Visibility, Reachability, etc) by
175 wrapping around the other. Named goes around the outside in
176 search().
177
178 * add a convenient API for getCandelas and friends to use. getCandelas
179 needs to not take lighting into account. If we have an reification of
180 'sensory' objects, then we can construct a custom one for this query.
181
182 * Make an "_Idealized" (or something) base class which implements an Item
183 with an 'idea' attribute so that we can manage it on L{Exit}, L{Thing}
184 and perhaps something else.
185
186 * The special-cased just-to-self path in Idea.obtain sucks, because it's
187 inconsistent. There's no 'shouldKeepGoing' call for the link that can
188 prevent it from yielding. If we move the responsibility for doing
189 lighting back into the navigator (where, really, it belongs: Visibility
190 is supposed to be a navigator, right?) this means the navigator can't
191 prevent you from accessing an actor aspect of yourself.
192
193 Other cases which will use this same system:
194
195 * Restraints. Let's say there's a chair that you sit in which
196 restrains you. It needs a proxy which can prevent you from
197 reaching your actor interface. This is much like darkness,
198 except it's going to want to *not* restrict visual events or
199 'examine'.
200
201 * Suppression / Anti-Magic Field. Restraints and darkness both
202 prevent a blanket "everything" with a few possible exceptions;
203 you might also want an effect which suspends only specific
204 actions / interfaces.
205
206 * Blindfold.
207
208 * The two systems that all of these seem to touch are 'vision' and
209 'reachability'. So we need link types that say "you can't see beyond
210 this point" and "you can't reach beyond this point". That's
211 "Visibility" and "Proximity", as implemented already, except
212 Visibility can't say "don't keep going" for darkness. It has to have
213 a way to say "you can't see stuff that is immediately on the other
214 side of this link, but if you go through another link that *is*
215 visible, then you can see stuff". i.e. the case where you can't see
216 yourself, or any of your inventory, but you *can* see another room.
217
218 * (test for) additional L{ILinkContributor} powerups being added, removed
219 (i.e. making sure that the 'idea' in question is the same).
220
221----
222
223Rules for lighting:
224
225If a room is dark, all objects in that room should have the darkness rule
226applied to them, regardless (?) of how they are discovered.
227
228Right now this is enforced entirely by the CanSee retriever and the
229_PossiblyDark link annotation.
230
231However, this is overkill. The link annotation is not used at any point during
232traversal. Only the final annotation in any given path is used, and even that
233annotation is discarded if there is an effective "null annotation"; a link to a
234location with no lighting. The way this is detected is (I think) suspect,
235because it doesn't use the path (path.of can't detect links which ).
236
237So we could implement the same rules by not annotating anything, and applying
238proxies only at the final link, inspecting its location, rather than applying
239an elaborate series of proxies as we go.
240
241Problems with the current implementation of lighting:
242
243 * applyLighting needs to know the interface that's being queried for so
244 that it can return a _DarkLocationProxy. There's no good way to
245 determine this right now, because the way we know what interface is being
246 asked for has to do with the Retriever, which (in theory?) could be
247 something arbitrary. We could make 'interface' a required attribute of
248 the navigator. That seems a bit weird, since one could (theoretically)
249 want to be able to retrieve things by arbitrary sets of rules, but maybe
250 that's not a useful use-case?
251
252 * Limitations: these can be deferred for a different branch since I think
253 they're mostly just a SMOP, but worth thinking about:
254
255 * the proxy you get for a darkened object ought to be pluggable, so
256 that descriptions can change depending on light level. This could be
257 a useful dramatic tool.
258
0259
=== modified file 'Imaginary/imaginary/action.py'
--- Imaginary/imaginary/action.py 2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/action.py 2011-09-16 20:42:26 +0000
@@ -15,6 +15,8 @@
15from imaginary import (iimaginary, eimaginary, iterutils, events,15from imaginary import (iimaginary, eimaginary, iterutils, events,
16 objects, text as T, language, pyparsing)16 objects, text as T, language, pyparsing)
17from imaginary.world import ImaginaryWorld17from imaginary.world import ImaginaryWorld
18from imaginary.idea import (
19 CanSee, Proximity, ProviderOf, Named, Traversability)
1820
19## Hacks because pyparsing doesn't have fantastic unicode support21## Hacks because pyparsing doesn't have fantastic unicode support
20_quoteRemovingQuotedString = pyparsing.quotedString.copy()22_quoteRemovingQuotedString = pyparsing.quotedString.copy()
@@ -44,9 +46,12 @@
4446
4547
46 def parse(self, player, line):48 def parse(self, player, line):
47 for cls in self.actions:49 """
50 Parse an action.
51 """
52 for eachActionType in self.actions:
48 try:53 try:
49 match = cls.match(player, line)54 match = eachActionType.match(player, line)
50 except pyparsing.ParseException:55 except pyparsing.ParseException:
51 pass56 pass
52 else:57 else:
@@ -56,85 +61,163 @@
56 if isinstance(v, pyparsing.ParseResults):61 if isinstance(v, pyparsing.ParseResults):
57 match[k] = v[0]62 match[k] = v[0]
5863
59 return cls().runEventTransaction(player, line, match)64 return eachActionType().runEventTransaction(player, line, match)
60 return defer.fail(eimaginary.NoSuchCommand(line))65 return defer.fail(eimaginary.NoSuchCommand(line))
6166
6267
6368
64class Action(object):69class Action(object):
70 """
71 An L{Action} represents an intention of a player to do something.
72 """
65 __metaclass__ = _ActionType73 __metaclass__ = _ActionType
66 infrastructure = True74 infrastructure = True
6775
76 actorInterface = iimaginary.IActor
6877
69 def runEventTransaction(self, player, line, match):78 def runEventTransaction(self, player, line, match):
70 """79 """
71 Take a player, line, and dictionary of parse results and execute the80 Take a player, input, and dictionary of parse results, resolve those
72 actual Action implementation.81 parse results into implementations of appropriate interfaces in the
7382 game world, and execute the actual Action implementation (contained in
74 @param player: A provider of C{self.actorInterface}83 the 'do' method) in an event transaction.
84
85 This is the top level of action invocation.
86
87 @param player: A L{Thing} representing the actor's body.
88
75 @param line: A unicode string containing the original input89 @param line: A unicode string containing the original input
90
76 @param match: A dictionary containing some parse results to pass91 @param match: A dictionary containing some parse results to pass
77 through to the C{run} method.92 through to this L{Action}'s C{do} method as keyword arguments.
7893
79 """94 @raise eimaginary.AmbiguousArgument: if multiple valid targets are
80 events.runEventTransaction(95 found for an argument.
81 player.store, self.run, player, line, **match)96 """
8297 def thunk():
8398 begin = time.time()
99 try:
100 actor = self.actorInterface(player)
101 for (k, v) in match.items():
102 try:
103 objs = self.resolve(player, k, v)
104 except NotImplementedError:
105 pass
106 else:
107 if len(objs) == 1:
108 match[k] = objs[0]
109 elif len(objs) == 0:
110 self.cantFind(player, actor, k, v)
111 else:
112 raise eimaginary.AmbiguousArgument(self, k, v, objs)
113 return self.do(actor, line, **match)
114 finally:
115 end = time.time()
116 log.msg(interface=iaxiom.IStatEvent,
117 stat_actionDuration=end - begin,
118 stat_actionExecuted=1)
119 events.runEventTransaction(player.store, thunk)
120
121
122 def cantFind(self, player, actor, slot, name):
123 """
124 This hook is invoked when a target cannot be found.
125
126 This will delegate to a method like C{self.cantFind_<slot>(actor,
127 name)} if one exists, to determine the error message to show to the
128 actor. It will then raise L{eimaginary.ActionFailure} to stop
129 processing of this action.
130
131 @param player: The L{Thing} doing the searching.
132
133 @type player: L{IThing}
134
135 @param actor: The L{IActor} doing the searching.
136
137 @type actor: L{IActor}
138
139 @param slot: The slot in question.
140
141 @type slot: C{str}
142
143 @param name: The name of the object being searched for.
144
145 @type name: C{unicode}
146
147 @raise eimaginary.ActionFailure: always.
148 """
149 func = getattr(self, "cantFind_"+slot, None)
150 if func:
151 msg = func(actor, name)
152 else:
153 msg = "Who's that?"
154 raise eimaginary.ActionFailure(
155 events.ThatDoesntWork(
156 actorMessage=msg,
157 actor=player))
158
159
160 @classmethod
84 def match(cls, player, line):161 def match(cls, player, line):
162 """
163 @return: a list of 2-tuples of all the results of parsing the given
164 C{line} using this L{Action} type's pyparsing C{expr} attribute, or
165 None if the expression does not match the given line.
166
167 @param line: a line of user input to be interpreted as an action.
168
169 @see: L{imaginary.pyparsing}
170 """
85 return cls.expr.parseString(line)171 return cls.expr.parseString(line)
86 match = classmethod(match)172
87173
88174 def do(self, player, line, **slots):
89 def run(self, player, line, **kw):175 """
90 begin = time.time()176 Subclasses override this method to actually perform the action.
91 try:177
92 return self._reallyRun(player, line, kw)178 This method is performed in an event transaction, by 'run'.
93 finally:179
94 end = time.time()180 NB: The suggested implementation strategy for a 'do' method is to do
95 log.msg(181 action-specific setup but then delegate the bulk of the actual logic to
96 interface=iaxiom.IStatEvent,182 a method on a target/tool interface. The 'do' method's job is to
97 stat_actionDuration=end - begin,183 select the appropriate methods to invoke.
98 stat_actionExecuted=1,184
99 )185 @param player: a provider of this L{Action}'s C{actorInterface}.
100186
101187 @param line: the input string that created this action.
102 def _reallyRun(self, player, line, kw):188
103 for (k, v) in kw.items():189 @param slots: The results of calling C{self.resolve} on each parsing
104 try:190 result (described by a setResultsName in C{self.expr}).
105 objs = self.resolve(player, k, v)191 """
106 except NotImplementedError:192 raise NotImplementedError("'do' method not implemented")
107 pass
108 else:
109 if len(objs) != 1:
110 raise eimaginary.AmbiguousArgument(self, k, v, objs)
111 else:
112 kw[k] = objs[0]
113 return self.do(player, line, **kw)
114193
115194
116 def resolve(self, player, name, value):195 def resolve(self, player, name, value):
117 raise NotImplementedError("Don't know how to resolve %r (%r)" % (name, value))196 """
118197 Resolve a given parsed value to a valid action parameter by calling a
119198 'resolve_<name>' method on this L{Action} with the given C{player} and
120199 C{value}.
121class NoTargetAction(Action):200
122 """201 @param player: the L{Thing} attempting to perform this action.
123 @cvar actorInterface: Interface that the actor must provide.202
124 """203 @type player: L{Thing}
125 infrastructure = True204
126205 @param name: the name of the slot being filled. For example, 'target'.
127 actorInterface = iimaginary.IActor206
128207 @type name: L{str}
129 def match(cls, player, line):208
130 actor = cls.actorInterface(player, None)209 @param value: a string representing the value that was parsed. For
131 if actor is not None:210 example, if the user typed 'get fish', this would be 'fish'.
132 return super(NoTargetAction, cls).match(player, line)211
133 return None212 @return: a value which will be passed as the 'name' parameter to this
134 match = classmethod(match)213 L{Action}'s C{do} method.
135214 """
136 def run(self, player, line, **kw):215 resolver = getattr(self, 'resolve_%s' % (name,), None)
137 return super(NoTargetAction, self).run(self.actorInterface(player), line, **kw)216 if resolver is None:
217 raise NotImplementedError(
218 "Don't know how to resolve %r (%r)" % (name, value))
219 return resolver(player, value)
220
138221
139222
140def targetString(name):223def targetString(name):
@@ -144,7 +227,7 @@
144227
145228
146229
147class TargetAction(NoTargetAction):230class TargetAction(Action):
148 """231 """
149 Subclass L{TargetAction} to implement an action that acts on a target, like232 Subclass L{TargetAction} to implement an action that acts on a target, like
150 'take foo' or 'eat foo' where 'foo' is the target.233 'take foo' or 'eat foo' where 'foo' is the target.
@@ -160,10 +243,9 @@
160 def targetRadius(self, player):243 def targetRadius(self, player):
161 return 2244 return 2
162245
163 def resolve(self, player, k, v):246 def resolve_target(self, player, targetName):
164 if k == "target":247 return _getIt(player, targetName,
165 return list(player.thing.search(self.targetRadius(player), self.targetInterface, v))248 self.targetInterface, self.targetRadius(player))
166 return super(TargetAction, self).resolve(player, k, v)
167249
168250
169251
@@ -183,21 +265,29 @@
183 def toolRadius(self, player):265 def toolRadius(self, player):
184 return 2266 return 2
185267
186 def resolve(self, player, k, v):268 def resolve_tool(self, player, toolName):
187 if k == "tool":269 return _getIt(player, toolName,
188 return list(player.thing.search(270 self.toolInterface, self.toolRadius(player))
189 self.toolRadius(player), self.toolInterface, v))271
190 return super(ToolAction, self).resolve(player, k, v)272
191273
192274def _getIt(player, thingName, iface, radius):
193275 return list(player.search(radius, iface, thingName))
194class LookAround(NoTargetAction):276
277
278
279class LookAround(Action):
195 actionName = "look"280 actionName = "look"
196 expr = pyparsing.Literal("look") + pyparsing.StringEnd()281 expr = pyparsing.Literal("look") + pyparsing.StringEnd()
197282
198 def do(self, player, line):283 def do(self, player, line):
284 ultimateLocation = player.thing.location
285 while ultimateLocation.location is not None:
286 ultimateLocation = ultimateLocation.location
199 for visible in player.thing.findProviders(iimaginary.IVisible, 1):287 for visible in player.thing.findProviders(iimaginary.IVisible, 1):
200 if player.thing.location is visible.thing:288 # XXX what if my location is furniture? I want to see '( Foo,
289 # sitting in the Bar )', not '( Bar )'.
290 if visible.isViewOf(ultimateLocation):
201 concept = visible.visualize()291 concept = visible.visualize()
202 break292 break
203 else:293 else:
@@ -217,7 +307,35 @@
217307
218 targetInterface = iimaginary.IVisible308 targetInterface = iimaginary.IVisible
219309
220 def targetNotAvailable(self, player, exc):310 def resolve_target(self, player, targetName):
311 """
312 Resolve the target to look at by looking for a named, visible object in
313 a proximity of 3 meters from the player.
314
315 @param player: The player doing the looking.
316
317 @type player: L{IThing}
318
319 @param targetName: The name of the object we are looking for.
320
321 @type targetName: C{unicode}
322
323 @return: A list of visible objects.
324
325 @rtype: C{list} of L{IVisible}
326
327 @raise eimaginary.ActionFailure: with an appropriate message if the
328 target cannot be resolved for an identifiable reason. See
329 L{imaginary.objects.Thing.obtainOrReportWhyNot} for a description
330 of how such reasons may be identified.
331 """
332 return player.obtainOrReportWhyNot(
333 Proximity(3.0, Named(targetName,
334 CanSee(ProviderOf(iimaginary.IVisible)),
335 player)))
336
337
338 def cantFind_target(self, player, name):
221 return "You don't see that."339 return "You don't see that."
222340
223 def targetRadius(self, player):341 def targetRadius(self, player):
@@ -238,7 +356,7 @@
238356
239357
240358
241class Illuminate(NoTargetAction):359class Illuminate(Action):
242 """360 """
243 Change the ambient light level at the location of the actor. Since this is361 Change the ambient light level at the location of the actor. Since this is
244 an administrative action that directly manipulates the environment, the362 an administrative action that directly manipulates the environment, the
@@ -488,7 +606,7 @@
488606
489607
490608
491class Equipment(NoTargetAction):609class Equipment(Action):
492 expr = pyparsing.Literal("equipment")610 expr = pyparsing.Literal("equipment")
493611
494 actorInterface = iimaginary.IClothingWearer612 actorInterface = iimaginary.IClothingWearer
@@ -528,9 +646,9 @@
528 pyparsing.White() +646 pyparsing.White() +
529 targetString("tool"))647 targetString("tool"))
530648
531 def targetNotAvailable(self, player, exc):649 def cantFind_target(self, player, targetName):
532 return "Nothing like that around here."650 return "Nothing like that around here."
533 toolNotAvailable = targetNotAvailable651 cantFind_tool = cantFind_target
534652
535 def do(self, player, line, target, tool):653 def do(self, player, line, target, tool):
536 # XXX Make sure target is in tool654 # XXX Make sure target is in tool
@@ -547,7 +665,7 @@
547 toolInterface = iimaginary.IThing665 toolInterface = iimaginary.IThing
548 targetInterface = iimaginary.IContainer666 targetInterface = iimaginary.IContainer
549667
550 def targetNotAvailable(self, player, exc):668 def cantFind_target(self, player, targetName):
551 return "That doesn't work."669 return "That doesn't work."
552670
553 expr = (pyparsing.Literal("put") +671 expr = (pyparsing.Literal("put") +
@@ -609,7 +727,7 @@
609 pyparsing.White() +727 pyparsing.White() +
610 targetString("target"))728 targetString("target"))
611729
612 def targetNotAvailable(self, player, exc):730 def cantFind_target(self, player, targetName):
613 return u"Nothing like that around here."731 return u"Nothing like that around here."
614732
615 def targetRadius(self, player):733 def targetRadius(self, player):
@@ -641,7 +759,7 @@
641 pyparsing.White() +759 pyparsing.White() +
642 targetString("target"))760 targetString("target"))
643761
644 def targetNotAvailable(self, player, exc):762 def cantFind_target(self, player, targetName):
645 return "Nothing like that around here."763 return "Nothing like that around here."
646764
647 def targetRadius(self, player):765 def targetRadius(self, player):
@@ -678,7 +796,7 @@
678796
679797
680798
681class Dig(NoTargetAction):799class Dig(Action):
682 expr = (pyparsing.Literal("dig") +800 expr = (pyparsing.Literal("dig") +
683 pyparsing.White() +801 pyparsing.White() +
684 DIRECTION_LITERAL +802 DIRECTION_LITERAL +
@@ -707,7 +825,7 @@
707825
708826
709827
710class Bury(NoTargetAction):828class Bury(Action):
711 expr = (pyparsing.Literal("bury") +829 expr = (pyparsing.Literal("bury") +
712 pyparsing.White() +830 pyparsing.White() +
713 DIRECTION_LITERAL)831 DIRECTION_LITERAL)
@@ -738,47 +856,59 @@
738856
739857
740858
741class Go(NoTargetAction):859class Go(Action):
742 expr = (pyparsing.Optional(pyparsing.Literal("go") + pyparsing.White()) +860 expr = (
743 DIRECTION_LITERAL)861 DIRECTION_LITERAL |
862 (pyparsing.Literal("go") + pyparsing.White() +
863 targetString("direction")) |
864 (pyparsing.Literal("enter") + pyparsing.White() +
865 targetString("direction")) |
866 (pyparsing.Literal("exit") + pyparsing.White() +
867 targetString("direction")))
868
869 actorInterface = iimaginary.IThing
870
871 def resolve_direction(self, player, directionName):
872 """
873 Identify a direction by having the player search for L{IExit}
874 providers that they can see and reach.
875 """
876 return player.obtainOrReportWhyNot(
877 Proximity(
878 3.0,
879 Traversability(
880 Named(directionName,
881 CanSee(ProviderOf(iimaginary.IExit)), player))))
882
883
884 def cantFind_direction(self, actor, directionName):
885 """
886 Explain to the user that they can't go in a direction that they can't
887 locate.
888 """
889 return u"You can't go that way."
890
744891
745 def do(self, player, line, direction):892 def do(self, player, line, direction):
746 try:893 location = player.location
747 exit = iimaginary.IContainer(player.thing.location).getExitNamed(direction)
748 except KeyError:
749 raise eimaginary.ActionFailure(events.ThatDoesntWork(
750 actor=player.thing,
751 actorMessage=u"You can't go that way."))
752
753 dest = exit.toLocation
754 location = player.thing.location
755894
756 evt = events.Success(895 evt = events.Success(
757 location=location,896 location=location,
758 actor=player.thing,897 actor=player,
759 otherMessage=(player.thing, " leaves ", direction, "."))898 otherMessage=(player, " leaves ", direction.name, "."))
760 evt.broadcast()899 evt.broadcast()
761900
762 if exit.sibling is not None:
763 arriveDirection = exit.sibling.name
764 else:
765 arriveDirection = object.OPPOSITE_DIRECTIONS[exit.name]
766
767 try:901 try:
768 player.thing.moveTo(902 direction.traverse(player)
769 dest,
770 arrivalEventFactory=lambda player: events.MovementArrivalEvent(
771 thing=player,
772 origin=None,
773 direction=arriveDirection))
774 except eimaginary.DoesntFit:903 except eimaginary.DoesntFit:
775 raise eimaginary.ActionFailure(events.ThatDoesntWork(904 raise eimaginary.ActionFailure(events.ThatDoesntWork(
776 actor=player.thing,905 actor=player,
777 actorMessage=language.ExpressString(u"There's no room for you there.")))906 actorMessage=language.ExpressString(
907 u"There's no room for you there.")))
778908
779 # XXX A convention for programmatically invoked actions?909 # This is subtly incorrect: see http://divmod.org/trac/ticket/2917
780 # None as the line?910 lookAroundActor = iimaginary.IActor(player)
781 LookAround().do(player, "look")911 LookAround().do(lookAroundActor, "look")
782912
783913
784914
@@ -789,9 +919,11 @@
789919
790 targetInterface = iimaginary.IActor920 targetInterface = iimaginary.IActor
791921
792 def targetNotAvailable(self, player, exc):922 def cantFind_target(self, player, targetName):
793 for thing in player.search(self.targetRadius(player), iimaginary.IThing, exc.partValue):923 for thing in player.thing.search(self.targetRadius(player),
794 return (language.Noun(thing).nounPhrase().plaintext(player), " cannot be restored.")924 iimaginary.IThing, targetName):
925 return (language.Noun(thing).nounPhrase().plaintext(player),
926 " cannot be restored.")
795 return "Who's that?"927 return "Who's that?"
796928
797 def targetRadius(self, player):929 def targetRadius(self, player):
@@ -831,7 +963,6 @@
831 return 3963 return 3
832964
833 def do(self, player, line, target):965 def do(self, player, line, target):
834 toBroadcast = []
835 if target is player:966 if target is player:
836 raise eimaginary.ActionFailure(967 raise eimaginary.ActionFailure(
837 events.ThatDoesntMakeSense(u"Hit yourself? Stupid.",968 events.ThatDoesntMakeSense(u"Hit yourself? Stupid.",
@@ -866,7 +997,7 @@
866997
867998
868999
869class Say(NoTargetAction):1000class Say(Action):
870 expr = (((pyparsing.Literal("say") + pyparsing.White()) ^1001 expr = (((pyparsing.Literal("say") + pyparsing.White()) ^
871 pyparsing.Literal("'")) +1002 pyparsing.Literal("'")) +
872 pyparsing.restOfLine.setResultsName("text"))1003 pyparsing.restOfLine.setResultsName("text"))
@@ -877,7 +1008,7 @@
8771008
8781009
8791010
880class Emote(NoTargetAction):1011class Emote(Action):
881 expr = (((pyparsing.Literal("emote") + pyparsing.White()) ^1012 expr = (((pyparsing.Literal("emote") + pyparsing.White()) ^
882 pyparsing.Literal(":")) +1013 pyparsing.Literal(":")) +
883 pyparsing.restOfLine.setResultsName("text"))1014 pyparsing.restOfLine.setResultsName("text"))
@@ -890,7 +1021,7 @@
8901021
8911022
8921023
893class Actions(NoTargetAction):1024class Actions(Action):
894 expr = pyparsing.Literal("actions")1025 expr = pyparsing.Literal("actions")
8951026
896 def do(self, player, line):1027 def do(self, player, line):
@@ -903,7 +1034,7 @@
9031034
9041035
9051036
906class Search(NoTargetAction):1037class Search(Action):
907 expr = (pyparsing.Literal("search") +1038 expr = (pyparsing.Literal("search") +
908 targetString("name"))1039 targetString("name"))
9091040
@@ -920,7 +1051,7 @@
9201051
9211052
9221053
923class Score(NoTargetAction):1054class Score(Action):
924 expr = pyparsing.Literal("score")1055 expr = pyparsing.Literal("score")
9251056
926 scoreFormat = (1057 scoreFormat = (
@@ -951,7 +1082,7 @@
9511082
9521083
9531084
954class Who(NoTargetAction):1085class Who(Action):
955 expr = pyparsing.Literal("who")1086 expr = pyparsing.Literal("who")
9561087
957 def do(self, player, line):1088 def do(self, player, line):
@@ -1003,7 +1134,7 @@
10031134
10041135
10051136
1006class Inventory(NoTargetAction):1137class Inventory(Action):
1007 expr = pyparsing.Literal("inventory")1138 expr = pyparsing.Literal("inventory")
10081139
1009 def do(self, player, line):1140 def do(self, player, line):
@@ -1113,7 +1244,7 @@
11131244
11141245
11151246
1116class Help(NoTargetAction):1247class Help(Action):
1117 """1248 """
1118 A command for looking up help files.1249 A command for looking up help files.
11191250
11201251
=== modified file 'Imaginary/imaginary/creation.py'
--- Imaginary/imaginary/creation.py 2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/creation.py 2011-09-16 20:42:26 +0000
@@ -16,7 +16,7 @@
16from imaginary.iimaginary import IThingType16from imaginary.iimaginary import IThingType
17from imaginary.eimaginary import ActionFailure, DoesntFit17from imaginary.eimaginary import ActionFailure, DoesntFit
1818
19from imaginary.action import NoTargetAction, insufficientSpace19from imaginary.action import Action, insufficientSpace
20from imaginary.action import targetString20from imaginary.action import targetString
2121
22from imaginary.pyparsing import Literal, White, Optional, restOfLine22from imaginary.pyparsing import Literal, White, Optional, restOfLine
@@ -109,7 +109,7 @@
109 otherMessage=language.Sentence([player, " creates ", phrase, "."]))109 otherMessage=language.Sentence([player, " creates ", phrase, "."]))
110110
111111
112class Create(NoTargetAction):112class Create(Action):
113 """113 """
114 An action which can create items by looking at the L{IThingType} plugin114 An action which can create items by looking at the L{IThingType} plugin
115 registry.115 registry.
@@ -163,7 +163,7 @@
163163
164164
165165
166class ListThingTypes(NoTargetAction):166class ListThingTypes(Action):
167 """167 """
168 An action which tells the invoker what thing types exist to be created with168 An action which tells the invoker what thing types exist to be created with
169 the L{Create} command.169 the L{Create} command.
170170
=== modified file 'Imaginary/imaginary/events.py'
--- Imaginary/imaginary/events.py 2009-01-14 05:21:23 +0000
+++ Imaginary/imaginary/events.py 2011-09-16 20:42:26 +0000
@@ -1,10 +1,11 @@
1# -*- test-case-name: imaginary.test -*-1# -*- test-case-name: imaginary.test.test_actions.TargetActionTests.test_resolveTargetCaseInsensitively -*-
22
3from zope.interface import implements3from zope.interface import implements
44
5from twisted.python import context5from twisted.python import context
66
7from imaginary import iimaginary, language, eimaginary7from imaginary import iimaginary, language, eimaginary
8from imaginary.idea import Proximity, ProviderOf
89
910
10class Event(language.BaseExpress):11class Event(language.BaseExpress):
@@ -35,8 +36,13 @@
3536
3637
37 def conceptFor(self, observer):38 def conceptFor(self, observer):
38 # This can't be a dict because then the ordering when actor is target39 """
39 # or target is tool or etc is non-deterministic.40 Retrieve the appropriate L{IConcept} provider for a given observer. If
41 the observer is this L{Event}'s C{actor}, it will return the
42 C{actorMessage} for this event, and so on for the tool and the target.
43 If it doesn't match a L{Thing} known to this event, it will return
44 C{otherMessage}.
45 """
40 if observer is self.actor:46 if observer is self.actor:
41 msg = self.actorMessage47 msg = self.actorMessage
42 elif observer is self.target:48 elif observer is self.target:
@@ -65,13 +71,12 @@
65 L{Event}'s location when this method, L{Event.reify}, was called.71 L{Event}'s location when this method, L{Event.reify}, was called.
66 """72 """
67 L = []73 L = []
68 for ob in iimaginary.IContainer(self.location).getContents():74 for observer in (self.location.idea.obtain(
69 observer = iimaginary.IEventObserver(ob, None)75 Proximity(0.5, ProviderOf(iimaginary.IEventObserver)))):
70 if observer:76 sender = observer.prepare(self)
71 sender = observer.prepare(self)77 if not callable(sender):
72 if not callable(sender):78 raise TypeError("Senders must be callable", sender)
73 raise TypeError("Senders must be callable", sender)79 L.append(sender)
74 L.append(sender)
75 return lambda: map(apply, L)80 return lambda: map(apply, L)
7681
7782
@@ -163,7 +168,7 @@
163 raise168 raise
164 try:169 try:
165 result = store.transact(runHelper)170 result = store.transact(runHelper)
166 except eimaginary.ActionFailure, e:171 except eimaginary.ActionFailure:
167 broadcaster.broadcastRevertEvents()172 broadcaster.broadcastRevertEvents()
168 return None173 return None
169 else:174 else:
170175
=== modified file 'Imaginary/imaginary/garments.py'
--- Imaginary/imaginary/garments.py 2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/garments.py 2011-09-16 20:42:26 +0000
@@ -11,6 +11,9 @@
11from axiom import item, attributes11from axiom import item, attributes
1212
13from imaginary import iimaginary, language, objects13from imaginary import iimaginary, language, objects
14from imaginary.eimaginary import ActionFailure
15from imaginary.events import ThatDoesntWork
16from imaginary.idea import Link
14from imaginary.creation import createCreator17from imaginary.creation import createCreator
15from imaginary.enhancement import Enhancement18from imaginary.enhancement import Enhancement
1619
@@ -80,9 +83,17 @@
8083
8184
82class Garment(item.Item, Enhancement):85class Garment(item.Item, Enhancement):
86 """
87 An enhancement for a L{Thing} representing its utility as an article of
88 clothing.
89 """
83 implements(iimaginary.IClothing,90 implements(iimaginary.IClothing,
84 iimaginary.IDescriptionContributor)91 iimaginary.IDescriptionContributor,
85 powerupInterfaces = (iimaginary.IClothing, iimaginary.IDescriptionContributor)92 iimaginary.IMovementRestriction)
93
94 powerupInterfaces = (iimaginary.IClothing,
95 iimaginary.IDescriptionContributor,
96 iimaginary.IMovementRestriction)
8697
87 thing = attributes.reference()98 thing = attributes.reference()
8899
@@ -113,6 +124,43 @@
113 return self.garmentDescription124 return self.garmentDescription
114125
115126
127 def nowWornBy(self, wearer):
128 """
129 This garment is now worn by the given wearer. As this garment is now
130 on top, set its C{wearLevel} to be higher than any other L{Garment}
131 related to the new C{wearer}.
132 """
133 self.wearer = wearer
134 self.wearLevel = wearer.store.query(
135 Garment,
136 Garment.wearer == wearer).getColumn("wearLevel").max(default=0) + 1
137
138
139 def noLongerWorn(self):
140 """
141 This garment is no longer being worn by anyone.
142 """
143 self.wearer = None
144 self.wearLevel = None
145
146
147 def movementImminent(self, movee, destination):
148 """
149 Something is trying to move. Don't allow it if I'm currently worn.
150 """
151 if self.wearer is not None and movee is self.thing:
152 raise ActionFailure(
153 ThatDoesntWork(
154 # XXX I don't actually know who is performing the action
155 # :-(.
156 actor=self.wearer.thing,
157 actorMessage=[
158 "You can't move ",
159 language.Noun(self.thing).definiteNounPhrase(),
160 " without removing it first."]))
161
162
163
116def _orderTopClothingByGlobalSlotList(tempClothes):164def _orderTopClothingByGlobalSlotList(tempClothes):
117 """165 """
118 This function orders a dict as returned by getGarmentDict in the order that166 This function orders a dict as returned by getGarmentDict in the order that
@@ -154,9 +202,15 @@
154 person or mannequin.202 person or mannequin.
155 """203 """
156204
157 implements(iimaginary.IClothingWearer, iimaginary.IDescriptionContributor)205 _interfaces = (iimaginary.IClothingWearer,
158 powerupInterfaces = (iimaginary.IClothingWearer, iimaginary.IDescriptionContributor,206 iimaginary.IDescriptionContributor,
159 iimaginary.ILinkContributor)207 iimaginary.ILinkContributor,
208 iimaginary.ILinkAnnotator,
209 )
210
211 implements(*_interfaces)
212
213 powerupInterfaces = _interfaces
160214
161215
162 thing = attributes.reference()216 thing = attributes.reference()
@@ -172,27 +226,52 @@
172226
173227
174 def putOn(self, newGarment):228 def putOn(self, newGarment):
229 """
230 Wear a new L{Garment} on this L{Wearer}, first moving it to this
231 L{Wearer}'s C{thing} if it is not already there.
232
233 @param newGarment: the article of clothing to wear.
234
235 @type newGarment: L{Garment}
236
237 @raise TooBulky: if the bulk of any of the slots occupied by
238 C{newGarment} is greater than the bulk of any other clothing
239 already in that slot. (For example, if you tried to wear a T-shirt
240 over a heavy coat.)
241 """
175 c = self.getGarmentDict()242 c = self.getGarmentDict()
176 for garmentSlot in newGarment.garmentSlots:243 for garmentSlot in newGarment.garmentSlots:
177 if garmentSlot in c:244 if garmentSlot in c:
178 # We don't want to be able to wear T-shirts over heavy coats;
179 # therefore, heavy coats have a high "bulk"
180 currentTopOfSlot = c[garmentSlot][-1]245 currentTopOfSlot = c[garmentSlot][-1]
181 if currentTopOfSlot.bulk >= newGarment.bulk:246 if currentTopOfSlot.bulk >= newGarment.bulk:
182 raise TooBulky(currentTopOfSlot, newGarment)247 raise TooBulky(currentTopOfSlot, newGarment)
183248
184 newGarment.thing.moveTo(None)249 newGarment.thing.moveTo(self.thing)
185 newGarment.wearer = self250 newGarment.nowWornBy(self)
186 newGarment.wearLevel = self.store.query(Garment, Garment.wearer == self).getColumn("wearLevel").max(default=0) + 1
187251
188252
189 def takeOff(self, garment):253 def takeOff(self, garment):
254 """
255 Remove a garment which this player is wearing.
256
257 (Note: no error checking is currently performed to see if this garment
258 is actually already worn by this L{Wearer}.)
259
260 @param garment: the article of clothing to remove.
261
262 @type garment: L{Garment}
263
264 @raise InaccessibleGarment: if the garment is obscured by any other
265 clothing, and is therefore not in the top slot for any of the slots
266 it occupies. For example, if you put on an undershirt, then a
267 turtleneck, you can't remove the undershirt without removing the
268 turtleneck first.
269 """
190 gdict = self.getGarmentDict()270 gdict = self.getGarmentDict()
191 for slot in garment.garmentSlots:271 for slot in garment.garmentSlots:
192 if gdict[slot][-1] is not garment:272 if gdict[slot][-1] is not garment:
193 raise InaccessibleGarment(self, garment, gdict[slot][-1])273 raise InaccessibleGarment(self, garment, gdict[slot][-1])
194 garment.thing.moveTo(garment.wearer.thing)274 garment.noLongerWorn()
195 garment.wearer = garment.wearLevel = None
196275
197276
198 # IDescriptionContributor277 # IDescriptionContributor
@@ -205,11 +284,45 @@
205284
206 # ILinkContributor285 # ILinkContributor
207 def links(self):286 def links(self):
208 d = {}287 for garmentThing in self.store.query(objects.Thing,
209 for t in self.store.query(objects.Thing, attributes.AND(Garment.thing == objects.Thing.storeID,288 attributes.AND(
210 Garment.wearer == self)):289 Garment.thing == objects.Thing.storeID,
211 d.setdefault(t.name, []).append(t)290 Garment.wearer == self)):
212 return d291 yield Link(self.thing.idea, garmentThing.idea)
292
293
294 # ILinkAnnotator
295 def annotationsFor(self, link, idea):
296 """
297 Tell the containment system to disregard containment relationships for
298 which I will generate a link.
299 """
300 if list(link.of(iimaginary.IContainmentRelationship)):
301 if link.source.delegate is self.thing:
302 clothing = iimaginary.IClothing(link.target.delegate, None)
303 if clothing is not None:
304 if clothing.wearer is self:
305 yield _DisregardYourWearingIt()
306
307
308
309class _DisregardYourWearingIt(object):
310 """
311 This is an annotation, produced by L{Wearer} for containment relationships
312 between people (who are containers) and the clothing that they're wearing.
313 A hopefully temporary workaround for the fact that clothing is rendered in
314 its own way and therefor shouldn't show up in the list of a person's
315 contents.
316 """
317 implements(iimaginary.IElectromagneticMedium)
318
319 def isOpaque(self):
320 """
321 I am opaque, so that clothing will show up only once (in your "wearing"
322 list, rather than there and in your "contained" list), and obscured
323 clothing won't show up at all.
324 """
325 return True
213326
214327
215328
216329
=== added file 'Imaginary/imaginary/idea.py'
--- Imaginary/imaginary/idea.py 1970-01-01 00:00:00 +0000
+++ Imaginary/imaginary/idea.py 2011-09-16 20:42:26 +0000
@@ -0,0 +1,625 @@
1# -*- test-case-name: imaginary -*-
2
3"""
4This module implements a highly abstract graph-traversal system for actions and
5events to locate the objects which can respond to them. The top-level
6entry-point to this system is L{Idea.obtain}.
7
8It also implements several basic retrievers related to visibility and physical
9reachability.
10"""
11
12from zope.interface import implements
13from epsilon.structlike import record
14
15from imaginary.iimaginary import (
16 INameable, ILitLink, IThing, IObstruction, IElectromagneticMedium,
17 IDistance, IRetriever, IExit)
18
19
20
21class Link(record("source target")):
22 """
23 A L{Link} is a connection between two L{Idea}s in a L{Path}.
24
25 @ivar source: the idea that this L{Link} originated from.
26 @type source: L{Idea}
27
28 @ivar target: the idea that this L{Link} refers to.
29 @type target: L{Idea}
30 """
31
32 def __init__(self, *a, **k):
33 super(Link, self).__init__(*a, **k)
34 self.annotations = []
35
36
37 def annotate(self, annotations):
38 """
39 Annotate this link with a list of annotations.
40 """
41 self.annotations.extend(annotations)
42
43
44 def of(self, interface):
45 """
46 Yield all annotations on this link which provide the given interface.
47 """
48 for annotation in self.annotations:
49 provider = interface(annotation, None)
50 if provider is not None:
51 yield provider
52
53
54
55class Path(record('links')):
56 """
57 A list of L{Link}s.
58 """
59
60 def of(self, interface):
61 """
62 @return: an iterator of providers of interfaces, adapted from each link
63 in this path.
64 """
65 for link in self.links:
66 for annotation in link.of(interface):
67 yield annotation
68
69
70 def eachTargetAs(self, interface):
71 """
72 @return: an iterable of all non-None results of each L{Link.targetAs}
73 method in this L{Path}'s C{links} attribute.
74 """
75 for link in self.links:
76 provider = interface(link.target.delegate, None)
77 if provider is not None:
78 yield provider
79
80
81 def targetAs(self, interface):
82 """
83 Retrieve the target of the last link of this path, its final
84 destination, as a given interface.
85
86 @param interface: the interface to retrieve.
87 @type interface: L{zope.interface.interfaces.IInterface}
88
89 @return: the last link's target, adapted to the given interface, or
90 C{None} if no appropriate adapter or component exists.
91 @rtype: C{interface} or C{NoneType}
92 """
93 return interface(self.links[-1].target.delegate, None)
94
95
96 def isCyclic(self):
97 """
98 Determine if this path is cyclic, to avoid descending down infinite
99 loops.
100
101 @return: a boolean indicating whether this L{Path} is cyclic or not,
102 i.e. whether the L{Idea} its last link points at is the source of
103 any of its links.
104 """
105 if len(self.links) < 2:
106 return False
107 return (self.links[-1].target in (x.source for x in self.links))
108
109
110 def to(self, link):
111 """
112 Create a new path, extending this one by one new link.
113 """
114 return Path(self.links + [link])
115
116
117 def __repr__(self):
118 """
119 @return: an expanded pretty-printed representation of this Path,
120 suitable for debugging.
121 """
122 s = 'Path('
123 for link in self.links:
124 dlgt = link.target.delegate
125 src = link.source.delegate
126 s += "\n\t"
127 s += repr(getattr(src, 'name', src))
128 s += " => "
129 s += repr(getattr(dlgt, 'name', dlgt))
130 s += " "
131 s += repr(link.annotations)
132 s += ')'
133 return s
134
135
136
137class Idea(record("delegate linkers annotators")):
138 """
139 Consider a person's activities with the world around them as having two
140 layers. One is a physical layer, out in the world, composed of matter and
141 energy. The other is a cognitive layer, internal to the person, composed
142 of ideas about that matter and energy.
143
144 For example, when a person wants to sit in a wooden chair, they must first
145 visually locate the arrangement of wood in question, make the determination
146 of that it is a "chair" based on its properties, and then perform the
147 appropriate actions to sit upon it.
148
149 However, a person may also interact with symbolic abstractions rather than
150 physical objects. They may read a word, or point at a window on a computer
151 screen. An L{Idea} is a representation of the common unit that can be
152 referred to in this way.
153
154 Both physical and cognitive layers are present in Imaginary. The cognitive
155 layer is modeled by L{imaginary.idea}. The physical layer is modeled by a
156 rudimentary point-of-interest simulation in L{imaginary.objects}. An
157 L{imaginary.thing.Thing} is a physical object; an L{Idea} is a node in a
158 non-physical graph, related by links that are annotated to describe the
159 nature of the relationship between it and other L{Idea}s.
160
161 L{Idea} is the most abstract unit of simulation. It does not have any
162 behavior or simulation semantics of its own; it merely ties together
163 different related systems.
164
165 An L{Idea} is composed of a C{delegate}, which is an object that implements
166 simulation-defined interfaces; a list of L{ILinkContributor}s, which
167 produce L{Link}s to other L{Idea}s, an a set of C{ILinkAnnotator}s, which
168 apply annotations (which themselves implement simulation-defined
169 link-annotation interfaces) to those links.
170
171 Each L{imaginary.thing.Thing} has a corresponding L{Idea} to represent it
172 in the simulation. The physical simulation defines only a few types of
173 links: objects have links to their containers, containers have links to
174 their contents, rooms have links to their exits, exits have links to their
175 destinations. Any L{imaginary.thing.Thing} can have a powerup applied to
176 it which adds to the list of linkers or annotators for its L{Idea},
177 however, which allows users to create arbitrary objects.
178
179 For example, the target of the "look" action must implement
180 L{imaginary.iimaginary.IVisible}, but need not be a
181 L{iimaginary.objects.Thing}. A simulation might want to provide a piece of
182 graffiti that you could look at, but would not be a physical object, in the
183 sense that you couldn't pick it up, weigh it, push it, etc. Such an object
184 could be implemented as a powerup for both
185 L{imaginary.iimaginary.IDescriptionContributor}, which would impart some
186 short flavor text to the room, and L{imaginary.iimaginary.IVisible}, which
187 would be an acceptable target of 'look'. The
188 L{imaginary.iimaginary.IVisible} implementation could even be an in-memory
189 object, not stored in the database at all; and there could be different
190 implementations for different observers, depending on their level of
191 knowledge about the in-world graffiti.
192
193 @ivar delegate: this object is the object which may be adaptable to a set
194 of interfaces. This L{Idea} delegates all adaptation to its delegate.
195 In many cases (when referring to a physical object), this will be an
196 L{imaginary.thing.Thing}, but not necessarily.
197
198 @ivar linkers: a L{list} of L{ILinkContributor}s which are used to gather
199 L{Link}s from this L{Idea} during L{Idea.obtain} traversal.
200
201 @ivar annotators: a L{list} of L{ILinkAnnotator}s which are used to annotate
202 L{Link}s gathered from this L{Idea} via the C{linkers} list.
203 """
204
205 def __init__(self, delegate):
206 super(Idea, self).__init__(delegate, [], [])
207
208
209 def _allLinks(self):
210 """
211 Return an iterator of all L{Links} away from this idea.
212 """
213 for linker in self.linkers:
214 for link in linker.links():
215 yield link
216
217
218 def _applyAnnotators(self, linkiter):
219 """
220 Apply my list of annotators to each link in the given iterable.
221 """
222 for link in linkiter:
223 self._annotateOneLink(link)
224 yield link
225
226
227 def _annotateOneLink(self, link):
228 """
229 Apply all L{ILinkAnnotator}s in this L{Idea}'s C{annotators} list.
230 """
231 allAnnotations = []
232 for annotator in self.annotators:
233 # XXX important to test: annotators shouldn't mutate the links.
234 # The annotators show up in a non-deterministic order, so in order
235 # to facilitate a consistent view of the link in annotationsFor(),
236 # all annotations are applied at the end.
237 allAnnotations.extend(annotator.annotationsFor(link, self))
238 link.annotate(allAnnotations)
239
240
241 def obtain(self, retriever):
242 """
243 Traverse the graph of L{Idea}s, starting with C{self}, looking for
244 objects which the given L{IRetriever} can retrieve.
245
246 The graph will be traversed by looking at all the links generated by
247 this L{Idea}'s C{linkers}, only continuing down those links for which
248 the given L{IRetriever}'s C{shouldKeepGoing} returns L{True}.
249
250 @param retriever: an object which will be passed each L{Path} in turn,
251 discovered during traversal of the L{Idea} graph. If any
252 invocation of L{IRetriever.retrieve} on this parameter should
253 succeed, that will be yielded as a result from this method.
254 @type retriever: L{IRetriever}
255
256 @return: a generator which yields the results of C{retriever.retrieve}
257 which are not L{None}.
258 """
259 return ObtainResult(self, retriever)
260
261
262 def _doObtain(self, retriever, path, reasonsWhyNot):
263 """
264 A generator that implements the logic for obtain()
265 """
266 if path is None:
267 # Special case: we only get a self->self link if we are the
268 # beginning _and_ the end.
269 path = Path([])
270 selfLink = Link(self, self)
271 self._annotateOneLink(selfLink)
272 finalPath = path.to(selfLink)
273 else:
274 finalPath = Path(path.links[:])
275 self._annotateOneLink(finalPath.links[-1])
276
277 result = retriever.retrieve(finalPath)
278 objections = set(retriever.objectionsTo(finalPath, result))
279 reasonsWhyNot |= objections
280 if result is not None:
281 if not objections:
282 yield result
283
284 for link in self._applyAnnotators(self._allLinks()):
285 subpath = path.to(link)
286 if subpath.isCyclic():
287 continue
288 if retriever.shouldKeepGoing(subpath):
289 for obtained in link.target._doObtain(retriever, subpath, reasonsWhyNot):
290 yield obtained
291
292
293
294class ObtainResult(record("idea retriever")):
295 """
296 The result of L{Idea.obtain}, this provides an iterable of results.
297
298 @ivar reasonsWhyNot: If this iterator has already been exhausted, this will
299 be a C{set} of L{IWhyNot} objects explaining possible reasons why there
300 were no results. For example, if the room where the player attempted
301 to obtain targets is dark, this may contain an L{IWhyNot} provider.
302 However, until this iterator has been exhausted, it will be C{None}.
303 @type reasonsWhyNot: C{set} of L{IWhyNot}, or C{NoneType}
304
305 @ivar idea: the L{Idea} that L{Idea.obtain} was invoked on.
306 @type idea: L{Idea}
307
308 @ivar retriever: The L{IRetriever} that L{Idea.obtain} was invoked with.
309 @type retriever: L{IRetriever}
310 """
311
312 reasonsWhyNot = None
313
314 def __iter__(self):
315 """
316 A generator which yields each result of the query, then sets
317 C{reasonsWhyNot}.
318 """
319 reasonsWhyNot = set()
320 for result in self.idea._doObtain(self.retriever, None, reasonsWhyNot):
321 yield result
322 self.reasonsWhyNot = reasonsWhyNot
323
324
325
326class DelegatingRetriever(object):
327 """
328 A delegating retriever, so that retrievers can be easily composed.
329
330 See the various methods marked for overriding.
331
332 @ivar retriever: A retriever to delegate most operations to.
333 @type retriever: L{IRetriever}
334 """
335
336 implements(IRetriever)
337
338 def __init__(self, retriever):
339 """
340 Create a delegator with a retriever to delegate to.
341 """
342 self.retriever = retriever
343
344
345 def moreObjectionsTo(self, path, result):
346 """
347 Override in subclasses to yield objections to add to this
348 L{DelegatingRetriever}'s C{retriever}'s C{objectionsTo}.
349
350 By default, offer no additional objections.
351 """
352 return []
353
354
355 def objectionsTo(self, path, result):
356 """
357 Concatenate C{self.moreObjectionsTo} with C{self.moreObjectionsTo}.
358 """
359 for objection in self.retriever.objectionsTo(path, result):
360 yield objection
361 for objection in self.moreObjectionsTo(path, result):
362 yield objection
363
364
365 def shouldStillKeepGoing(self, path):
366 """
367 Override in subclasses to halt traversal via a C{False} return value for
368 C{shouldKeepGoing} if this L{DelegatingRetriever}'s C{retriever}'s
369 C{shouldKeepGoing} returns C{True}.
370
371 By default, return C{True} to keep going.
372 """
373 return True
374
375
376 def shouldKeepGoing(self, path):
377 """
378 If this L{DelegatingRetriever}'s C{retriever}'s C{shouldKeepGoing}
379 returns C{False} for the given path, return C{False} and stop
380 traversing. Otherwise, delegate to C{shouldStillKeepGoing}.
381 """
382 return (self.retriever.shouldKeepGoing(path) and
383 self.shouldStillKeepGoing(path))
384
385
386 def resultRetrieved(self, path, retrievedResult):
387 """
388 A result was retrieved. Post-process it if desired.
389
390 Override this in subclasses to modify (non-None) results returned from
391 this L{DelegatingRetriever}'s C{retriever}'s C{retrieve} method.
392
393 By default, simply return the result retrieved.
394 """
395 return retrievedResult
396
397
398 def retrieve(self, path):
399 """
400 Delegate to this L{DelegatingRetriever}'s C{retriever}'s C{retrieve}
401 method, then post-process it with C{resultRetrieved}.
402 """
403 subResult = self.retriever.retrieve(path)
404 if subResult is None:
405 return None
406 return self.resultRetrieved(path, subResult)
407
408
409
410class Proximity(DelegatingRetriever):
411 """
412 L{Proximity} is a retriever which will continue traversing any path which
413 is shorter than its proscribed distance, but not any longer.
414
415 @ivar distance: the distance, in meters, to query for.
416
417 @type distance: L{float}
418 """
419
420 def __init__(self, distance, retriever):
421 DelegatingRetriever.__init__(self, retriever)
422 self.distance = distance
423
424
425 def shouldStillKeepGoing(self, path):
426 """
427 Implement L{IRetriever.shouldKeepGoing} to stop for paths whose sum of
428 L{IDistance} annotations is greater than L{Proximity.distance}.
429 """
430 dist = sum(vector.distance for vector in path.of(IDistance))
431 ok = (self.distance >= dist)
432 return ok
433
434
435
436class Reachable(DelegatingRetriever):
437 """
438 L{Reachable} is a navivator which will object to any path with an
439 L{IObstruction} annotation on it.
440 """
441
442 def moreObjectionsTo(self, path, result):
443 """
444 Yield an objection from each L{IObstruction.whyNot} method annotating
445 the given path.
446 """
447 if result is not None:
448 for obstruction in path.of(IObstruction):
449 yield obstruction.whyNot()
450
451
452
453class Traversability(DelegatingRetriever):
454 """
455 A path is only traversible if it terminates in *one* exit. Once you've
456 gotten to an exit, you have to stop, because the player needs to go through
457 that exit to get to the next one.
458 """
459
460 def shouldStillKeepGoing(self, path):
461 """
462 Stop at the first exit that you find.
463 """
464 for index, target in enumerate(path.eachTargetAs(IExit)):
465 if index > 0:
466 return False
467 return True
468
469
470
471class Vector(record('distance direction')):
472 """
473 A L{Vector} is a link annotation which remembers a distance and a
474 direction; for example, a link through a 'north' exit between rooms will
475 have a direction of 'north' and a distance specified by that
476 L{imaginary.objects.Exit} (defaulting to 1 meter).
477 """
478
479 implements(IDistance)
480
481
482
483class ProviderOf(record("interface")):
484 """
485 L{ProviderOf} is a retriever which will retrieve the facet which provides
486 its C{interface}, if any exists at the terminus of the path.
487
488 @ivar interface: The interface which defines the type of values returned by
489 the C{retrieve} method.
490 @type interface: L{zope.interface.interfaces.IInterface}
491 """
492
493 implements(IRetriever)
494
495 def retrieve(self, path):
496 """
497 Retrieve the target of the path, as it provides the interface specified
498 by this L{ProviderOf}.
499
500 @return: the target of the path, adapted to this retriever's interface,
501 as defined by L{Path.targetAs}.
502
503 @rtype: L{ProviderOf.interface}
504 """
505 return path.targetAs(self.interface)
506
507
508 def objectionsTo(self, path, result):
509 """
510 Implement L{IRetriever.objectionsTo} to yield no objections.
511 """
512 return []
513
514
515 def shouldKeepGoing(self, path):
516 """
517 Implement L{IRetriever.shouldKeepGoing} to always return C{True}.
518 """
519 return True
520
521
522
523class AlsoKnownAs(record('name')):
524 """
525 L{AlsoKnownAs} is an annotation that indicates that the link it annotates
526 is known as a particular name.
527
528 @ivar name: The name that this L{AlsoKnownAs}'s link's target is also known
529 as.
530 @type name: C{unicode}
531 """
532
533 implements(INameable)
534
535 def knownTo(self, observer, name):
536 """
537 An L{AlsoKnownAs} is known to all observers as its C{name} attribute.
538 """
539 return (self.name == name)
540
541
542
543class Named(DelegatingRetriever):
544 """
545 A retriever which wraps another retriever, but yields only results known to
546 a particular observer by a particular name.
547
548 @ivar name: the name to search for.
549
550 @ivar observer: the observer who should identify the target by the name
551 this L{Named} is searching for.
552 @type observer: L{Thing}
553 """
554
555 def __init__(self, name, retriever, observer):
556 DelegatingRetriever.__init__(self, retriever)
557 self.name = name
558 self.observer = observer
559
560
561 def resultRetrieved(self, path, subResult):
562 """
563 Invoke C{retrieve} on the L{IRetriever} which we wrap, but only return
564 it if the L{INameable} target of the given path is known as this
565 L{Named}'s C{name}.
566 """
567 named = path.targetAs(INameable)
568 allAliases = list(path.links[-1].of(INameable))
569 if named is not None:
570 allAliases += [named]
571 for alias in allAliases:
572 if alias.knownTo(self.observer, self.name):
573 return subResult
574 return None
575
576
577
578class CanSee(DelegatingRetriever):
579 """
580 Wrap a L{ProviderOf}, yielding the results that it would yield, but
581 applying lighting to the ultimate target based on the last L{IThing} the
582 path.
583
584 @ivar retriever: The lowest-level retriever being wrapped.
585
586 @type retriever: L{ProviderOf} (Note: it might be a good idea to add an
587 'interface' attribute to L{IRetriever} so this no longer depends on a
588 more specific type than other L{DelegatingRetriever}s, to make the
589 order of composition more flexible.)
590 """
591
592 def resultRetrieved(self, path, subResult):
593 """
594 Post-process retrieved results by determining if lighting applies to
595 them.
596 """
597 litlinks = list(path.of(ILitLink))
598 if not litlinks:
599 return subResult
600 # XXX what if there aren't any IThings on the path?
601 litThing = list(path.eachTargetAs(IThing))[-1]
602 # you need to be able to look from a light room to a dark room, so only
603 # apply the most "recent" lighting properties.
604 return litlinks[-1].applyLighting(
605 litThing, subResult, self.retriever.interface)
606
607
608 def shouldStillKeepGoing(self, path):
609 """
610 Don't keep going through links that are opaque to the observer.
611 """
612 for opacity in path.of(IElectromagneticMedium):
613 if opacity.isOpaque():
614 return False
615 return True
616
617
618 def moreObjectionsTo(self, path, result):
619 """
620 Object to paths which have L{ILitLink} annotations which are not lit.
621 """
622 for lighting in path.of(ILitLink):
623 if not lighting.isItLit(path, result):
624 tmwn = lighting.whyNotLit()
625 yield tmwn
0626
=== modified file 'Imaginary/imaginary/iimaginary.py'
--- Imaginary/imaginary/iimaginary.py 2009-01-14 05:21:23 +0000
+++ Imaginary/imaginary/iimaginary.py 2011-09-16 20:42:26 +0000
@@ -40,16 +40,18 @@
40 A powerup interface which can add more connections between objects in the40 A powerup interface which can add more connections between objects in the
41 world graph.41 world graph.
4242
43 All ILinkContributors which are powered up on a particular Thing will be43 All L{ILinkContributors} which are powered up on a particular
44 given a chance to add to the L{IThing.link} method's return value.44 L{imaginary.objects.Thing} will be appended to that
45 L{imaginary.objects.Thing}'s value.
45 """46 """
4647
47 def links():48 def links():
48 """49 """
49 Return a C{dict} mapping names of connections to C{IThings}.50 @return: an iterable of L{imaginary.idea.Link}s.
50 """51 """
5152
5253
54
53class IDescriptionContributor(Interface):55class IDescriptionContributor(Interface):
54 """56 """
55 A powerup interface which can add text to the description of an object.57 A powerup interface which can add text to the description of an object.
@@ -64,6 +66,70 @@
64 """66 """
6567
6668
69class INameable(Interface):
70 """
71 A provider of L{INameable} is an object which can be identified by an
72 imaginary actor by a name.
73 """
74
75 def knownTo(observer, name):
76 """
77 Is this L{INameable} known to the given C{observer} by the given
78 C{name}?
79
80 @param name: the name to test for
81
82 @type name: L{unicode}
83
84 @param observer: the thing which is observing this namable.
85
86 @type observer: L{IThing}
87
88 @rtype: L{bool}
89
90 @return: L{True} if C{name} identifies this L{INameable}, L{False}
91 otherwise.
92 """
93
94
95class ILitLink(Interface):
96 """
97 This interface is an annotation interface for L{imaginary.idea.Link}
98 objects, for indicating that the link can apply lighting.
99 """
100
101 def applyLighting(litThing, eventualTarget, requestedInterface):
102 """
103 Apply a transformation to an object that an
104 L{imaginary.idea.Idea.obtain} is requesting, based on the light level
105 of this link and its surroundings.
106
107 @param litThing: The L{IThing} to apply lighting to.
108
109 @type litThing: L{IThing}
110
111 @param eventualTarget: The eventual, ultimate target of the path in
112 question.
113
114 @type eventualTarget: C{requestedInterface}
115
116 @param requestedInterface: The interface requested by the query that
117 resulted in this path; this is the interface which
118 C{eventualTarget} should implement.
119
120 @type requestedInterface: L{Interface}
121
122 @return: C{eventualTarget}, or, if this L{ILitLink} knows how to deal
123 with lighting specifically for C{requestedInterface}, a modified
124 version thereof which still implements C{requestedInterface}. If
125 insufficient lighting results in the player being unable to access
126 the desired object at all, C{None} will be returned.
127
128 @rtype: C{NoneType}, or C{requestedInterface}
129 """
130
131
132
67133
68class IThing(Interface):134class IThing(Interface):
69 """135 """
@@ -71,6 +137,12 @@
71 """137 """
72 location = Attribute("An IThing which contains this IThing")138 location = Attribute("An IThing which contains this IThing")
73139
140 proper = Attribute(
141 "A boolean indicating the definiteness of this thing's pronoun.")
142
143 name = Attribute(
144 "A unicode string, the name of this Thing.")
145
74146
75 def moveTo(where, arrivalEventFactory=None):147 def moveTo(where, arrivalEventFactory=None):
76 """148 """
@@ -78,7 +150,7 @@
78150
79 @type where: L{IThing} provider.151 @type where: L{IThing} provider.
80 @param where: The new location to be moved to.152 @param where: The new location to be moved to.
81 153
82 @type arrivalEventFactory: A callable which takes a single154 @type arrivalEventFactory: A callable which takes a single
83 argument, the thing being moved, and returns an event.155 argument, the thing being moved, and returns an event.
84 @param arrivalEventFactory: Will be called to produce the156 @param arrivalEventFactory: Will be called to produce the
@@ -86,7 +158,6 @@
86 thing. If not specified (or None), no event will be broadcast.158 thing. If not specified (or None), no event will be broadcast.
87 """159 """
88160
89
90 def findProviders(interface, distance):161 def findProviders(interface, distance):
91 """162 """
92 Retrieve all game objects which provide C{interface} within C{distance}.163 Retrieve all game objects which provide C{interface} within C{distance}.
@@ -95,19 +166,31 @@
95 """166 """
96167
97168
98 def proxiedThing(thing, interface, distance):169
99 """170class IMovementRestriction(Interface):
100 Given an L{IThing} provider, return a provider of L{interface} as it is171 """
101 accessible from C{self}. Any necessary proxies will be applied.172 A L{MovementRestriction} is a powerup that can respond to a L{Thing}'s
102 """173 movement before it occurs, and thereby restrict it.
103174
104175 Powerups of this type are consulted on L{Thing} before movement is allowed
105 def knownAs(name):176 to complete.
106 """177 """
107 Return a boolean indicating whether this thing might reasonably be178
108 called C{name}.179 def movementImminent(movee, destination):
109180 """
110 @type name: C{unicode}181 An object is about to move. Implementations can raise an exception if
182 they wish to to prevent it.
183
184 @param movee: the object that is moving.
185
186 @type movee: L{Thing}
187
188 @param destination: The L{Thing} of the container that C{movee} will be
189 moving to.
190
191 @type destination: L{IThing}
192
193 @raise Exception: if the movement is to be prevented.
111 """194 """
112195
113196
@@ -116,6 +199,7 @@
116 hitpoints = Attribute("L{Points} instance representing hit points")199 hitpoints = Attribute("L{Points} instance representing hit points")
117 experience = Attribute("C{int} representing experience")200 experience = Attribute("C{int} representing experience")
118 level = Attribute("C{int} representing player's level")201 level = Attribute("C{int} representing player's level")
202 thing = Attribute("L{IThing} which represents the actor's physical body.")
119203
120 def send(event):204 def send(event):
121 """Describe something to the actor.205 """Describe something to the actor.
@@ -224,22 +308,68 @@
224308
225309
226310
311class IExit(Interface):
312 """
313 An interface representing one direction that a player may move in. While
314 L{IExit} only represents one half of a passageway, it is not necessarily
315 one-way; in most cases, a parallel exit will exist on the other side.
316 (However, it I{may} be one-way; there is no guarantee that you will be able
317 to traverse it backwards, or even indeed that it will take you somewhere at
318 all!)
319 """
320
321 name = Attribute(
322 """
323 The name of this exit. This must be something adaptable to
324 L{IConcept}, to display to players.
325 """)
326
327 def traverse(thing):
328 """
329 Attempt to move the given L{IThing} through this L{IExit} to the other
330 side. (Note that this may not necessarily result in actual movement,
331 if the exit does something tricky like disorienting you or hurting
332 you.)
333
334 @param thing: Something which is passing through this exit.
335
336 @type thing: L{IThing}
337 """
338
339
340
341
342class IObstruction(Interface):
343 """
344 An L{IObstruction} is a link annotation indicating that there is a physical
345 obstruction preventing solid objects from reaching between the two ends of
346 the link. For example, a closed door might annotate its link to its
347 destination with an L{IObstruction}.
348 """
349
350 def whyNot():
351 """
352 @return: a reason why this is obstructed.
353
354 @rtype: L{IWhyNot}
355 """
356
357
358
227class IContainer(Interface):359class IContainer(Interface):
228 """360 """
229 An object which can contain other objects.361 An object which can contain other objects.
230 """362 """
231 capacity = Attribute("""363 capacity = Attribute(
232 The maximum weight this container is capable of holding.364 """
233 """)365 The maximum weight this container is capable of holding.
234366 """)
235# lid = Attribute("""367
236# A reference to an L{IThing} which serves as this containers lid, or368 closed = Attribute(
237# C{None} if there is no lid.369 """
238# """)370 A boolean indicating whether this container is closed.
239371 """)
240 closed = Attribute("""372
241 A boolean indicating whether this container is closed.
242 """)
243373
244 def add(object):374 def add(object):
245 """375 """
@@ -331,63 +461,84 @@
331461
332462
333463
334class IProxy(Interface):464class ILinkAnnotator(Interface):
335 """465 """
336 | > look466 An L{ILinkAnnotator} provides annotations for links from one
337 | [ Nuclear Reactor Core ]467 L{imaginary.idea.Idea} to another.
338 | High-energy particles are wizzing around here at a fantastic rate. You can468 """
339 | feel the molecules in your body splitting apart as neutrons bombard the469
340 | nuclei of their constituent atoms. In a few moments you will be dead.470 def annotationsFor(link, idea):
341 | There is a radiation suit on the floor.471 """
342 | > take radiation suit472 Produce an iterator of annotations to be applied to a link whose source
343 | You take the radiation suit.473 or target is the L{Idea} that this L{ILinkAnnotator} has been applied
344 | Your internal organs hurt a lot.474 to.
345 | > wear radiation suit475 """
346 | You wear the radiation suit.476
347 | You start to feel better.477
348478
349 That is to say, a factory for objects which take the place of elements in479class ILocationLinkAnnotator(Interface):
350 the result of L{IThing.findProviders} for the purpose of altering their480 """
351 behavior in some manner due to a particular property of the path in the481 L{ILocationLinkAnnotator} is a powerup interface to allow powerups for a
352 game object graph through which the original element would have been found.482 L{Thing} to act as L{ILinkAnnotator}s for every L{Thing} contained within
353483 it. This allows area-effect link annotators to be implemented simply,
354 Another example to consider is that of a pair of sunglasses worn by a484 without needing to monitor movement.
355 player: these might power up that player for IProxy so as to be able to485 """
356 proxy IVisible in such a way as to reduce glaring light.486
357 """487 def annotationsFor(link, idea):
358 # XXX: Perhaps add 'distance' here, so Fog can be implemented as an488 """
359 # IVisibility proxy which reduces the distance a observer can see.489 Produce an iterator of annotations to be applied to a link whose source
360 def proxy(iface, facet):490 or target is an L{Idea} of a L{Thing} contained in the L{Thing} that
361 """491 this L{ILocationLinkAnnotator} has been applied to.
362 Proxy C{facet} which provides C{iface}.492 """
363493
364 @param facet: A candidate for inclusion in the set of objects returned494
365 by findProviders.495
366496class IRetriever(Interface):
367 @return: Either a provider of C{iface} or C{None}. If C{None} is497 """
368 returned, then the object will not be returned from findProviders.498 An L{IRetriever} examines a L{Path} and retrieves a desirable object from
369 """499 it to yield from L{Idea.obtain}, if the L{Path} is suitable.
370500
371501 Every L{IRetriever} has a different definition of suitability; you should
372502 examine some of their implementations for more detail.
373class ILocationProxy(Interface):503 """
374 """504
375 Similar to L{IProxy}, except the pathway between the observer and the505 def retrieve(path):
376 target is not considered: instead, all targets are wrapped by all506 """
377 ILocationProxy providers on their location.507 Return the suitable object described by C{path}, or None if the path is
378 """508 unsuitable for this retriever's purposes.
379509 """
380 def proxy(iface, facet):510
381 """511 def shouldKeepGoing(path):
382 Proxy C{facet} which provides C{iface}.512 """
383513 Inspect a L{Path}. True if it should be searched, False if not.
384 @param facet: A candidate B{contained by the location on which this is514 """
385 a powerup} for inclusion in the set of objects returned by515
386 findProviders.516
387517 def objectionsTo(path, result):
388 @return: Either a provider of C{iface} or C{None}. If C{None} is518 """
389 returned, then the object will not be returned from findProviders.519 @return: an iterator of IWhyNot, if you object to this result being
390 """520 yielded.
521 """
522
523
524
525class IContainmentRelationship(Interface):
526 """
527 Indicate the containment of one idea within another, via a link.
528
529 This is an annotation interface, used to annotate L{iimaginary.idea.Link}s
530 to specify that the relationship between linked objects is one of
531 containment. In other words, the presence of an
532 L{IContainmentRelationship} annotation on a L{iimaginary.idea.Link}
533 indicates that the target of that link is contained by the source of that
534 link.
535 """
536
537 containedBy = Attribute(
538 """
539 A reference to the L{IContainer} which contains the target of the link
540 that this L{IContainmentRelationship} annotates.
541 """)
391542
392543
393544
@@ -395,6 +546,7 @@
395 """546 """
396 A thing which can be seen.547 A thing which can be seen.
397 """548 """
549
398 def visualize():550 def visualize():
399 """551 """
400 Return an IConcept which represents the visible aspects of this552 Return an IConcept which represents the visible aspects of this
@@ -402,6 +554,15 @@
402 """554 """
403555
404556
557 def isViewOf(thing):
558 """
559 Is this L{IVisible} a view of a given L{Thing}?
560
561 @rtype: L{bool}
562 """
563
564
565
405566
406class ILightSource(Interface):567class ILightSource(Interface):
407 """568 """
@@ -478,6 +639,36 @@
478 """)639 """)
479640
480641
642 def nowWornBy(wearer):
643 """
644 This article of clothing is now being worn by C{wearer}.
645
646 @param wearer: The wearer of the clothing.
647
648 @type wearer: L{IClothingWearer}
649 """
650
651
652 def noLongerWorn():
653 """
654 This article of clothing is no longer being worn.
655 """
656
657
658
659class ISittable(Interface):
660 """
661 Something you can sit on.
662 """
663
664 def seat(sitterThing):
665 """
666 @param sitterThing: The person sitting down on this sittable surface.
667
668 @type sitterThing: L{imaginary.objects.Thing}
669 """
670
671
481672
482class IDescriptor(IThingPowerUp):673class IDescriptor(IThingPowerUp):
483 """674 """
@@ -494,4 +685,39 @@
494 """685 """
495686
496687
688class IWhyNot(Interface):
689 """
690 This interface is an idea link annotation interface, designed to be applied
691 by L{ILinkAnnotator}s, that indicates a reason why a given path cannot
692 yield a provider. This is respected by L{imaginary.idea.ProviderOf}.
693 """
694
695 def tellMeWhyNot():
696 """
697 Return something adaptable to L{IConcept}, that explains why this link
698 is unsuitable for producing results. For example, the string "It's too
699 dark in here."
700 """
701
702
703
704class IDistance(Interface):
705 """
706 A link annotation that provides a distance.
707 """
708
709 distance = Attribute("floating point, distance in meters")
710
711
712
713class IElectromagneticMedium(Interface):
714 """
715 A medium through which electromagnetic radiation may or may not pass; used
716 as a link annotation.
717 """
718
719 def isOpaque():
720 """
721 Will this propagate radiation the visible spectrum?
722 """
497723
498724
=== modified file 'Imaginary/imaginary/language.py'
--- Imaginary/imaginary/language.py 2008-05-04 21:35:09 +0000
+++ Imaginary/imaginary/language.py 2011-09-16 20:42:26 +0000
@@ -136,6 +136,17 @@
136 Concepts will be ordered by the C{preferredOrder} class attribute.136 Concepts will be ordered by the C{preferredOrder} class attribute.
137 Concepts not named in this list will appear last in an unpredictable137 Concepts not named in this list will appear last in an unpredictable
138 order.138 order.
139
140 @ivar name: The name of the thing being described.
141
142 @ivar description: A basic description of the thing being described, the
143 first thing to show up.
144
145 @ivar exits: An iterable of L{IExit}, to be listed as exits in the
146 description.
147
148 @ivar others: An iterable of L{IDescriptionContributor} that will
149 supplement the description.
139 """150 """
140 implements(iimaginary.IConcept)151 implements(iimaginary.IConcept)
141152
@@ -167,6 +178,7 @@
167 description = (T.fg.green, self.description, u'\n')178 description = (T.fg.green, self.description, u'\n')
168179
169 descriptionConcepts = []180 descriptionConcepts = []
181
170 for pup in self.others:182 for pup in self.others:
171 descriptionConcepts.append(pup.conceptualize())183 descriptionConcepts.append(pup.conceptualize())
172184
173185
=== modified file 'Imaginary/imaginary/objects.py'
--- Imaginary/imaginary/objects.py 2009-06-29 04:03:17 +0000
+++ Imaginary/imaginary/objects.py 2011-09-16 20:42:26 +0000
@@ -1,4 +1,12 @@
1# -*- test-case-name: imaginary.test.test_objects -*-1# -*- test-case-name: imaginary.test.test_objects,imaginary.test.test_actions -*-
2
3"""
4This module contains the core, basic objects in Imaginary.
5
6L{imaginary.objects} contains the physical simulation (L{Thing}), objects
7associated with scoring (L{Points}), and the basic actor interface which allows
8the user to perform simple actions (L{Actor}).
9"""
210
3from __future__ import division11from __future__ import division
412
@@ -9,6 +17,7 @@
9from twisted.python import reflect, components17from twisted.python import reflect, components
1018
11from epsilon import structlike19from epsilon import structlike
20from epsilon.remember import remembered
1221
13from axiom import item, attributes22from axiom import item, attributes
1423
@@ -16,17 +25,9 @@
1625
17from imaginary.enhancement import Enhancement as _Enhancement26from imaginary.enhancement import Enhancement as _Enhancement
1827
19def merge(d1, *dn):28from imaginary.idea import (
20 """29 Idea, Link, Proximity, Reachable, ProviderOf, Named, AlsoKnownAs, CanSee,
21 da = {a: [1, 2]}30 Vector, DelegatingRetriever)
22 db = {b: [3]}
23 dc = {b: [5], c: [2, 4]}
24 merge(da, db, dc)
25 da == {a: [1, 2], b: [3, 5], c: [2, 4]}
26 """
27 for d in dn:
28 for (k, v) in d.iteritems():
29 d1.setdefault(k, []).extend(v)
3031
3132
32class Points(item.Item):33class Points(item.Item):
@@ -66,8 +67,58 @@
66 return self.current67 return self.current
6768
6869
70
69class Thing(item.Item):71class Thing(item.Item):
70 implements(iimaginary.IThing, iimaginary.IVisible)72 """
73 A L{Thing} is a physically located object in the game world.
74
75 While a game object in Imaginary is composed of many different Python
76 objects, the L{Thing} is the central object that most game objects will
77 share. It's central for several reasons.
78
79 First, a L{Thing} is connected to the point-of-interest simulation that
80 makes up the environment of an Imaginary game. A L{Thing} has a location,
81 and a L{Container} can list the L{Thing}s located within it, which is how
82 you can see the objects in your surroundings or a container.
83
84 Each L{Thing} has an associated L{Idea}, which provides the graph that can
85 be traversed to find other L{Thing}s to be the target for actions or
86 events.
87
88 A L{Thing} is also the object which serves as the persistent nexus of
89 powerups that define behavior. An L{_Enhancement} is a powerup for a
90 L{Thing}. L{Thing}s can be powered up for a number of different interfaces:
91
92 - L{iimaginary.IMovementRestriction}, for preventing the L{Thing} from
93 moving around,
94
95 - L{iimaginary.ILinkContributor}, which can provide links from the
96 L{Thing}'s L{Idea} to other L{Idea}s,
97
98 - L{iimaginary.ILinkAnnotator}, which can provide annotations on links
99 incoming to or outgoing from the L{Thing}'s L{Idea},
100
101 - L{iimaginary.ILocationLinkAnnotator}, which can provide annotations on
102 links to or from any L{Thing}'s L{Idea} which is ultimately located
103 within the powered-up L{Thing}.
104
105 - L{iimaginary.IDescriptionContributor}, which provide components of
106 the L{Thing}'s description when viewed with the L{Look} action.
107
108 - and finally, any interface used as a target for an action or event.
109
110 The way this all fits together is as follows: if you wanted to make a
111 shirt, for example, you would make a L{Thing}, give it an appropriate name
112 and description, make a new L{Enhancement} class which implements
113 L{IMovementRestriction} to prevent the shirt from moving around unless it
114 is correctly in the un-worn state, and then power up that L{Enhancement} on
115 the L{Thing}. This particular example is implemented in
116 L{imaginary.garments}, but almost any game-logic implementation will follow
117 this general pattern.
118 """
119
120 implements(iimaginary.IThing, iimaginary.IVisible, iimaginary.INameable,
121 iimaginary.ILinkAnnotator, iimaginary.ILinkContributor)
71122
72 weight = attributes.integer(doc="""123 weight = attributes.integer(doc="""
73 Units of weight of this object.124 Units of weight of this object.
@@ -106,117 +157,114 @@
106157
107158
108 def links(self):159 def links(self):
109 d = {self.name.lower(): [self]}160 """
110 if self.location is not None:161 Implement L{ILinkContributor.links()} by offering a link to this
111 merge(d, {self.location.name: [self.location]})162 L{Thing}'s C{location} (if it has one).
163 """
164 # since my link contribution is to go up (out), put this last, since
165 # containment (i.e. going down (in)) is a powerup. we want to explore
166 # contained items first.
112 for pup in self.powerupsFor(iimaginary.ILinkContributor):167 for pup in self.powerupsFor(iimaginary.ILinkContributor):
113 merge(d, pup.links())168 for link in pup.links():
114 return d169 # wooo composition
115170 yield link
116171 if self.location is not None:
117 thing = property(lambda self: self)172 l = Link(self.idea, self.location.idea)
118173 # XXX this incorrectly identifies any container with an object in
119 _ProviderStackElement = structlike.record('distance stability target proxies')174 # it as 'here', since it doesn't distinguish the observer; however,
175 # cycle detection will prevent these links from being considered in
176 # any case I can think of. However, 'here' is ambiguous in the
177 # case where you are present inside a container, and that should
178 # probably be dealt with.
179 l.annotate([AlsoKnownAs('here')])
180 yield l
181
182
183 def allAnnotators(self):
184 """
185 A generator which yields all L{iimaginary.ILinkAnnotator} providers
186 that should affect this L{Thing}'s L{Idea}. This includes:
187
188 - all L{iimaginary.ILocationLinkAnnotator} powerups on all
189 L{Thing}s which contain this L{Thing} (the container it's in, the
190 room its container is in, etc)
191
192 - all L{iimaginary.ILinkAnnotator} powerups on this L{Thing}.
193 """
194 loc = self
195 while loc is not None:
196 # TODO Test the loc is None case
197 if loc is not None:
198 for pup in loc.powerupsFor(iimaginary.ILocationLinkAnnotator):
199 yield pup
200 loc = loc.location
201 for pup in self.powerupsFor(iimaginary.ILinkAnnotator):
202 yield pup
203
204
205 def annotationsFor(self, link, idea):
206 """
207 Implement L{ILinkAnnotator.annotationsFor} to consult each
208 L{ILinkAnnotator} for this L{Thing}, as defined by
209 L{Thing.allAnnotators}, and yield each annotation for the given L{Link}
210 and L{Idea}.
211 """
212 for annotator in self.allAnnotators():
213 for annotation in annotator.annotationsFor(link, idea):
214 yield annotation
215
216
217 @remembered
218 def idea(self):
219 """
220 An L{Idea} which represents this L{Thing}.
221 """
222 idea = Idea(self)
223 idea.linkers.append(self)
224 idea.annotators.append(self)
225 return idea
226
120227
121 def findProviders(self, interface, distance):228 def findProviders(self, interface, distance):
122229 """
123 # Dictionary keyed on Thing instances used to ensure any particular230 Temporary emulation of the old way of doing things so that I can
124 # Thing is yielded at most once.231 surgically replace findProviders.
125 seen = {}232 """
126233 return self.idea.obtain(
127 # Dictionary keyed on Thing instances used to ensure any particular234 Proximity(distance, CanSee(ProviderOf(interface))))
128 # Thing only has its links inspected at most once.235
129 visited = {self: True}236
130237 def obtainOrReportWhyNot(self, retriever):
131 # Load proxies that are installed directly on this Thing as well as238 """
132 # location proxies on this Thing's location: if self is adaptable to239 Invoke L{Idea.obtain} on C{self.idea} with the given C{retriever}.
133 # interface, use them as arguments to _applyProxies and yield a proxied240
134 # and adapted facet of self.241 If no results are yielded, then investigate the reasons why no results
135 facet = interface(self, None)242 have been yielded, and raise an exception describing one of them.
136 initialProxies = list(self.powerupsFor(iimaginary.IProxy))243
137 locationProxies = set()244 Objections may be registered by:
138 if self.location is not None:245
139 locationProxies.update(set(self.location.powerupsFor(iimaginary.ILocationProxy)))246 - an L{iimaginary.IWhyNot} annotation on any link traversed in the
140 if facet is not None:247 attempt to discover results, or,
141 seen[self] = True248
142 proxiedFacet = self._applyProxies(locationProxies, initialProxies, facet, interface)249 - an L{iimaginary.IWhyNot} yielded by the given C{retriever}'s
143 if proxiedFacet is not None:250 L{iimaginary.IRetriever.objectionsTo} method.
144 yield proxiedFacet251
145252 @return: a list of objects returned by C{retriever.retrieve}
146 # Toss in for the _ProviderStackElement list/stack. Ensures ordering253
147 # in the descendTo list remains consistent with a breadth-first254 @rtype: C{list}
148 # traversal of links (there is probably a better way to do this).255
149 stabilityHelper = 1256 @raise eimaginary.ActionFailure: if no results are available, and an
150257 objection has been registered.
151 # Set up a stack of Things to ask for links to visit - start with just258 """
152 # ourself and the proxies we have found already.259 obt = self.idea.obtain(retriever)
153 descendTo = [self._ProviderStackElement(distance, 0, self, initialProxies)]260 results = list(obt)
154261 if not results:
155 while descendTo:262 reasons = list(obt.reasonsWhyNot)
156 element = descendTo.pop()263 if reasons:
157 distance, target, proxies = (element.distance, element.target,264 raise eimaginary.ActionFailure(events.ThatDoesntWork(
158 element.proxies)265 actor=self,
159 links = target.links().items()266 actorMessage=reasons[0].tellMeWhyNot()))
160 links.sort()267 return results
161 for (linkName, linkedThings) in links:
162 for linkedThing in linkedThings:
163 if distance:
164 if linkedThing not in visited:
165 # A Thing which was linked and has not yet been
166 # visited. Create a new list of proxies from the
167 # current list and any which it has and push this
168 # state onto the stack. Also extend the total list
169 # of location proxies with any location proxies it
170 # has.
171 visited[linkedThing] = True
172 stabilityHelper += 1
173 locationProxies.update(set(linkedThing.powerupsFor(iimaginary.ILocationProxy)))
174 proxies = proxies + list(
175 linkedThing.powerupsFor(iimaginary.IProxy))
176 descendTo.append(self._ProviderStackElement(
177 distance - 1, stabilityHelper,
178 linkedThing, proxies))
179
180 # If the linked Thing hasn't been yielded before and is
181 # adaptable to the desired interface, wrap it in the
182 # appropriate proxies and yield it.
183 facet = interface(linkedThing, None)
184 if facet is not None and linkedThing not in seen:
185 seen[linkedThing] = True
186 proxiedFacet = self._applyProxies(locationProxies, proxies, facet, interface)
187 if proxiedFacet is not None:
188 yield proxiedFacet
189
190 # Re-order anything we've appended so that we visit it in the right
191 # order.
192 descendTo.sort()
193
194
195 def _applyProxies(self, locationProxies, proxies, obj, interface):
196 # Extremely pathetic algorithm - loop over all location proxies we have
197 # seen and apply any which belong to the location of the target object.
198 # This could do with some serious optimization.
199 for proxy in locationProxies:
200 if iimaginary.IContainer(proxy.thing).contains(obj.thing) or proxy.thing is obj.thing:
201 obj = proxy.proxy(obj, interface)
202 if obj is None:
203 return None
204
205 # Loop over the other proxies and simply apply them in turn, giving up
206 # as soon as one eliminates the object entirely.
207 for proxy in proxies:
208 obj = proxy.proxy(obj, interface)
209 if obj is None:
210 return None
211
212 return obj
213
214
215 def proxiedThing(self, thing, interface, distance):
216 for prospectiveFacet in self.findProviders(interface, distance):
217 if prospectiveFacet.thing is thing:
218 return prospectiveFacet
219 raise eimaginary.ThingNotFound(thing)
220268
221269
222 def search(self, distance, interface, name):270 def search(self, distance, interface, name):
@@ -224,59 +272,59 @@
224 Retrieve game objects answering to the given name which provide the272 Retrieve game objects answering to the given name which provide the
225 given interface and are within the given distance.273 given interface and are within the given distance.
226274
227 @type distance: C{int}
228 @param distance: How many steps to traverse (note: this is wrong, it275 @param distance: How many steps to traverse (note: this is wrong, it
229 will become a real distance-y thing with real game-meaning someday).276 will become a real distance-y thing with real game-meaning
277 someday).
278 @type distance: C{float}
230279
231 @param interface: The interface which objects within the required range280 @param interface: The interface which objects within the required range
232 must be adaptable to in order to be returned.281 must be adaptable to in order to be returned.
233282
283 @param name: The name of the stuff.
234 @type name: C{str}284 @type name: C{str}
235 @param name: The name of the stuff.
236285
237 @return: An iterable of L{iimaginary.IThing} providers which are found.286 @return: An iterable of L{iimaginary.IThing} providers which are found.
238 """287 """
239 # TODO - Move this into the action system. It is about finding things288 return self.obtainOrReportWhyNot(
240 # using strings, which isn't what the action system is all about, but289 Proximity(
241 # the action system is where we do that sort of thing now. -exarkun290 distance,
242 extras = []291 Reachable(Named(name, CanSee(ProviderOf(interface)), self))))
243
244 container = iimaginary.IContainer(self.location, None)
245 if container is not None:
246 potentialExit = container.getExitNamed(name, None)
247 if potentialExit is not None:
248 try:
249 potentialThing = self.proxiedThing(
250 potentialExit.toLocation, interface, distance)
251 except eimaginary.ThingNotFound:
252 pass
253 else:
254 yield potentialThing
255
256 if name == "me" or name == "self":
257 facet = interface(self, None)
258 if facet is not None:
259 extras.append(self)
260
261 if name == "here" and self.location is not None:
262 facet = interface(self.location, None)
263 if facet is not None:
264 extras.append(self.location)
265
266 for res in self.findProviders(interface, distance):
267 if res.thing in extras:
268 yield res
269 elif res.thing.knownAs(name):
270 yield res
271292
272293
273 def moveTo(self, where, arrivalEventFactory=None):294 def moveTo(self, where, arrivalEventFactory=None):
274 """295 """
275 @see: L{iimaginary.IThing.moveTo}.296 Implement L{iimaginary.IThing.moveTo} to change the C{location} of this
297 L{Thing} to a new L{Thing}, broadcasting an L{events.DepartureEvent} to
298 note this object's departure from its current C{location}.
299
300 Before moving it, invoke each L{IMovementRestriction} powerup on this
301 L{Thing} to allow them to prevent this movement.
276 """302 """
277 if where is self.location:303 whereContainer = iimaginary.IContainer(where, None)
304 if (whereContainer is
305 iimaginary.IContainer(self.location, None)):
306 # Early out if I'm being moved to the same location that I was
307 # already in.
278 return308 return
309 if whereContainer is None:
310 whereThing = None
311 else:
312 whereThing = whereContainer.thing
313 if whereThing is not None and whereThing.location is self:
314 # XXX should be checked against _all_ locations of whereThing, not
315 # just the proximate one.
316
317 # XXX actor= here is wrong, who knows who is moving this thing.
318 raise eimaginary.ActionFailure(events.ThatDoesntWork(
319 actor=self,
320 actorMessage=[
321 language.Noun(where.thing).definiteNounPhrase()
322 .capitalizeConcept(),
323 " won't fit inside itself."]))
324
279 oldLocation = self.location325 oldLocation = self.location
326 for restriction in self.powerupsFor(iimaginary.IMovementRestriction):
327 restriction.movementImminent(self, where)
280 if oldLocation is not None:328 if oldLocation is not None:
281 events.DepartureEvent(oldLocation, self).broadcast()329 events.DepartureEvent(oldLocation, self).broadcast()
282 if where is not None:330 if where is not None:
@@ -290,20 +338,33 @@
290 iimaginary.IContainer(oldLocation).remove(self)338 iimaginary.IContainer(oldLocation).remove(self)
291339
292340
293 def knownAs(self, name):341 def knownTo(self, observer, name):
294 """342 """
295 Return C{True} if C{name} might refer to this L{Thing}, C{False} otherwise.343 Implement L{INameable.knownTo} to compare the name to L{Thing.name} as
296344 well as few constant values based on the relationship of the observer
297 XXX - See #2604.345 to this L{Thing}, such as 'me', 'self', and 'here'.
298 """346
347 @param observer: an L{IThing} provider.
348 """
349
350 mine = self.name.lower()
299 name = name.lower()351 name = name.lower()
300 mine = self.name.lower()352 if name == mine or name in mine.split():
301 return name == mine or name in mine.split()353 return True
354 if observer == self:
355 if name in ('me', 'self'):
356 return True
357 return False
302358
303359
304 # IVisible360 # IVisible
305 def visualize(self):361 def visualize(self):
306 container = iimaginary.IContainer(self.thing, None)362 """
363 Implement L{IVisible.visualize} to return a
364 L{language.DescriptionConcept} that describes this L{Thing}, including
365 all its L{iimaginary.IDescriptionContributor} powerups.
366 """
367 container = iimaginary.IContainer(self, None)
307 if container is not None:368 if container is not None:
308 exits = list(container.getExits())369 exits = list(container.getExits())
309 else:370 else:
@@ -313,8 +374,41 @@
313 self.name,374 self.name,
314 self.description,375 self.description,
315 exits,376 exits,
377 # Maybe we should listify this or something; see
378 # http://divmod.org/trac/ticket/2905
316 self.powerupsFor(iimaginary.IDescriptionContributor))379 self.powerupsFor(iimaginary.IDescriptionContributor))
317components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(), Thing, iimaginary.IConcept)380
381
382 def isViewOf(self, thing):
383 """
384 Implement L{IVisible.isViewOf} to return C{True} if its argument is
385 C{self}. In other words, this L{Thing} is only a view of itself.
386 """
387 return (thing is self)
388
389components.registerAdapter(lambda thing: language.Noun(thing).nounPhrase(),
390 Thing,
391 iimaginary.IConcept)
392
393
394def _eventuallyContains(containerThing, containeeThing):
395 """
396 Does a container, or any containers within it (or any containers within any
397 of those, etc etc) contain some object?
398
399 @param containeeThing: The L{Thing} which may be contained.
400
401 @param containerThing: The L{Thing} which may have a L{Container} that
402 contains C{containeeThing}.
403
404 @return: L{True} if the containee is contained by the container.
405 """
406 while containeeThing is not None:
407 if containeeThing is containerThing:
408 return True
409 containeeThing = containeeThing.location
410 return False
411
318412
319413
320414
@@ -323,18 +417,41 @@
323 u"west": u"east",417 u"west": u"east",
324 u"northwest": u"southeast",418 u"northwest": u"southeast",
325 u"northeast": u"southwest"}419 u"northeast": u"southwest"}
326for (k, v) in OPPOSITE_DIRECTIONS.items():420
327 OPPOSITE_DIRECTIONS[v] = k421
422def _populateOpposite():
423 """
424 Populate L{OPPOSITE_DIRECTIONS} with inverse directions.
425
426 (Without leaking any loop locals into the global scope, thank you very
427 much.)
428 """
429 for (k, v) in OPPOSITE_DIRECTIONS.items():
430 OPPOSITE_DIRECTIONS[v] = k
431
432_populateOpposite()
433
328434
329435
330class Exit(item.Item):436class Exit(item.Item):
331 fromLocation = attributes.reference(doc="""437 """
332 Where this exit leads from.438 An L{Exit} is an oriented pathway between two L{Thing}s which each
333 """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing)439 represent a room.
334440 """
335 toLocation = attributes.reference(doc="""441
336 Where this exit leads to.442 implements(iimaginary.INameable, iimaginary.IExit)
337 """, allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Thing)443
444 fromLocation = attributes.reference(
445 doc="""
446 Where this exit leads from.
447 """, allowNone=False,
448 whenDeleted=attributes.reference.CASCADE, reftype=Thing)
449
450 toLocation = attributes.reference(
451 doc="""
452 Where this exit leads to.
453 """, allowNone=False,
454 whenDeleted=attributes.reference.CASCADE, reftype=Thing)
338455
339 name = attributes.text(doc="""456 name = attributes.text(doc="""
340 What this exit is called/which direction it is in.457 What this exit is called/which direction it is in.
@@ -344,15 +461,70 @@
344 The reverse exit object, if one exists.461 The reverse exit object, if one exists.
345 """)462 """)
346463
347464 distance = attributes.ieee754_double(
348 def link(cls, a, b, forwardName, backwardName=None):465 doc="""
466 How far, in meters, does a user have to travel to traverse this exit?
467 """, allowNone=False, default=1.0)
468
469 def knownTo(self, observer, name):
470 """
471 Implement L{iimaginary.INameable.knownTo} to identify this L{Exit} as
472 its C{name} attribute.
473 """
474 return name == self.name
475
476
477 def traverse(self, thing):
478 """
479 Implement L{iimaginary.IExit} to move the given L{Thing} to this
480 L{Exit}'s C{toLocation}.
481 """
482 if self.sibling is not None:
483 arriveDirection = self.sibling.name
484 else:
485 arriveDirection = OPPOSITE_DIRECTIONS.get(self.name)
486
487 thing.moveTo(
488 self.toLocation,
489 arrivalEventFactory=lambda player: events.MovementArrivalEvent(
490 thing=thing,
491 origin=None,
492 direction=arriveDirection))
493
494
495 # XXX This really needs to be renamed now that links are a thing.
496 @classmethod
497 def link(cls, a, b, forwardName, backwardName=None, distance=1.0):
498 """
499 Create two L{Exit}s connecting two rooms.
500
501 @param a: The first room.
502
503 @type a: L{Thing}
504
505 @param b: The second room.
506
507 @type b: L{Thing}
508
509 @param forwardName: The name of the link going from C{a} to C{b}. For
510 example, u'east'.
511
512 @type forwardName: L{unicode}
513
514 @param backwardName: the name of the link going from C{b} to C{a}. For
515 example, u'west'. If not provided or L{None}, this will be
516 computed based on L{OPPOSITE_DIRECTIONS}.
517
518 @type backwardName: L{unicode}
519 """
349 if backwardName is None:520 if backwardName is None:
350 backwardName = OPPOSITE_DIRECTIONS[forwardName]521 backwardName = OPPOSITE_DIRECTIONS[forwardName]
351 me = cls(store=a.store, fromLocation=a, toLocation=b, name=forwardName)522 forward = cls(store=a.store, fromLocation=a, toLocation=b,
352 him = cls(store=b.store, fromLocation=b, toLocation=a, name=backwardName)523 name=forwardName, distance=distance)
353 me.sibling = him524 backward = cls(store=b.store, fromLocation=b, toLocation=a,
354 him.sibling = me525 name=backwardName, distance=distance)
355 link = classmethod(link)526 forward.sibling = backward
527 backward.sibling = forward
356528
357529
358 def destroy(self):530 def destroy(self):
@@ -361,29 +533,79 @@
361 self.deleteFromStore()533 self.deleteFromStore()
362534
363535
364 # NOTHING536 @remembered
537 def exitIdea(self):
538 """
539 This property is the L{Idea} representing this L{Exit}; this is a
540 fairly simple L{Idea} that will link only to the L{Exit.toLocation}
541 pointed to by this L{Exit}, with a distance annotation indicating the
542 distance traversed to go through this L{Exit}.
543 """
544 x = Idea(self)
545 x.linkers.append(self)
546 return x
547
548
549 def links(self):
550 """
551 Generate a link to the location that this exit points at.
552
553 @return: an iterator which yields a single L{Link}, annotated with a
554 L{Vector} that indicates a distance of 1.0 (a temporary measure,
555 since L{Exit}s don't have distances yet) and a direction of this
556 exit's C{name}.
557 """
558 l = Link(self.exitIdea, self.toLocation.idea)
559 l.annotate([Vector(self.distance, self.name),
560 # We annotate this link with ourselves because the 'Named'
561 # retriever will use the last link in the path to determine
562 # if an object has any aliases. We want this direction
563 # name to be an alias for the room itself as well as the
564 # exit, so we want to annotate the link with an INameable.
565 # This also has an effect of annotating the link with an
566 # IExit, and possibly one day an IItem as well (if such a
567 # thing ever comes to exist), so perhaps we eventually want
568 # a wrapper which elides all references here except
569 # INameable since that's what we want. proxyForInterface
570 # perhaps? However, for the moment, the extra annotations
571 # do no harm, so we'll leave them there.
572 self])
573 yield l
574
575
365 def conceptualize(self):576 def conceptualize(self):
366 return language.ExpressList([u'the exit to ', language.Noun(self.toLocation).nounPhrase()])577 return language.ExpressList(
367components.registerAdapter(lambda exit: exit.conceptualize(), Exit, iimaginary.IConcept)578 [u'the exit to ', language.Noun(self.toLocation).nounPhrase()])
579
580components.registerAdapter(lambda exit: exit.conceptualize(),
581 Exit, iimaginary.IConcept)
582
583
584
585class ContainmentRelationship(structlike.record("containedBy")):
586 """
587 Implementation of L{iimaginary.IContainmentRelationship}. The interface
588 specifies no methods or attributes. See its documentation for more
589 information.
590 """
591 implements(iimaginary.IContainmentRelationship)
368592
369593
370594
371class Containment(object):595class Containment(object):
372 """Functionality for containment to be used as a mixin in Powerups.596 """