Merge lp:~jerith/divmod.org/short-directions-1215929 into lp:divmod.org

Proposed by Jeremy Thurgood
Status: Merged
Approved by: Jean-Paul Calderone
Approved revision: 2719
Merged at revision: 2717
Proposed branch: lp:~jerith/divmod.org/short-directions-1215929
Merge into: lp:divmod.org
Diff against target: 397 lines (+213/-52)
6 files modified
Imaginary/imaginary/action.py (+24/-8)
Imaginary/imaginary/objects.py (+12/-0)
Imaginary/imaginary/resources/help/bury (+2/-1)
Imaginary/imaginary/resources/help/dig (+2/-1)
Imaginary/imaginary/resources/help/go (+2/-1)
Imaginary/imaginary/test/test_actions.py (+171/-41)
To merge this branch: bzr merge lp:~jerith/divmod.org/short-directions-1215929
Reviewer Review Type Date Requested Status
Jean-Paul Calderone Approve
Review via email: mp+181830@code.launchpad.net

Description of the change

Adds some short aliases for standard directions.

To post a comment you must log in.
2716. By Jeremy Thurgood

camelCase, not separated_with_slugs.

2717. By Jeremy Thurgood

Fix up some tests.

Revision history for this message
Jean-Paul Calderone (exarkun) wrote :

I wonder about the change to explicitly use `pyparsing.LineEnd()` in the expression for `Go`. Am I correct in concluding from this change that commands are currently allowed to match lines as long as the command is a *prefix* of the line (potentially leaving unmatched text at the end)? I can imagine this arising here since none of the existing commands are as like to be prefixes of other commands or bogus inputs as the new "n", "e", "s", and "w". If this is the case it might make sense to fix this more generally by declaring (to us, the development team) that `Action.expr` implicitly has `pyparsing.LineEnd()` at the end and make `Action` automatically put it there for us.

