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
=== modified file 'Imaginary/imaginary/action.py'
--- Imaginary/imaginary/action.py 2013-06-09 16:07:17 +0000
+++ Imaginary/imaginary/action.py 2013-09-22 10:10:54 +0000
@@ -160,15 +160,18 @@
160 @classmethod160 @classmethod
161 def match(cls, player, line):161 def match(cls, player, line):
162 """162 """
163 @return: a list of 2-tuples of all the results of parsing the given163 Parse the given C{line} using this L{Action} type's pyparsing C{expr}
164 C{line} using this L{Action} type's pyparsing C{expr} attribute, or164 attribute. A C{pyparsing.LineEnd} is appended to C{expr} to avoid
165 None if the expression does not match the given line.165 accidentally matching a prefix instead of the whole line.
166
167 @return: a list of 2-tuples of all the results of parsing, or None if
168 the expression does not match the given line.
166169
167 @param line: a line of user input to be interpreted as an action.170 @param line: a line of user input to be interpreted as an action.
168171
169 @see: L{imaginary.pyparsing}172 @see: L{imaginary.pyparsing}
170 """173 """
171 return cls.expr.parseString(line)174 return (cls.expr + pyparsing.LineEnd()).parseString(line)
172175
173176
174 def do(self, player, line, **slots):177 def do(self, player, line, **slots):
@@ -788,11 +791,21 @@
788791
789792
790793
794_directionNames = objects.OPPOSITE_DIRECTIONS.keys()
795_directionNames.extend(objects.DIRECTION_ALIASES.keys())
796
791DIRECTION_LITERAL = reduce(797DIRECTION_LITERAL = reduce(
792 operator.xor, [798 operator.xor, [
793 pyparsing.Literal(d)799 pyparsing.Literal(d)
794 for d800 for d in _directionNames]).setResultsName("direction")
795 in objects.OPPOSITE_DIRECTIONS]).setResultsName("direction")801
802
803
804def expandDirection(direction):
805 """
806 Expand direction aliases into the names of the directions they refer to.
807 """
808 return objects.DIRECTION_ALIASES.get(direction, direction)
796809
797810
798811
@@ -804,6 +817,7 @@
804 pyparsing.restOfLine.setResultsName("name"))817 pyparsing.restOfLine.setResultsName("name"))
805818
806 def do(self, player, line, direction, name):819 def do(self, player, line, direction, name):
820 direction = expandDirection(direction)
807 if iimaginary.IContainer(player.thing.location).getExitNamed(direction, None) is not None:821 if iimaginary.IContainer(player.thing.location).getExitNamed(direction, None) is not None:
808 raise eimaginary.ActionFailure(events.ThatDoesntMakeSense(822 raise eimaginary.ActionFailure(events.ThatDoesntMakeSense(
809 actor=player.thing,823 actor=player.thing,
@@ -831,6 +845,7 @@
831 DIRECTION_LITERAL)845 DIRECTION_LITERAL)
832846
833 def do(self, player, line, direction):847 def do(self, player, line, direction):
848 direction = expandDirection(direction)
834 for exit in iimaginary.IContainer(player.thing.location).getExits():849 for exit in iimaginary.IContainer(player.thing.location).getExits():
835 if exit.name == direction:850 if exit.name == direction:
836 if exit.sibling is not None:851 if exit.sibling is not None:
@@ -858,13 +873,13 @@
858873
859class Go(Action):874class Go(Action):
860 expr = (875 expr = (
861 DIRECTION_LITERAL |
862 (pyparsing.Literal("go") + pyparsing.White() +876 (pyparsing.Literal("go") + pyparsing.White() +
863 targetString("direction")) |877 targetString("direction")) |
864 (pyparsing.Literal("enter") + pyparsing.White() +878 (pyparsing.Literal("enter") + pyparsing.White() +
865 targetString("direction")) |879 targetString("direction")) |
866 (pyparsing.Literal("exit") + pyparsing.White() +880 (pyparsing.Literal("exit") + pyparsing.White() +
867 targetString("direction")))881 targetString("direction")) |
882 DIRECTION_LITERAL)
868883
869 actorInterface = iimaginary.IThing884 actorInterface = iimaginary.IThing
870885
@@ -873,6 +888,7 @@
873 Identify a direction by having the player search for L{IExit}888 Identify a direction by having the player search for L{IExit}
874 providers that they can see and reach.889 providers that they can see and reach.
875 """890 """
891 directionName = expandDirection(directionName)
876 return player.obtainOrReportWhyNot(892 return player.obtainOrReportWhyNot(
877 Proximity(893 Proximity(
878 3.0,894 3.0,
879895
=== modified file 'Imaginary/imaginary/objects.py'
--- Imaginary/imaginary/objects.py 2013-07-24 22:20:29 +0000
+++ Imaginary/imaginary/objects.py 2013-09-22 10:10:54 +0000
@@ -433,6 +433,18 @@
433433
434434
435435
436DIRECTION_ALIASES = {
437 u"n": u"north",
438 u"s": u"south",
439 u"w": u"west",
440 u"e": u"east",
441 u"nw": u"northwest",
442 u"se": u"southeast",
443 u"ne": u"northeast",
444 u"sw": u"southwest"}
445
446
447
436class Exit(item.Item):448class Exit(item.Item):
437 """449 """
438 An L{Exit} is an oriented pathway between two L{Thing}s which each450 An L{Exit} is an oriented pathway between two L{Thing}s which each
439451
=== modified file 'Imaginary/imaginary/resources/help/bury'
--- Imaginary/imaginary/resources/help/bury 2006-04-12 02:41:46 +0000
+++ Imaginary/imaginary/resources/help/bury 2013-09-22 10:10:54 +0000
@@ -2,4 +2,5 @@
22
3Usage: bury <north | south | east | west>3Usage: bury <north | south | east | west>
44
5Block off the exit in the indicated direction.5Block off the exit in the indicated direction. Short directions (n, s, e, w)
6may be used in place of the full direction names.
67
=== modified file 'Imaginary/imaginary/resources/help/dig'
--- Imaginary/imaginary/resources/help/dig 2006-04-12 02:41:46 +0000
+++ Imaginary/imaginary/resources/help/dig 2013-09-22 10:10:54 +0000
@@ -5,4 +5,5 @@
5Create a new location with the indicated name and create a two-way5Create a new location with the indicated name and create a two-way
6passage between it and the current location. The exit will be in6passage between it and the current location. The exit will be in
7the specified direction from the current location to the new7the specified direction from the current location to the new
8location.8location. Short directions (n, s, e, w) may be used in place of the
9full direction names.
910
=== modified file 'Imaginary/imaginary/resources/help/go'
--- Imaginary/imaginary/resources/help/go 2006-04-12 02:41:46 +0000
+++ Imaginary/imaginary/resources/help/go 2013-09-22 10:10:54 +0000
@@ -2,4 +2,5 @@
22
3Usage: go <north | south | east | west>3Usage: go <north | south | east | west>
44
5Travel in the indicated direction.5Travel in the indicated direction. Short directions (n, s, e, w) may be used
6in place of the full direction names.
67
=== modified file 'Imaginary/imaginary/test/test_actions.py'
--- Imaginary/imaginary/test/test_actions.py 2013-07-26 00:20:17 +0000
+++ Imaginary/imaginary/test/test_actions.py 2013-09-22 10:10:54 +0000
@@ -423,7 +423,11 @@
423 self.assertEquals(x[5].groups(), ())423 self.assertEquals(x[5].groups(), ())
424424
425425
426 def testDig(self):426 def test_dig(self):
427 """
428 The I{dig} action creates a new location connected to the current
429 location through an exit in the specified direction.
430 """
427 self._test(431 self._test(
428 "dig west dark tunnel",432 "dig west dark tunnel",
429 ["You create an exit."],433 ["You create an exit."],
@@ -447,45 +451,125 @@
447 "dig west boring tunnel",451 "dig west boring tunnel",
448 ["There is already an exit in that direction."])452 ["There is already an exit in that direction."])
449453
450 def testBury(self):454
451 self._test(455 def test_digWithDirectionAliases(self):
452 "bury south",456 """
453 ["There isn't an exit in that direction."])457 The I{dig} action creates a new location connected to the current
454 self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])458 location through an exit in the specified direction even when that
455459 direction is an alias.
456 room = objects.Thing(store=self.store, name=u"destination", proper=True)460 """
457 objects.Container.createFor(room, capacity=1000)461 self._test(
458 objects.Exit.link(room, self.location, u'north')462 "dig w dark tunnel",
459463 ["You create an exit."],
460 self._test(464 ["Test Player created an exit to the west."])
461 "bury south",465 room = iimaginary.IContainer(self.location).getExitNamed(u'west').toLocation
462 ["It's gone."],466 self.assertEquals(room.name, u"dark tunnel")
463 ["Test Player destroyed the exit to destination."])467 self.assertEquals(room.description, u'')
464468 self.assertIdentical(iimaginary.IContainer(room).getExitNamed(u'east').toLocation,
465 self.assertEquals(469 self.location)
466 list(iimaginary.IContainer(self.location).getExits()),470
467 [])471 self._test(
468472 "dig e bright tunnel",
469 self.assertEquals(473 ["You create an exit."],
470 list(iimaginary.IContainer(room).getExits()),474 ["Test Player created an exit to the east."])
471 [])475 room = iimaginary.IContainer(self.location).getExitNamed(u'east').toLocation
472476 self.assertEquals(room.name, u"bright tunnel")
473 objects.Exit.link(self.location, room, u'south')477 self.assertEquals(room.description, u'')
474 self.observer.moveTo(room)478 self.assertIdentical(iimaginary.IContainer(room).getExitNamed(u'west').toLocation, self.location)
475479
476 self._test(480 self._test(
477 "bury south",481 "dig w boring tunnel",
478 ["It's gone."],482 ["There is already an exit in that direction."])
479 ["The exit to Test Location crumbles and disappears."])483
480 self.assertEquals(484
481 list(iimaginary.IContainer(self.location).getExits()),485 def test_bury(self):
482 [])486 """
483 self.assertEquals(487 The I{bury} action destroys an exit in the specified direction.
484 list(iimaginary.IContainer(room).getExits()),488 """
485 [])489 self._test(
486490 "bury south",
487491 ["There isn't an exit in that direction."])
488 def testGo(self):492 self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])
493
494 room = objects.Thing(store=self.store, name=u"destination", proper=True)
495 objects.Container.createFor(room, capacity=1000)
496 objects.Exit.link(room, self.location, u'north')
497
498 self._test(
499 "bury south",
500 ["It's gone."],
501 ["Test Player destroyed the exit to destination."])
502
503 self.assertEquals(
504 list(iimaginary.IContainer(self.location).getExits()),
505 [])
506
507 self.assertEquals(
508 list(iimaginary.IContainer(room).getExits()),
509 [])
510
511 objects.Exit.link(self.location, room, u'south')
512 self.observer.moveTo(room)
513
514 self._test(
515 "bury south",
516 ["It's gone."],
517 ["The exit to Test Location crumbles and disappears."])
518 self.assertEquals(
519 list(iimaginary.IContainer(self.location).getExits()),
520 [])
521 self.assertEquals(
522 list(iimaginary.IContainer(room).getExits()),
523 [])
524
525
526 def test_buryWithDirectionAliases(self):
527 """
528 The I{bury} action destroys an exit in the specified direction even
529 when that direction is an alias.
530 """
531 self._test(
532 "bury s",
533 ["There isn't an exit in that direction."])
534 self.assertEquals(list(iimaginary.IContainer(self.location).getExits()), [])
535
536 room = objects.Thing(store=self.store, name=u"destination", proper=True)
537 objects.Container.createFor(room, capacity=1000)
538 objects.Exit.link(room, self.location, u'north')
539
540 self._test(
541 "bury s",
542 ["It's gone."],
543 ["Test Player destroyed the exit to destination."])
544
545 self.assertEquals(
546 list(iimaginary.IContainer(self.location).getExits()),
547 [])
548
549 self.assertEquals(
550 list(iimaginary.IContainer(room).getExits()),
551 [])
552
553 objects.Exit.link(self.location, room, u'south')
554 self.observer.moveTo(room)
555
556 self._test(
557 "bury s",
558 ["It's gone."],
559 ["The exit to Test Location crumbles and disappears."])
560 self.assertEquals(
561 list(iimaginary.IContainer(self.location).getExits()),
562 [])
563 self.assertEquals(
564 list(iimaginary.IContainer(room).getExits()),
565 [])
566
567
568 def test_go(self):
569 """
570 The I{go} action moves the player through an exit in the specified
571 direction.
572 """
489 self._test(573 self._test(
490 "go west",574 "go west",
491 ["You can't go that way."])575 ["You can't go that way."])
@@ -516,6 +600,41 @@
516 ["Test Player arrives from the west."])600 ["Test Player arrives from the west."])
517601
518602
603 def test_goThroughDirectionAliases(self):
604 """
605 The I{go} action moves the player through an exit in the specified
606 direction even when that direction is an alias.
607 """
608 self._test(
609 "go w",
610 ["You can't go that way."])
611 self._test(
612 "w",
613 ["You can't go that way."])
614
615 room = objects.Thing(store=self.store, name=u"destination")
616 objects.Container.createFor(room, capacity=1000)
617 objects.Exit.link(self.location, room, u"west")
618
619 self._test(
620 "w",
621 [E("[ destination ]"),
622 E("( east )"),
623 ""],
624 ["Test Player leaves west."])
625
626 self._test(
627 "n",
628 ["You can't go that way."])
629 self._test(
630 "go e",
631 [E("[ Test Location ]"),
632 E("( west )"),
633 "Location for testing.",
634 "Here, you see Observer Player."],
635 ["Test Player arrives from the west."])
636
637
519 def test_goThroughOneWayExit(self):638 def test_goThroughOneWayExit(self):
520 """639 """
521 Going through a one-way exit with a known direction will announce that640 Going through a one-way exit with a known direction will announce that
@@ -559,7 +678,11 @@
559 self.assertCommandOutput("go east", [E("You can't go that way.")], [])678 self.assertCommandOutput("go east", [E("You can't go that way.")], [])
560679
561680
562 def testDirectionalMovement(self):681 def test_directionalMovement(self):
682 """
683 You can move through exits in standard directions by just specifying
684 the direction.
685 """
563 # A couple tweaks to state to make the test simpler686 # A couple tweaks to state to make the test simpler
564 self.observer.location = None687 self.observer.location = None
565 self.location.description = None688 self.location.description = None
@@ -579,6 +702,13 @@
579 [E("[ ") + ".*" + E(" ]"), # Not testing room description702 [E("[ ") + ".*" + E(" ]"), # Not testing room description
580 E("( ") + ".*" + E(" )"), # Just do enough to make sure it was not an error.703 E("( ") + ".*" + E(" )"), # Just do enough to make sure it was not an error.
581 ""])704 ""])
705 shortDirections = ["nw", "n", "ne", "e", "w", "sw", "s", "se"]
706 for d, rd in zip(shortDirections, reversed(shortDirections)):
707 self._test(
708 d,
709 [E("[ ") + ".*" + E(" ]"), # Not testing room description
710 E("( ") + ".*" + E(" )"), # Just do enough to make sure it was not an error.
711 ""])
582712
583713
584 def test_scrutinize(self):714 def test_scrutinize(self):

Subscribers

People subscribed via source and target branches

to all changes: