diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index a44329d40a..6e093c7455 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -116,6 +116,18 @@ def proto_def(obj, with_tags=True): protodef['tags'] = tags return protodef + +def maskout_protodef(protodef, mask): + """ + Returns a new protodef after removing protodef values based on mask + """ + protodef = dict(protodef) + for m in mask: + if m in protodef: + protodef.pop(m) + return protodef + + # Colorize the default success message def _colorize_message(msg): _i = 0 @@ -140,6 +152,7 @@ class PuzzleRecipe(DefaultScript): self.db.puzzle_name = str(puzzle_name) self.db.parts = tuple(parts) self.db.results = tuple(results) + self.db.mask = tuple() self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE self.db.use_success_location_message = _PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE @@ -275,6 +288,7 @@ class CmdCreatePuzzleRecipe(MuxCommand): puzzle = create_script(PuzzleRecipe, key=puzzle_name) puzzle.save_recipe(puzzle_name, proto_parts, proto_results) + puzzle.locks.add('control:id(%s) or perm(Builder)' % caller.dbref[1:]) caller.msg( "Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n." @@ -297,6 +311,7 @@ class CmdEditPuzzle(MuxCommand): @puzzleedit[/delete] <#dbref> @puzzleedit <#dbref>/use_success_message = @puzzleedit <#dbref>/use_success_location_message = + @puzzleedit <#dbref>/mask = attr1[,attr2,...]> @puzzleedit[/addpart] <#dbref> = @puzzleedit[/delpart] <#dbref> = @puzzleedit[/addresult] <#dbref> = @@ -309,6 +324,7 @@ class CmdEditPuzzle(MuxCommand): delresult - removes results from the puzzle delete - deletes the recipe. Existing parts and results aren't modified + mask - attributes to exclude during matching (e.g. location, desc, etc.) use_success_location_message containing {result_names} and {caller} will automatically be replaced with correct values. Both are optional. When removing parts/results, it's possible to remove all. @@ -400,6 +416,12 @@ class CmdEditPuzzle(MuxCommand): "%s use_success_location_message = %s\n" % (puzzle_name_id, puzzle.db.use_success_location_message) ) return + elif attr == 'mask': + puzzle.db.mask = tuple(self.rhslist) + caller.msg( + "%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask) + ) + return def _get_objs(self): if not self.rhslist: @@ -576,14 +598,19 @@ class CmdUsePuzzleParts(MuxCommand): matched_puzzles = dict() for puzzle in puzzles: puzzle_protoparts = list(puzzle.db.parts[:]) + puzzle_mask = puzzle.db.mask[:] # remove tags and prototype_key as they prevent equality - for puzzle_protopart in puzzle_protoparts: + for i, puzzle_protopart in enumerate(puzzle_protoparts[:]): del(puzzle_protopart['tags']) del(puzzle_protopart['prototype_key']) + puzzle_protopart = maskout_protodef(puzzle_protopart, puzzle_mask) + puzzle_protoparts[i] = puzzle_protopart + matched_dbrefparts = [] parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name] for part_dbref in parts_dbrefs: protopart = puzzle_ingredients[part_dbref] + protopart = maskout_protodef(protopart, puzzle_mask) if protopart in puzzle_protoparts: puzzle_protoparts.remove(protopart) matched_dbrefparts.append(part_dbref) @@ -669,6 +696,7 @@ class CmdListPuzzleRecipes(MuxCommand): text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref)) text.append('Success Caller message:\n' + recipe.db.use_success_message + '\n') text.append('Success Location message:\n' + recipe.db.use_success_location_message + '\n') + text.append('Mask:\n' + str(recipe.db.mask) + '\n') text.append('Parts') for protopart in recipe.db.parts[:]: mark = '-' diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 71236a14df..6dfcab5d7f 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2015,14 +2015,6 @@ class TestPuzzles(CommandTest): _puzzleedit('', recipe_dbref, 'dummy', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") _puzzleedit('', self.script.dbref, '', 'Script(#1) is not a puzzle') - # no permissions - _puzzleedit('', recipe_dbref, '/use_success_message = Yes!', "You don't have permission") - _puzzleedit('/delete', recipe_dbref, '', "You don't have permission") - - # grant perm to char1 - puzzle = search.search_script(recipe_dbref)[0] - puzzle.locks.add('control:id(%s)' % self.char1.dbref[1:]) - # edit use_success_message and use_success_location_message _puzzleedit('', recipe_dbref, '/use_success_message = Yes!', 'makefire(%s) use_success_message = Yes!' % recipe_dbref) _puzzleedit('', recipe_dbref, '/use_success_location_message = {result_names} Yeah baby! {caller}', 'makefire(%s) use_success_location_message = {result_names} Yeah baby! {caller}' % recipe_dbref) @@ -2031,6 +2023,20 @@ class TestPuzzles(CommandTest): self.room1.msg_contents = Mock() self._use('stone, flint', 'Yes!') self.room1.msg_contents.assert_called_once_with('fire Yeah baby! Char', exclude=(self.char1,)) + self.room1.msg_contents.reset_mock() + + # edit mask: exclude location and desc during matching + _puzzleedit('', recipe_dbref, '/mask = location,desc', + "makefire(%s) mask = ('location', 'desc')" % recipe_dbref) + + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + # change location and desc + self.char1.search('stone').db.desc = 'A solid slab of granite' + self.char1.search('stone').location = self.char1 + self.char1.search('flint').db.desc = 'A flint stone' + self.char1.search('flint').location = self.char1 + self._use('stone, flint', 'Yes!') + self.room1.msg_contents.assert_called_once_with('fire Yeah baby! Char', exclude=(self.char1,)) # delete _puzzleedit('/delete', recipe_dbref, '', 'makefire(%s) was deleted' % recipe_dbref) @@ -2135,6 +2141,7 @@ class TestPuzzles(CommandTest): r"^Puzzle 'makefire'.*$", r"^Success Caller message:$", r"^Success Location message:$", + r"^Mask:$", r"^Parts$", r"^.*key: stone$", r"^.*key: flint$", @@ -2301,14 +2308,6 @@ class TestPuzzles(CommandTest): expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} self._check_room_contents(expected) - # TODO: results has Exit - - # TODO: results has NPC - - # TODO: results has Room - - # TODO: parts' location can be different from Character's location - def test_e2e_interchangeable_parts_and_results(self): # Parts and Results can be used in multiple puzzles egg = create_object(