Otherwise I think this is generally okay, but a couple more hopefully-straightforward comments:

  - there are help files in Imaginary/imaginary/resources/ that should be kept up-to-date with respect to the actual implementation of commands.
  - the unit tests should be broken up more so that each one is only testing one case (I know this does not reflect the prevailing style in Imaginary's test suite, sorry :/)
  - I want to mumble something about twisted.python.constants but that can clearly be delayed for a later ticket

Thanks for the contribution!

Revision history for this message
Jeremy Thurgood (jerith) wrote :

Commands currently do match prefixes, and the general `pyparsing.LineEnd()` in actions is a reasonable way to avoid doing this accidentally. We'll still either need an explicit `pyparsing.LineEnd()` in the `Go` expression or we'll need to move the `DIRECTION_LITERAL` option to the end of the expression. I'm inclined to go with the latter.

2718. By Jeremy Thurgood

More general end-of-line matching for actions.

2719. By Jeremy Thurgood

Update help text with direction aliases.

Revision history for this message
Jean-Paul Calderone (exarkun) wrote :

Thanks! This looks pretty good to me. A few points, all of which I'm happy to see addressed as follow-ups rather than directly in this branch:

  - There's still lots of room for shortening tests. A lot of dig, bury, and movement tests are still exercising multiple things in a single method.

  - There should be an explicit unit test for the new automatic `LineEnd` feature of Action.match

  - It occurs to me that the expression for `Go` is a bit repetitive. It might be nice to shorten that expression.

As I mentioned above, I don't think any of these are blockers for this branch. If you're happy with it as-is, please go ahead and merge (ping me if you don't have permission to do that yet). If you could file bugs for the above three points as well, that'd be great. :)

Thanks again.

Revision history for this message
Jean-Paul Calderone (exarkun) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Imaginary/imaginary/action.py'
2--- Imaginary/imaginary/action.py 2013-06-09 16:07:17 +0000
3+++ Imaginary/imaginary/action.py 2013-09-22 10:10:54 +0000
4@@ -160,15 +160,18 @@
5 @classmethod
6 def match(cls, player, line):
7 """
8- @return: a list of 2-tuples of all the results of parsing the given
9- C{line} using this L{Action} type's pyparsing C{expr} attribute, or
10- None if the expression does not match the given line.
11+ Parse the given C{line} using this L{Action} type's pyparsing C{expr}
12+ attribute. A C{pyparsing.LineEnd} is appended to C{expr} to avoid
13+ accidentally matching a prefix instead of the whole line.
14+
15+ @return: a list of 2-tuples of all the results of parsing, or None if
16+ the expression does not match the given line.
17
18 @param line: a line of user input to be interpreted as an action.
19
20 @see: L{imaginary.pyparsing}
21 """
22- return cls.expr.parseString(line)
23+ return (cls.expr + pyparsing.LineEnd()).parseString(line)
24
25
26 def do(self, player, line, **slots):
27@@ -788,11 +791,21 @@
28
29
30
31+_directionNames = objects.OPPOSITE_DIRECTIONS.keys()
32+_directionNames.extend(objects.DIRECTION_ALIASES.keys())
33+
34 DIRECTION_LITERAL = reduce(
35 operator.xor, [
36 pyparsing.Literal(d)
37- for d
38- in objects.OPPOSITE_DIRECTIONS]).setResultsName("direction")
39+ for d in _directionNames]).setResultsName("direction")
40+
41+
42+
43+def expandDirection(direction):
44+ """
45+ Expand direction aliases into the names of the directions they refer to.
46+ """
47+ return objects.DIRECTION_ALIASES.get(direction, direction)
48
49
50
51@@ -804,6 +817,7 @@
52 pyparsing.restOfLine.setResultsName("name"))
53
54 def do(self, player, line, direction, name):
55+ direction = expandDirection(direction)
56 if iimaginary.IContainer(player.thing.location).getExitNamed(direction, None) is not None:
57 raise eimaginary.ActionFailure(events.ThatDoesntMakeSense(
58 actor=player.thing,
59@@ -831,6 +845,7 @@
60 DIRECTION_LITERAL)
61
62 def do(self, player, line, direction):
63+ direction = expandDirection(direction)
64 for exit in iimaginary.IContainer(player.thing.location).getExits():
65 if exit.name == direction:
66 if exit.sibling is not None:
67@@ -858,13 +873,13 @@
68
69 class Go(Action):
70 expr = (
71- DIRECTION_LITERAL |
72 (pyparsing.Literal("go") + pyparsing.White() +
73 targetString("direction")) |
74 (pyparsing.Literal("enter") + pyparsing.White() +
75 targetString("direction")) |
76 (pyparsing.Literal("exit") + pyparsing.White() +
77- targetString("direction")))
78+ targetString("direction")) |
79+ DIRECTION_LITERAL)
80
81 actorInterface = iimaginary.IThing
82
83@@ -873,6 +888,7 @@
84 Identify a direction by having the player search for L{IExit}
85 providers that they can see and reach.
86 """
87+ directionName = expandDirection(directionName)
88 return player.obtainOrReportWhyNot(
89 Proximity(
90 3.0,
91
92=== modified file 'Imaginary/imaginary/objects.py'
93--- Imaginary/imaginary/objects.py 2013-07-24 22:20:29 +0000
94+++ Imaginary/imaginary/objects.py 2013-09-22 10:10:54 +0000
95@@ -433,6 +433,18 @@
96
97
98
99+DIRECTION_ALIASES = {
100+ u"n": u"north",
101+ u"s": u"south",
102+ u"w": u"west",
103+ u"e": u"east",
104+ u"nw": u"northwest",
105+ u"se": u"southeast",
106+ u"ne": u"northeast",
107+ u"sw": u"southwest"}
108+
109+
110+
111 class Exit(item.Item):
112 """
113 An L{Exit} is an oriented pathway between two L{Thing}s which each
114
115=== modified file 'Imaginary/imaginary/resources/help/bury'
116--- Imaginary/imaginary/resources/help/bury 2006-04-12 02:41:46 +0000
117+++ Imaginary/imaginary/resources/help/bury 2013-09-22 10:10:54 +0000
118@@ -2,4 +2,5 @@
119
120 Usage: bury <north | south | east | west>
121
122-Block off the exit in the indicated direction.
123+Block off the exit in the indicated direction. Short directions (n, s, e, w)
124+may be used in place of the full direction names.
125
126=== modified file 'Imaginary/imaginary/resources/help/dig'
127--- Imaginary/imaginary/resources/help/dig 2006-04-12 02:41:46 +0000
128+++ Imaginary/imaginary/resources/help/dig 2013-09-22 10:10:54 +0000
129@@ -5,4 +5,5 @@
130 Create a new location with the indicated name and create a two-way
131 passage between it and the current location. The exit will be in
132 the specified direction from the current location to the new
133-location.
134+location. Short directions (n, s, e, w) may be used in place of the
135+full direction names.
136
137=== modified file 'Imaginary/imaginary/resources/help/go'
138--- Imaginary/imaginary/resources/help/go 2006-04-12 02:41:46 +0000
139+++ Imaginary/imaginary/resources/help/go 2013-09-22 10:10:54 +0000
140@@ -2,4 +2,5 @@
141
142 Usage: go <north | south | east | west>
143
144-Travel in the indicated direction.
145+Travel in the indicated direction. Short directions (n, s, e, w) may be used
146+in place of the full direction names.
147
148=== modified file 'Imaginary/imaginary/test/test_actions.py'
149--- Imaginary/imaginary/test/test_actions.py 2013-07-26 00:20:17 +0000
150+++ Imaginary/imaginary/test/test_actions.py 2013-09-22 10:10:54 +0000
151@@ -423,7 +423,11 @@
152 self.assertEquals(x[5].groups(), ())
153
154
155- def testDig(self):
156+ def test_dig(self):
157+ """
158+ The I{dig} action creates a new location connected to the current
159+ location through an exit in the specified direction.
160+ """
161 self._test(
162 "dig west dark tunnel",
163 ["You create an exit."],
164@@ -447,45 +451,125 @@
165 "dig west boring tunnel",
166 ["There is already an exit in that direction."])
167
168- def testBury(self):
169- self._test(
170- "bury south",
171- ["There isn't an exit in that direction."])
172- self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])
173-
174- room = objects.Thing(store=self.store, name=u"destination", proper=True)
175- objects.Container.createFor(room, capacity=1000)
176- objects.Exit.link(room, self.location, u'north')
177-
178- self._test(
179- "bury south",
180- ["It's gone."],
181- ["Test Player destroyed the exit to destination."])
182-
183- self.assertEquals(
184- list(iimaginary.IContainer(self.location).getExits()),
185- [])
186-
187- self.assertEquals(
188- list(iimaginary.IContainer(room).getExits()),
189- [])
190-
191- objects.Exit.link(self.location, room, u'south')
192- self.observer.moveTo(room)
193-
194- self._test(
195- "bury south",
196- ["It's gone."],
197- ["The exit to Test Location crumbles and disappears."])
198- self.assertEquals(
199- list(iimaginary.IContainer(self.location).getExits()),
200- [])
201- self.assertEquals(
202- list(iimaginary.IContainer(room).getExits()),
203- [])
204-
205-
206- def testGo(self):
207+
208+ def test_digWithDirectionAliases(self):
209+ """
210+ The I{dig} action creates a new location connected to the current
211+ location through an exit in the specified direction even when that
212+ direction is an alias.
213+ """
214+ self._test(
215+ "dig w dark tunnel",
216+ ["You create an exit."],
217+ ["Test Player created an exit to the west."])
218+ room = iimaginary.IContainer(self.location).getExitNamed(u'west').toLocation
219+ self.assertEquals(room.name, u"dark tunnel")
220+ self.assertEquals(room.description, u'')
221+ self.assertIdentical(iimaginary.IContainer(room).getExitNamed(u'east').toLocation,
222+ self.location)
223+
224+ self._test(
225+ "dig e bright tunnel",
226+ ["You create an exit."],
227+ ["Test Player created an exit to the east."])
228+ room = iimaginary.IContainer(self.location).getExitNamed(u'east').toLocation
229+ self.assertEquals(room.name, u"bright tunnel")
230+ self.assertEquals(room.description, u'')
231+ self.assertIdentical(iimaginary.IContainer(room).getExitNamed(u'west').toLocation, self.location)
232+
233+ self._test(
234+ "dig w boring tunnel",
235+ ["There is already an exit in that direction."])
236+
237+
238+ def test_bury(self):
239+ """
240+ The I{bury} action destroys an exit in the specified direction.
241+ """
242+ self._test(
243+ "bury south",
244+ ["There isn't an exit in that direction."])
245+ self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])
246+
247+ room = objects.Thing(store=self.store, name=u"destination", proper=True)
248+ objects.Container.createFor(room, capacity=1000)
249+ objects.Exit.link(room, self.location, u'north')
250+
251+ self._test(
252+ "bury south",
253+ ["It's gone."],
254+ ["Test Player destroyed the exit to destination."])
255+
256+ self.assertEquals(
257+ list(iimaginary.IContainer(self.location).getExits()),
258+ [])
259+
260+ self.assertEquals(
261+ list(iimaginary.IContainer(room).getExits()),
262+ [])
263+
264+ objects.Exit.link(self.location, room, u'south')
265+ self.observer.moveTo(room)
266+
267+ self._test(
268+ "bury south",
269+ ["It's gone."],
270+ ["The exit to Test Location crumbles and disappears."])
271+ self.assertEquals(
272+ list(iimaginary.IContainer(self.location).getExits()),
273+ [])
274+ self.assertEquals(
275+ list(iimaginary.IContainer(room).getExits()),
276+ [])
277+
278+
279+ def test_buryWithDirectionAliases(self):
280+ """
281+ The I{bury} action destroys an exit in the specified direction even
282+ when that direction is an alias.
283+ """
284+ self._test(
285+ "bury s",
286+ ["There isn't an exit in that direction."])
287+ self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])
288+
289+ room = objects.Thing(store=self.store, name=u"destination", proper=True)
290+ objects.Container.createFor(room, capacity=1000)
291+ objects.Exit.link(room, self.location, u'north')
292+
293+ self._test(
294+ "bury s",
295+ ["It's gone."],
296+ ["Test Player destroyed the exit to destination."])
297+
298+ self.assertEquals(
299+ list(iimaginary.IContainer(self.location).getExits()),
300+ [])
301+
302+ self.assertEquals(
303+ list(iimaginary.IContainer(room).getExits()),
304+ [])
305+
306+ objects.Exit.link(self.location, room, u'south')
307+ self.observer.moveTo(room)
308+
309+ self._test(
310+ "bury s",
311+ ["It's gone."],
312+ ["The exit to Test Location crumbles and disappears."])
313+ self.assertEquals(
314+ list(iimaginary.IContainer(self.location).getExits()),
315+ [])
316+ self.assertEquals(
317+ list(iimaginary.IContainer(room).getExits()),
318+ [])
319+
320+
321+ def test_go(self):
322+ """
323+ The I{go} action moves the player through an exit in the specified
324+ direction.
325+ """
326 self._test(
327 "go west",
328 ["You can't go that way."])
329@@ -516,6 +600,41 @@
330 ["Test Player arrives from the west."])
331
332
333+ def test_goThroughDirectionAliases(self):
334+ """
335+ The I{go} action moves the player through an exit in the specified
336+ direction even when that direction is an alias.
337+ """
338+ self._test(
339+ "go w",
340+ ["You can't go that way."])
341+ self._test(
342+ "w",
343+ ["You can't go that way."])
344+
345+ room = objects.Thing(store=self.store, name=u"destination")
346+ objects.Container.createFor(room, capacity=1000)
347+ objects.Exit.link(self.location, room, u"west")
348+
349+ self._test(
350+ "w",
351+ [E("[ destination ]"),
352+ E("( east )"),
353+ ""],
354+ ["Test Player leaves west."])
355+
356+ self._test(
357+ "n",
358+ ["You can't go that way."])
359+ self._test(
360+ "go e",
361+ [E("[ Test Location ]"),
362+ E("( west )"),
363+ "Location for testing.",
364+ "Here, you see Observer Player."],
365+ ["Test Player arrives from the west."])
366+
367+
368 def test_goThroughOneWayExit(self):
369 """
370 Going through a one-way exit with a known direction will announce that
371@@ -559,7 +678,11 @@
372 self.assertCommandOutput("go east", [E("You can't go that way.")], [])
373
374
375- def testDirectionalMovement(self):
376+ def test_directionalMovement(self):
377+ """
378+ You can move through exits in standard directions by just specifying
379+ the direction.
380+ """
381 # A couple tweaks to state to make the test simpler
382 self.observer.location = None
383 self.location.description = None
384@@ -579,6 +702,13 @@
385 [E("[ ") + ".*" + E(" ]"), # Not testing room description
386 E("( ") + ".*" + E(" )"), # Just do enough to make sure it was not an error.
387 ""])
388+ shortDirections = ["nw", "n", "ne", "e", "w", "sw", "s", "se"]
389+ for d, rd in zip(shortDirections, reversed(shortDirections)):
390+ self._test(
391+ d,
392+ [E("[ ") + ".*" + E(" ]"), # Not testing room description
393+ E("( ") + ".*" + E(" )"), # Just do enough to make sure it was not an error.
394+ ""])
395
396
397 def test_scrutinize(self):

Subscribers

People subscribed via source and target branches

to all changes: