diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 59b780af2d..0a2a4cb447 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -44,7 +44,7 @@ Details: Puzzles are created from existing objects. The given objects are introspected to create prototypes for the puzzle parts and results. These prototypes become the -puzzle recipe. (See PuzzleRecipeObject and @puzzle +puzzle recipe. (See PuzzleRecipe and @puzzle command). Once the recipe is created, all parts and result can be disposed (i.e. destroyed). @@ -69,9 +69,10 @@ Alternatively: import itertools from random import choice -from evennia import create_object +from evennia import create_object, create_script from evennia import CmdSet from evennia import DefaultObject +from evennia import DefaultScript from evennia import DefaultCharacter from evennia import DefaultRoom from evennia.commands.default.muxcommand import MuxCommand @@ -127,7 +128,7 @@ class PuzzlePartObject(DefaultObject): self.db.puzzle_name = puzzle_name -class PuzzleRecipeObject(DefaultObject): +class PuzzleRecipe(DefaultScript): """ Definition of a Puzzle Recipe """ @@ -217,7 +218,7 @@ class CmdCreatePuzzleRecipe(MuxCommand): proto_parts = [proto_def(obj) for obj in parts] proto_results = [proto_def(obj) for obj in results] - puzzle = create_object(PuzzleRecipeObject, key=puzzle_name) + puzzle = create_script(PuzzleRecipe, key=puzzle_name) puzzle.save_recipe(puzzle_name, proto_parts, proto_results) caller.msg( @@ -253,10 +254,12 @@ class CmdArmPuzzle(MuxCommand): caller.msg("A puzzle recipe's #dbref must be specified") return - puzzle = caller.search(self.args, global_search=True) - if not puzzle or not inherits_from(puzzle, PuzzleRecipeObject): + puzzle = search.search_script(self.args) + if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe): + caller.msg('Invalid puzzle %r' % (self.args)) return + puzzle = puzzle[0] caller.msg( "Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..." % ( puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))) @@ -336,17 +339,16 @@ class CmdUsePuzzleParts(MuxCommand): (part.dbref, proto_def(part, with_tags=False)) ) + # Find all puzzles by puzzle name # FIXME: we rely on obj.db.puzzle_name which is visible and may be cnaged afterwards. Can we lock it and hide it? puzzles = [] for puzzle_name, parts in puzzle_ingredients.items(): - _puzzles = caller.search( - puzzle_name, - typeclass=[PuzzleRecipeObject], - attribute_name='puzzle_name', - quiet=True, - exact=True, - global_search=True) + _puzzles = search.search_script_attribute( + key='puzzle_name', + value=puzzle_name + ) + _puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles)) if not _puzzles: continue else: @@ -356,12 +358,11 @@ class CmdUsePuzzleParts(MuxCommand): # Create lookup dict puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles) - # Check if parts can be combined to solve a puzzle matched_puzzles = dict() for puzzle in puzzles: - puzzleparts = puzzle.db.parts[:] - parts = puzzle_ingredients[puzzle.db.puzzle_name][:] + puzzleparts = list(sorted(puzzle.db.parts[:], key=lambda p: p['key'])) + parts = list(sorted(puzzle_ingredients[puzzle.db.puzzle_name][:], key=lambda p: p[1]['key'])) pz = 0 p = 0 matched_dbrefparts = set() @@ -454,7 +455,7 @@ class CmdListPuzzleRecipes(MuxCommand): def func(self): caller = self.caller - recipes = search.search_tag( + recipes = search.search_script_tag( _PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) div = "-" * 60 diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 1210486411..1d8b2f2324 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1198,7 +1198,7 @@ class TestPuzzles(CommandTest): def _keys(items): return [item['key'] for item in items] - recipes = search.search_tag('', category=puzzles._PUZZLES_TAG_CATEGORY) + recipes = search.search_script_tag('', category=puzzles._PUZZLES_TAG_CATEGORY) self.assertEqual(1, len(recipes)) self.assertEqual(name, recipes[0].db.puzzle_name) self.assertEqual(parts, _keys(recipes[0].db.parts)) @@ -1207,8 +1207,10 @@ class TestPuzzles(CommandTest): puzzles._PUZZLES_TAG_RECIPE, recipes[0].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY) ) + recipe_dbref = recipes[0].dbref if and_destroy_it: recipes[0].delete() + return recipe_dbref if not and_destroy_it else None def _assert_no_recipes(self): self.assertEqual( @@ -1216,14 +1218,41 @@ class TestPuzzles(CommandTest): len(search.search_tag('', category=puzzles._PUZZLES_TAG_CATEGORY)) ) - def test_cmd_use(self): - def _use(cmdstr, msg): - self.call(puzzles.CmdUsePuzzleParts(), cmdstr, msg, caller=self.char1) + # good recipes + def _good_recipe(self, name, parts, results, and_destroy_it=True): + regexs = [] + for p in parts: + regexs.append(r'^Part %s\(#\d+\)$' % (p)) + for r in results: + regexs.append(r'^Result %s\(#\d+\)$' % (r)) + regexs.append(r"^Puzzle '%s' %s\(#\d+\) has been created successfully.$" % (name, name)) + lhs = [name] + parts + cmdstr = ','.join(lhs) + '=' + ','.join(results) + msg = self.call( + puzzles.CmdCreatePuzzleRecipe(), + cmdstr, + caller=self.char1 + ) + matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) + recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it) + return recipe_dbref - _use('', 'Use what?') - _use('stone', 'You have no idea how this can be used') - _use('stone flint', 'There is no stone flint around.') - _use('stone, flint', 'You have no idea how these can be used') + def _arm(self, recipe_dbref): + msg = self.call( + puzzles.CmdArmPuzzle(), + recipe_dbref, + caller=self.char1 + ) + print(msg) + # TODO: add regex for parts and whatnot + # similar to _good_recipe + ''' + Puzzle Recipe makefire(#2) 'makefire' found. +Spawning 2 parts ... +Part stone(#11) spawned and placed at Room(#1) +Part flint(#12) spawned and placed at Room(#1) +Puzzle armed successfully.''' + self.assertIsNotNone(re.match(r"Puzzle Recipe .* found.*Puzzle armed successfully.", msg, re.MULTILINE | re.DOTALL)) def test_cmd_puzzle(self): self._assert_no_recipes() @@ -1249,32 +1278,14 @@ class TestPuzzles(CommandTest): self._assert_no_recipes() - # good recipes - def _good_recipe(name, parts, results): - regexs = [] - for p in parts: - regexs.append(r'^Part %s\(#\d+\)$' % (p)) - for r in results: - regexs.append(r'^Result %s\(#\d+\)$' % (r)) - regexs.append(r"^Puzzle '%s' %s\(#\d+\) has been created successfully.$" % (name, name)) - lhs = [name] + parts - cmdstr = ','.join(lhs) + '=' + ','.join(results) - msg = self.call( - puzzles.CmdCreatePuzzleRecipe(), - cmdstr, - caller=self.char1 - ) - matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) - self._assert_recipe(name, parts, results) - - _good_recipe('makefire', ['stone', 'flint'], ['fire', 'stone', 'flint']) - _good_recipe('hot stones', ['stone', 'fire'], ['stone', 'fire']) - _good_recipe('hot stones', ['stone', 'fire'], ['stone', 'fire']) + self._good_recipe('makefire', ['stone', 'flint'], ['fire', 'stone', 'flint']) + self._good_recipe('hot stones', ['stone', 'fire'], ['stone', 'fire']) + self._good_recipe('furnace', ['stone', 'stone', 'fire'], ['stone', 'stone', 'fire', 'fire', 'fire', 'fire']) # bad recipes def _bad_recipe(name, parts, results, fail_regex): with self.assertRaisesRegexp(AssertionError, fail_regex): - _good_recipe(name, parts, results) + self._good_recipe(name, parts, results) self.assert_no_recipes() _bad_recipe('name', ['nothing'], ['neither'], r"Could not find 'nothing'.") @@ -1282,3 +1293,44 @@ class TestPuzzles(CommandTest): # _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], '') # FIXME: no name becomes '' #N(#N) self._assert_no_recipes() + + def test_cmd_armpuzzle(self): + recipe_dbref = self._good_recipe('makefile', ['stone', 'flint'], ['fire', 'stone', 'flint'], and_destroy_it=False) + self._arm(recipe_dbref) + + def test_cmd_use(self): + def _use(cmdstr, msg): + msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, msg, caller=self.char1) + return msg + + _use('', 'Use what?') + _use('something', 'There is no something around.') + _use('stone', 'You have no idea how this can be used') + _use('stone flint', 'There is no stone flint around.') + _use('stone, flint', 'You have no idea how these can be used') + + recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) + + # although there is stone and flint + # those aren't valid puzzle parts because + # the puzzle hasn't been armed + _use('stone', 'You have no idea how this can be used') + _use('stone, flint', 'You have no idea how these can be used') + self._arm(recipe_dbref) + + # there are duplicated objects now + msg = _use('stone', None) + self.assertIsNotNone(re.match(r'^Which stone. There are many.*', msg)) + msg = _use('flint', None) + self.assertIsNotNone(re.match(r'^Which flint. There are many.*', msg)) + # delete them + self.stone.delete() + self.flint.delete() + + msg = _use('stone, flint', None) + self.assertIsNotNone(re.match(r"^You are a Genius.*", msg)) + + # trying again will fail as it was resolved already + # and the parts were destroyed + _use('stone, flint', 'There is no stone around') + _use('flint, stone', 'There is no flint around')