From 7f9d8c1442586bc5b45c63f84fc2edb40cd28054 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Sep 2018 15:38:17 -0500 Subject: [PATCH 01/37] Puzzles System - first cut: PuzzlePartObject: typeclass for puzzle parts and results. PuzzleRecipeObject: typeclass to store prototypes of parts and results. PuzzleSystemCmdSet: commands to create, arm and resolve puzzles. --- evennia/contrib/puzzles.py | 472 +++++++++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 evennia/contrib/puzzles.py diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py new file mode 100644 index 0000000000..6523d13ea6 --- /dev/null +++ b/evennia/contrib/puzzles.py @@ -0,0 +1,472 @@ +""" +Puzzles System - Provides a typeclass and commands for +objects that can be combined (i.e. 'use'd) to produce +new objects. + +Evennia contribution - Henddher 2018 + +A Puzzle is a recipe of what objects (aka parts) must +be combined by a player so a new set of objects +(aka results) are automatically created. + +Consider this simple Puzzle: + + orange, mango, yogurt, blender = fruit smoothie + +As a Builder: + + @create/drop orange + @create/drop mango + @create/drop yogurt + @create/drop blender + + @puzzle smoothie puzzle, orange, mango, yogurt, blender = fruit smoothie + ... + Puzzle smoothie puzzle (#1234) created successfuly. + + @destroy/force orange, mango, yogurt, blender + + @armpuzzle #1234 + Part orange is spawned at ... + Part mango is spawned at ... + .... + Puzzle smoothie puzzle (#1234) has been armed successfully + +As Player: + + use orange, mango, yogurt, blender + ... + Genius, you blended all fruits to create a yummy smoothie! + +Details: + +Puzzles are created from existing objects. The given +objects are introspected to create prototypes for the +puzzle parts. These prototypes become the puzzle recipe. +(See PuzzleRecipeObject and @puzzle command). + +At a later time, a Builder or a Script can arm the puzzle +and spawn all puzzle parts (PuzzlePartObject) in their +respective locations (See @armpuzzle). + +A regular player can collect the puzzle parts and combine +them (See use command). If player has specified +all pieces, the puzzle is considered solved and all +its puzzle parts are destroyed while the puzzle results +are spawened on their corresponding location. + +Installation: + +Add the PuzzleSystemCmdSet to all players. +Alternatively: + + @py self.cmdset.add('evennia.contrib.puzzles.PuzzleSystemCmdSet') + +""" + +import itertools +from random import choice +from django.conf import settings +from evennia import create_object +from evennia import CmdSet +from evennia import DefaultObject +from evennia import DefaultCharacter +from evennia import DefaultRoom +from evennia.commands.default.muxcommand import MuxCommand +from evennia.utils.utils import inherits_from +from evennia.utils import search, utils, logger +from evennia.utils.spawner import spawn + +# ----------- UTILITY FUNCTIONS ------------ + +def proto_def(obj, with_tags=True): + """ + Basic properties needed to spawn + and compare recipe with candidate part + """ + protodef = { + 'key': obj.key, + 'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass + 'desc': obj.db.desc, + 'location': obj.location, + # FIXME: Can tags be INVISIBLE? We don't want player to know an object belongs to a puzzle + 'tags': [(_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)], + } + if not with_tags: + del(protodef['tags']) + return protodef + +# ------------------------------------------ + +# Tag used by puzzles +_PUZZLES_TAG_CATEGORY = 'puzzles' +_PUZZLES_TAG_RECIPE = 'puzzle_recipe' +# puzzle part and puzzle result +_PUZZLES_TAG_MEMBER = 'puzzle_member' + + +class PuzzlePartObject(DefaultObject): + """ + Puzzle Part, typically used by @armpuzzle command + """ + + def mark_as_puzzle_member(self, puzzle_name): + """ + Marks this object as a member of puzzle named + puzzle_name + """ + # FIXME: if multiple puzzles have the same + # puzzle_name, their ingredients may be + # combined but leave other parts orphan + # Similarly, if a puzzle_name were changed, + # its parts will become orphan + # Perhaps we should use #dbref but that will + # force specific parts to be combined + self.db.puzzle_name = puzzle_name + + +class PuzzleRecipeObject(DefaultObject): + """ + Definition of a Puzzle Recipe + """ + + def save_recipe(self, puzzle_name, parts, results): + self.db.puzzle_name = puzzle_name + self.db.parts = tuple(parts) + self.db.results = tuple(results) + self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) + + +class CmdCreatePuzzleRecipe(MuxCommand): + """ + Creates a puzzle recipe. + + Each part and result must exist and be placed in their corresponding location. + All parts and results are left intact. Caller must explicitly + destroy them. + + Usage: + @puzzle name,] = + """ + + key = '@puzzle' + aliases = '@puzzlerecipe' + locks = 'cmd:perm(puzzle) or perm(Builder)' + help_category = 'Puzzles' + + def func(self): + caller = self.caller + + if len(self.lhslist) < 2 \ + or not self.rhs: + string = "Usage: @puzzle name, = " + caller.msg(string) + return + + puzzle_name = self.lhslist[0] + + def is_valid_obj_location(obj): + valid = True + # Valid locations are: room, ... + # TODO: other valid locations must be added here + # Certain locations can be handled accordingly: e.g, + # a part is located in a character's inventory, + # perhaps will translate into the player character + # having the part in his/her inventory while being + # located in the same room where the builder was + # located. + # Parts and results may have different valid locations + # TODO: handle contents of a given part + if not inherits_from(obj.location, settings.BASE_ROOM_TYPECLASS): + caller.msg('Invalid location for %s' % (obj.key)) + valid = False + return valid + + def is_valid_part_location(part): + return is_valid_obj_location(part) + + def is_valid_result_location(part): + return is_valid_obj_location(part) + + parts = [] + for objname in self.lhslist[1:]: + obj = caller.search(objname) + if not obj: + return + if not is_valid_part_location(obj): + return + parts.append(obj) + + results = [] + for objname in self.rhslist: + obj = caller.search(objname) + if not obj: + return + if not is_valid_result_location(obj): + return + results.append(obj) + + for part in parts: + caller.msg('Part %s(%s)' % (part.name, part.dbref)) + + for result in results: + caller.msg('Result %s(%s)' % (result.name, result.dbref)) + + 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.save_recipe(puzzle_name, proto_parts, proto_results) + + caller.msg( + "Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n." + % (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref)) + caller.msg( + 'You may now dispose all parts and results. ' + 'Typically, results and parts are useless afterwards.\n' + 'You are now able to arm this puzzle using Builder command:\n' + ' @armpuzzle \n\n' + 'Or programmatically.\n' + ) + + # FIXME: puzzle recipe object exists but it has no location + # should we create a PuzzleLibrary where all puzzles are + # kept and cannot be reached by players? + + +class CmdArmPuzzle(MuxCommand): + """ + Arms a puzzle by spawning all its parts + """ + + key = '@armpuzzle' + # FIXME: permissions for scripts? + locks = 'cmd:perm(armpuzzle) or perm(Builder)' + help_category = 'Puzzles' + + def func(self): + caller = self.caller + + if self.args is None or not utils.dbref(self.args): + 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): + return + + caller.msg( + "Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..." % ( + puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))) + + for proto_part in puzzle.db.parts: + # caller.msg('Protopart %r %r' % (proto_part, type(proto_part))) + part = spawn(proto_part)[0] + caller.msg("Part %s(%s) spawned and placed at %s(%s)" % (part.name, part.dbref, part.location, part.location.dbref)) + part.mark_as_puzzle_member(puzzle.db.puzzle_name) + + caller.msg("Puzzle armed |gsuccessfully|n.") + + +class CmdUsePuzzleParts(MuxCommand): + """ + Searches for all puzzles whose parts + match the given set of objects. If + there are matching puzzles, the result + objects are spawned in their corresponding + location if all parts have been passed in. + + Usage: + use ] + """ + + # TODO: consider allowing builder to provide + # messages and "hooks" that can be displayed + # and/or fired whenever the resolver of the puzzle + # enters the location where a result was spawned + + key = 'use' + aliases = 'combine' + locks = 'cmd:pperm(use) or pperm(Player)' + help_category = 'Puzzles' + + def func(self): + caller = self.caller + + if not self.lhs: + caller.msg('Use what?') + return + + many = 'these' if len(self.lhslist) > 1 else 'this' + + # either all are parts, or abort finding matching puzzles + parts = [] + partnames = self.lhslist[:] + for partname in partnames: + part = caller.search( + partname, + multimatch_string='Which %s. There are many.\n' % (partname), + nofound_string='There is no %s around.' % (partname) + ) + + if not part: + return + + if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY) \ + or not inherits_from(part, PuzzlePartObject): + + # not a puzzle part ... abort + caller.msg('You have no idea how %s can be used' % (many)) + return + + # a valid part + parts.append(part) + + # Create lookup dict + parts_dict = dict((part.dbref, part) for part in parts) + + # Group parts by their puzzle name + puzzle_ingredients = dict() + for part in parts: + puzzle_name = part.db.puzzle_name + if puzzle_name not in puzzle_ingredients: + puzzle_ingredients[puzzle_name] = [] + puzzle_ingredients[puzzle_name].append( + (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) + if not _puzzles: + continue + else: + puzzles.extend(_puzzles) + + # 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][:] + pz = 0 + p = 0 + matched_dbrefparts = set() + while pz < len(puzzleparts) and p < len(parts): + puzzlepart = puzzleparts[pz] + if 'tags' in puzzlepart: + # remove 'tags' as they will prevent equality + del(puzzlepart['tags']) + dbref, part = parts[p] + if part == puzzlepart: + pz += 1 + matched_dbrefparts.add(dbref) + else: + pass + p += 1 + else: + if len(puzzleparts) == len(matched_dbrefparts): + matched_puzzles[puzzle.dbref] = matched_dbrefparts + + if len(matched_puzzles) == 0: + # FIXME: Add more random messages + # random part falls and lands on your feet + # random part hits you square on the face + caller.msg("As you try to utilize %s, nothing happens." % (many)) + return + + puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True) + + # sort all matched puzzles and pick largest one(s) + puzzledbref, matched_dbrefparts = puzzletuples[0] + nparts = len(matched_dbrefparts) + largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples)) + + # if there are more than one, let user pick + if len(largest_puzzles) > 1: + # FIXME: pick a random one or let user choose? + caller.msg( + 'Your gears start turning and a bunch of ideas come to your mind ...\n%s' % ( + ' ...\n'.join([lp.db.puzzle_name for lp in largest_puzzles])) + ) + puzzle = choice(largest_puzzles) + caller.msg("You try %s ..." % (puzzle.db.puzzle_name)) + + # got one, spawn its results + puzzle = puzzles_dict[puzzledbref] + # FIXME: DRY with parts + for proto_result in puzzle.db.results: + result = spawn(proto_result)[0] + result.mark_as_puzzle_member(puzzle.db.puzzle_name) + # FIXME: add 'ramdon' messages: + # Hmmm ... did I search result.location? + # What was that? ... I heard something in result.location? + # Eureka! you built a result + + # Destroy all parts used + for dbref in matched_dbrefparts: + parts_dict[dbref].delete() + + # FIXME: Add random messages + # You are a genius ... no matter what your 2nd grade teacher told you + # You hear thunders and a cloud of dust raises leaving + caller.msg("Puzzle solved |gsuccessfully|n.") + + +class CmdListPuzzleRecipes(MuxCommand): + """ + Searches for all puzzle recipes + + Usage: + @lspuzzlerecipes + """ + + key = '@lspuzzlerecipes' + locks = 'cmd:perm(lspuzzlerecipes) or perm(Builder)' + help_category = 'Puzzles' + + def func(self): + caller = self.caller + # TODO: use @tags/search puzzle_recipe : puzzles + + +class CmdListArmedPuzzles(MuxCommand): + """ + Searches for all armed puzzles + + Usage: + @lsarmedpuzzles + """ + + key = '@lsarmedpuzzles' + locks = 'cmd:perm(lsarmedpuzzles) or perm(Builder)' + help_category = 'Puzzles' + + def func(self): + caller = self.caller + # TODO: use @tags/search puzzle_member : puzzles + + +class PuzzleSystemCmdSet(CmdSet): + """ + CmdSet to create, arm and resolve Puzzles + + Add with @py self.cmdset.add("evennia.contrib.puzzles.PuzzlesCmdSet") + """ + + def at_cmdset_creation(self): + super(PuzzleSystemCmdSetCmdSet, self).at_cmdset_creation() + + self.add(CmdCreatePuzzleRecipe()) + self.add(CmdArmPuzzle()) + self.add(CmdUsePuzzleParts()) From ffdf8ec289b7bb2a93146efc0e4d490bf80baa51 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Sep 2018 16:46:57 -0500 Subject: [PATCH 02/37] typo in classname --- evennia/contrib/puzzles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 6523d13ea6..213cd7793c 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -465,7 +465,7 @@ class PuzzleSystemCmdSet(CmdSet): """ def at_cmdset_creation(self): - super(PuzzleSystemCmdSetCmdSet, self).at_cmdset_creation() + super(PuzzleSystemCmdSet, self).at_cmdset_creation() self.add(CmdCreatePuzzleRecipe()) self.add(CmdArmPuzzle()) From a1c428a3558cc7a85658e340785b551d8def2ced Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Sep 2018 16:46:57 -0500 Subject: [PATCH 03/37] When multiple puzzles are matched, show their names to the caller and then randomly pick one --- evennia/contrib/puzzles.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 6523d13ea6..2e730e9038 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -85,6 +85,7 @@ def proto_def(obj, with_tags=True): and compare recipe with candidate part """ protodef = { + # FIXME: Don't we need to honor ALL properties? locks, perms, etc. 'key': obj.key, 'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass 'desc': obj.db.desc, @@ -260,7 +261,6 @@ class CmdArmPuzzle(MuxCommand): puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))) for proto_part in puzzle.db.parts: - # caller.msg('Protopart %r %r' % (proto_part, type(proto_part))) part = spawn(proto_part)[0] caller.msg("Part %s(%s) spawned and placed at %s(%s)" % (part.name, part.dbref, part.location, part.location.dbref)) part.mark_as_puzzle_member(puzzle.db.puzzle_name) @@ -351,6 +351,8 @@ class CmdUsePuzzleParts(MuxCommand): else: puzzles.extend(_puzzles) + logger.log_info("PUZZLES %r" % ([p.dbref for p in puzzles])) + # Create lookup dict puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles) @@ -371,8 +373,6 @@ class CmdUsePuzzleParts(MuxCommand): if part == puzzlepart: pz += 1 matched_dbrefparts.add(dbref) - else: - pass p += 1 else: if len(puzzleparts) == len(matched_dbrefparts): @@ -387,27 +387,33 @@ class CmdUsePuzzleParts(MuxCommand): puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True) + logger.log_info("MATCHED PUZZLES %r" % (puzzletuples)) + # sort all matched puzzles and pick largest one(s) puzzledbref, matched_dbrefparts = puzzletuples[0] nparts = len(matched_dbrefparts) + puzzle = puzzles_dict[puzzledbref] largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples)) - # if there are more than one, let user pick + # if there are more than one, ... if len(largest_puzzles) > 1: # FIXME: pick a random one or let user choose? + # FIXME: do we show the puzzle name or something else? caller.msg( 'Your gears start turning and a bunch of ideas come to your mind ...\n%s' % ( - ' ...\n'.join([lp.db.puzzle_name for lp in largest_puzzles])) + ' ...\n'.join([puzzles_dict[lp[0]].db.puzzle_name for lp in largest_puzzles])) ) - puzzle = choice(largest_puzzles) + puzzletuple = choice(largest_puzzles) + puzzle = puzzles_dict[puzzletuple[0]] caller.msg("You try %s ..." % (puzzle.db.puzzle_name)) # got one, spawn its results - puzzle = puzzles_dict[puzzledbref] # FIXME: DRY with parts + result_names = [] for proto_result in puzzle.db.results: result = spawn(proto_result)[0] result.mark_as_puzzle_member(puzzle.db.puzzle_name) + result_names.append(result.name) # FIXME: add 'ramdon' messages: # Hmmm ... did I search result.location? # What was that? ... I heard something in result.location? @@ -420,7 +426,16 @@ class CmdUsePuzzleParts(MuxCommand): # FIXME: Add random messages # You are a genius ... no matter what your 2nd grade teacher told you # You hear thunders and a cloud of dust raises leaving - caller.msg("Puzzle solved |gsuccessfully|n.") + result_names = ', '.join(result_names) + caller.msg( + "You are a |wG|re|wn|ri|wu|rs|n!!!\nYou just created %s" % ( + result_names + )) + caller.location.msg_contents( + "|c%s|n performs some kind of tribal dance" + " and seems to create |y%s|n from thin air" % ( + caller, result_names), exclude=(caller,) + ) class CmdListPuzzleRecipes(MuxCommand): @@ -465,7 +480,7 @@ class PuzzleSystemCmdSet(CmdSet): """ def at_cmdset_creation(self): - super(PuzzleSystemCmdSetCmdSet, self).at_cmdset_creation() + super(PuzzleSystemCmdSet, self).at_cmdset_creation() self.add(CmdCreatePuzzleRecipe()) self.add(CmdArmPuzzle()) From 250a233703370e960e82cc15552ba10750054d72 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Sep 2018 22:34:17 -0500 Subject: [PATCH 04/37] Addition of CmdListPuzzleRecipes and CmdListArmedPuzzles --- evennia/contrib/puzzles.py | 61 ++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 2e730e9038..12fb83d03b 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -77,6 +77,12 @@ from evennia.utils.utils import inherits_from from evennia.utils import search, utils, logger from evennia.utils.spawner import spawn +# Tag used by puzzles +_PUZZLES_TAG_CATEGORY = 'puzzles' +_PUZZLES_TAG_RECIPE = 'puzzle_recipe' +# puzzle part and puzzle result +_PUZZLES_TAG_MEMBER = 'puzzle_member' + # ----------- UTILITY FUNCTIONS ------------ def proto_def(obj, with_tags=True): @@ -99,13 +105,6 @@ def proto_def(obj, with_tags=True): # ------------------------------------------ -# Tag used by puzzles -_PUZZLES_TAG_CATEGORY = 'puzzles' -_PUZZLES_TAG_RECIPE = 'puzzle_recipe' -# puzzle part and puzzle result -_PUZZLES_TAG_MEMBER = 'puzzle_member' - - class PuzzlePartObject(DefaultObject): """ Puzzle Part, typically used by @armpuzzle command @@ -452,7 +451,30 @@ class CmdListPuzzleRecipes(MuxCommand): def func(self): caller = self.caller - # TODO: use @tags/search puzzle_recipe : puzzles + + recipes = search.search_tag( + _PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) + + div = "-" * 60 + text = [div] + msgf_recipe = "Puzzle |y'%s' %s(%s)|n" + msgf_item = "%2s|c%15s|n: |w%s|n" + for recipe in recipes: + text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref)) + text.append('Parts') + for protopart in recipe.db.parts[:]: + mark = '-' + for k, v in protopart.items(): + text.append(msgf_item % (mark, k, v)) + mark = '' + text.append('Results') + for protoresult in recipe.db.results[:]: + mark = '-' + for k, v in protoresult.items(): + text.append(msgf_item % (mark, k, v)) + mark = '' + text.append(div) + caller.msg('\n'.join(text)) class CmdListArmedPuzzles(MuxCommand): @@ -469,7 +491,26 @@ class CmdListArmedPuzzles(MuxCommand): def func(self): caller = self.caller - # TODO: use @tags/search puzzle_member : puzzles + + armed_puzzles = search.search_tag( + _PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY) + + armed_puzzles = dict((k, list(g)) for k, g in itertools.groupby( + armed_puzzles, + lambda ap: ap.db.puzzle_name)) + + div = '-' * 60 + msgf_pznm = "Puzzle name: |y%s|n" + msgf_item = "|m%25s|w(%s)|n at |c%25s|w(%s)|n" + text = [div] + for pzname, items in armed_puzzles.items(): + text.append(msgf_pznm % (pzname)) + for item in items: + text.append(msgf_item % ( + item.name, item.dbref, + item.location.name, item.location.dbref)) + text.append(div) + caller.msg('\n'.join(text)) class PuzzleSystemCmdSet(CmdSet): @@ -484,4 +525,6 @@ class PuzzleSystemCmdSet(CmdSet): self.add(CmdCreatePuzzleRecipe()) self.add(CmdArmPuzzle()) + self.add(CmdListPuzzleRecipes()) + self.add(CmdListArmedPuzzles()) self.add(CmdUsePuzzleParts()) From 24649ef171c4cfc2e4dbaad6a8dbc303c6e01c2a Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Sep 2018 22:48:42 -0500 Subject: [PATCH 05/37] Documentation corrections and clarifications --- evennia/contrib/puzzles.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 12fb83d03b..70bb92c108 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -19,31 +19,34 @@ As a Builder: @create/drop mango @create/drop yogurt @create/drop blender + @create/drop fruit smoothie - @puzzle smoothie puzzle, orange, mango, yogurt, blender = fruit smoothie + @puzzle smoothie, orange, mango, yogurt, blender = fruit smoothie ... - Puzzle smoothie puzzle (#1234) created successfuly. + Puzzle smoothie(#1234) created successfuly. - @destroy/force orange, mango, yogurt, blender + @destroy/force orange, mango, yogurt, blender, fruit smoothie @armpuzzle #1234 Part orange is spawned at ... Part mango is spawned at ... .... - Puzzle smoothie puzzle (#1234) has been armed successfully + Puzzle smoothie(#1234) has been armed successfully As Player: use orange, mango, yogurt, blender ... - Genius, you blended all fruits to create a yummy smoothie! + Genius, you blended all fruits to create a fruit smoothie! Details: Puzzles are created from existing objects. The given objects are introspected to create prototypes for the -puzzle parts. These prototypes become the puzzle recipe. -(See PuzzleRecipeObject and @puzzle command). +puzzle parts and results. These prototypes become the +puzzle recipe. (See PuzzleRecipeObject and @puzzle +command). Once the recipe is created, all parts and result +can be disposed (i.e. destroyed). At a later time, a Builder or a Script can arm the puzzle and spawn all puzzle parts (PuzzlePartObject) in their From ae3f17122591a49a6cfbf5ae5b9bff8c13906633 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Mon, 3 Sep 2018 20:54:33 -0500 Subject: [PATCH 06/37] Tests for puzzles --- evennia/contrib/puzzles.py | 3 +- evennia/contrib/tests.py | 112 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 70bb92c108..59b780af2d 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -69,7 +69,6 @@ Alternatively: import itertools from random import choice -from django.conf import settings from evennia import create_object from evennia import CmdSet from evennia import DefaultObject @@ -180,7 +179,7 @@ class CmdCreatePuzzleRecipe(MuxCommand): # located. # Parts and results may have different valid locations # TODO: handle contents of a given part - if not inherits_from(obj.location, settings.BASE_ROOM_TYPECLASS): + if not inherits_from(obj.location, DefaultRoom): caller.msg('Invalid location for %s' % (obj.key)) valid = False return valid diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 2a337d2065..1210486411 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1170,3 +1170,115 @@ class TestRandomStringGenerator(EvenniaTest): # We can't generate one more with self.assertRaises(random_string_generator.ExhaustedGenerator): SIMPLE_GENERATOR.get() + + +# Test of the Puzzles module + +from evennia.contrib import puzzles +from evennia.utils import search + +class TestPuzzles(CommandTest): + + def setUp(self): + super(TestPuzzles, self).setUp() + self.stone = create_object(key='stone', location=self.char1.location) + self.flint = create_object(key='flint', location=self.char1.location) + self.fire = create_object(key='fire', location=self.char1.location) + + def _assert_msg_matched(self, msg, regexs, re_flags=0): + matches = [] + for regex in regexs: + m = re.search(regex, msg, re_flags) + self.assertIsNotNone(m, "%r didn't match %r" % (regex, msg)) + matches.append(m) + return matches + + def _assert_recipe(self, name, parts, results, and_destroy_it=True): + + def _keys(items): + return [item['key'] for item in items] + + recipes = search.search_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)) + self.assertEqual(results, _keys(recipes[0].db.results)) + self.assertEqual( + puzzles._PUZZLES_TAG_RECIPE, + recipes[0].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY) + ) + if and_destroy_it: + recipes[0].delete() + + def _assert_no_recipes(self): + self.assertEqual( + 0, + 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) + + _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 test_cmd_puzzle(self): + self._assert_no_recipes() + + # bad syntax + def _bad_syntax(cmdstr): + self.call( + puzzles.CmdCreatePuzzleRecipe(), + cmdstr, + 'Usage: @puzzle name, = ', + caller=self.char1 + ) + + _bad_syntax('') + _bad_syntax('=') + _bad_syntax('nothing =') + _bad_syntax('= nothing') + _bad_syntax('nothing') + _bad_syntax(',nothing') + _bad_syntax('name, nothing') + _bad_syntax('name, nothing =') + # _bad_syntax(', = ,') # FIXME: got: Could not find ''. + + 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']) + + # bad recipes + def _bad_recipe(name, parts, results, fail_regex): + with self.assertRaisesRegexp(AssertionError, fail_regex): + _good_recipe(name, parts, results) + self.assert_no_recipes() + + _bad_recipe('name', ['nothing'], ['neither'], r"Could not find 'nothing'.") + _bad_recipe('name', ['stone'], ['nothing'], r"Could not find 'nothing'.") + # _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], '') # FIXME: no name becomes '' #N(#N) + + self._assert_no_recipes() From e767d77db8c08ad5508dd14f61e8fa276f4a6eb8 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 9 Sep 2018 22:43:26 -0500 Subject: [PATCH 07/37] PuzzleRecipe as DefaultScript; not as DefaultObject. Misc tests --- evennia/contrib/puzzles.py | 35 ++++++------ evennia/contrib/tests.py | 112 +++++++++++++++++++++++++++---------- 2 files changed, 100 insertions(+), 47 deletions(-) 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') From 8e7106806bddbc41167d1da1564f9b08e42f4732 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Mon, 10 Sep 2018 20:00:08 -0500 Subject: [PATCH 08/37] @armpuzzle tests --- evennia/contrib/tests.py | 41 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 1d8b2f2324..c85180efee 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1237,22 +1237,20 @@ class TestPuzzles(CommandTest): recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it) return recipe_dbref - def _arm(self, recipe_dbref): + def _arm(self, recipe_dbref, name, parts): + regexs = [ + r"^Puzzle Recipe %s\(#\d+\) '%s' found.$" % (name, name), + r"^Spawning %d parts ...$" % (len(parts)), + ] + for p in parts: + regexs.append(r'^Part %s\(#\d+\) spawned .*$' % (p)) + regexs.append(r"^Puzzle armed successfully.$") 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)) + matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) def test_cmd_puzzle(self): self._assert_no_recipes() @@ -1295,8 +1293,23 @@ Puzzle armed successfully.''' 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) + # bad arms + self.call( + puzzles.CmdArmPuzzle(), + '1', + "A puzzle recipe's #dbref must be specified", + caller=self.char1 + ) + self.call( + puzzles.CmdArmPuzzle(), + '#1', + "Invalid puzzle '#1'", + caller=self.char1 + ) + + recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire', 'stone', 'flint'], and_destroy_it=False) + # goo arm + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) def test_cmd_use(self): def _use(cmdstr, msg): @@ -1316,7 +1329,7 @@ Puzzle armed successfully.''' # 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) + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) # there are duplicated objects now msg = _use('stone', None) From 434abe0aa65d8c906b1fdfba5a70c6a0a854bbc7 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Thu, 13 Sep 2018 20:00:50 -0500 Subject: [PATCH 09/37] Revamp _bad_recipe() helper function and fix @puzzle command empty name --- evennia/commands/default/muxcommand.py | 5 +++++ evennia/contrib/puzzles.py | 3 +++ evennia/contrib/tests.py | 18 ++++++++++++------ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/evennia/commands/default/muxcommand.py b/evennia/commands/default/muxcommand.py index 5d8d4b2890..d2d2c65986 100644 --- a/evennia/commands/default/muxcommand.py +++ b/evennia/commands/default/muxcommand.py @@ -118,6 +118,11 @@ class MuxCommand(Command): lhs, rhs = [arg.strip() for arg in args.split('=', 1)] lhslist = [arg.strip() for arg in lhs.split(',')] rhslist = [arg.strip() for arg in rhs.split(',')] + # eliminate all empty-strings + # if len(lhslist) > 0: + # lhslist = list(filter(lambda i: len(i) > 0, lhslist)) + # if len(rhslist) > 0: + # rhslist = list(filter(lambda i: len(i) > 0, rhslist)) # save to object properties: self.raw = raw diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 0a2a4cb447..9fd71d4ed9 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -167,6 +167,9 @@ class CmdCreatePuzzleRecipe(MuxCommand): return puzzle_name = self.lhslist[0] + if len(puzzle_name) == 0: + caller.msg('Invalid puzzle name %r.' % puzzle_name) + return def is_valid_obj_location(obj): valid = True diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index c85180efee..7adf8b9dcf 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1233,8 +1233,8 @@ class TestPuzzles(CommandTest): 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) + matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) return recipe_dbref def _arm(self, recipe_dbref, name, parts): @@ -1272,7 +1272,7 @@ class TestPuzzles(CommandTest): _bad_syntax(',nothing') _bad_syntax('name, nothing') _bad_syntax('name, nothing =') - # _bad_syntax(', = ,') # FIXME: got: Could not find ''. + # _bad_syntax(', = ,') # FIXME: MuxCommand issue? self._assert_no_recipes() @@ -1282,13 +1282,19 @@ class TestPuzzles(CommandTest): # bad recipes def _bad_recipe(name, parts, results, fail_regex): - with self.assertRaisesRegexp(AssertionError, fail_regex): - self._good_recipe(name, parts, results) - self.assert_no_recipes() + cmdstr = ','.join([name] + parts) \ + + '=' + ','.join(results) + msg = self.call( + puzzles.CmdCreatePuzzleRecipe(), + cmdstr, + caller=self.char1 + ) + self._assert_no_recipes() + self.assertIsNotNone(re.match(fail_regex, msg), msg) _bad_recipe('name', ['nothing'], ['neither'], r"Could not find 'nothing'.") _bad_recipe('name', ['stone'], ['nothing'], r"Could not find 'nothing'.") - # _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], '') # FIXME: no name becomes '' #N(#N) + _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], r"^Invalid puzzle name ''.") self._assert_no_recipes() From a349f6554ef95256273ae03b4eab6f1c47a80b70 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Fri, 14 Sep 2018 21:57:28 -0500 Subject: [PATCH 10/37] Minor cleanup --- evennia/commands/default/muxcommand.py | 5 ----- evennia/contrib/puzzles.py | 5 ----- evennia/contrib/tests.py | 1 - 3 files changed, 11 deletions(-) diff --git a/evennia/commands/default/muxcommand.py b/evennia/commands/default/muxcommand.py index d2d2c65986..5d8d4b2890 100644 --- a/evennia/commands/default/muxcommand.py +++ b/evennia/commands/default/muxcommand.py @@ -118,11 +118,6 @@ class MuxCommand(Command): lhs, rhs = [arg.strip() for arg in args.split('=', 1)] lhslist = [arg.strip() for arg in lhs.split(',')] rhslist = [arg.strip() for arg in rhs.split(',')] - # eliminate all empty-strings - # if len(lhslist) > 0: - # lhslist = list(filter(lambda i: len(i) > 0, lhslist)) - # if len(rhslist) > 0: - # rhslist = list(filter(lambda i: len(i) > 0, rhslist)) # save to object properties: self.raw = raw diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 9fd71d4ed9..e0423105f5 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -99,7 +99,6 @@ def proto_def(obj, with_tags=True): 'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass 'desc': obj.db.desc, 'location': obj.location, - # FIXME: Can tags be INVISIBLE? We don't want player to know an object belongs to a puzzle 'tags': [(_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)], } if not with_tags: @@ -235,10 +234,6 @@ class CmdCreatePuzzleRecipe(MuxCommand): 'Or programmatically.\n' ) - # FIXME: puzzle recipe object exists but it has no location - # should we create a PuzzleLibrary where all puzzles are - # kept and cannot be reached by players? - class CmdArmPuzzle(MuxCommand): """ diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 7adf8b9dcf..ab206dc632 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1272,7 +1272,6 @@ class TestPuzzles(CommandTest): _bad_syntax(',nothing') _bad_syntax('name, nothing') _bad_syntax('name, nothing =') - # _bad_syntax(', = ,') # FIXME: MuxCommand issue? self._assert_no_recipes() From 6e391450f2d63713aefc2ba668e9c2e53cc46a84 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Fri, 14 Sep 2018 23:28:56 -0500 Subject: [PATCH 11/37] Enforce parts and results to be DefaultObject not DefaultCharacter, DefaultRoom nor DefaultExit with tests. Tests for @lspuzzlerecipes and @lsarmedpuzzles --- evennia/contrib/puzzles.py | 21 +++++++++- evennia/contrib/tests.py | 81 +++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index e0423105f5..cf89a4834c 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -75,6 +75,7 @@ from evennia import DefaultObject from evennia import DefaultScript from evennia import DefaultCharacter from evennia import DefaultRoom +from evennia import DefaultExit from evennia.commands.default.muxcommand import MuxCommand from evennia.utils.utils import inherits_from from evennia.utils import search, utils, logger @@ -193,12 +194,28 @@ class CmdCreatePuzzleRecipe(MuxCommand): def is_valid_result_location(part): return is_valid_obj_location(part) + def is_valid_inheritance(obj): + valid = not inherits_from(obj, DefaultCharacter) \ + and not inherits_from(obj, DefaultRoom) \ + and not inherits_from(obj, DefaultExit) + if not valid: + caller.msg('Invalid typeclass for %s' % (obj)) + return valid + + def is_valid_part(part): + return is_valid_inheritance(part) \ + and is_valid_part_location(part) + + def is_valid_result(result): + return is_valid_inheritance(result) \ + and is_valid_result_location(result) + parts = [] for objname in self.lhslist[1:]: obj = caller.search(objname) if not obj: return - if not is_valid_part_location(obj): + if not is_valid_part(obj): return parts.append(obj) @@ -207,7 +224,7 @@ class CmdCreatePuzzleRecipe(MuxCommand): obj = caller.search(objname) if not obj: return - if not is_valid_result_location(obj): + if not is_valid_result(obj): return results.append(obj) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index ab206dc632..0e61a66f33 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1215,7 +1215,7 @@ class TestPuzzles(CommandTest): def _assert_no_recipes(self): self.assertEqual( 0, - len(search.search_tag('', category=puzzles._PUZZLES_TAG_CATEGORY)) + len(search.search_script_tag('', category=puzzles._PUZZLES_TAG_CATEGORY)) ) # good recipes @@ -1294,6 +1294,11 @@ class TestPuzzles(CommandTest): _bad_recipe('name', ['nothing'], ['neither'], r"Could not find 'nothing'.") _bad_recipe('name', ['stone'], ['nothing'], r"Could not find 'nothing'.") _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], r"^Invalid puzzle name ''.") + self.stone.location = self.char1 + _bad_recipe('name', ['stone'], ['fire'], r"^Invalid location for stone$") + _bad_recipe('name', ['flint'], ['stone'], r"^Invalid location for stone$") + _bad_recipe('name', ['self'], ['fire'], r"^Invalid typeclass for Char$") + _bad_recipe('name', ['here'], ['fire'], r"^Invalid typeclass for Room$") self._assert_no_recipes() @@ -1352,3 +1357,77 @@ class TestPuzzles(CommandTest): # and the parts were destroyed _use('stone, flint', 'There is no stone around') _use('flint, stone', 'There is no flint around') + + def test_lspuzzlerecipes_lsarmedpuzzles(self): + msg = self.call( + puzzles.CmdListPuzzleRecipes(), + '', + caller=self.char1 + ) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^-+$", + ], + re.MULTILINE | re.DOTALL + ) + + recipe_dbref = self._good_recipe( + 'makefire', ['stone', 'flint'], ['fire'], + and_destroy_it=False) + + msg = self.call( + puzzles.CmdListPuzzleRecipes(), + '', + caller=self.char1 + ) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^Puzzle 'makefire'.*$", + r"^Parts$", + r"^.*key: stone$", + r"^.*key: flint$", + r"^Results$", + r"^.*key: fire$", + r"^.*key: stone$", + r"^.*key: flint$", + r"^-+$", + ], + re.MULTILINE | re.DOTALL + ) + + msg = self.call( + puzzles.CmdListArmedPuzzles(), + '', + caller=self.char1 + ) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^-+$" + ], + re.MULTILINE | re.DOTALL + ) + + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + + msg = self.call( + puzzles.CmdListArmedPuzzles(), + '', + caller=self.char1 + ) + self._assert_msg_matched( + msg, + [ + r"^-+$", + r"^Puzzle name: makefire$", + r"^.*stone.* at \s+ Room.*$", + r"^.*flint.* at \s+ Room.*$", + r"^-+$", + ], + re.MULTILINE | re.DOTALL + ) From fe9d1fc7abd48caee352ee9af0c911f179f26b81 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 15 Sep 2018 01:36:33 -0500 Subject: [PATCH 12/37] Increase test coverage for puzzles module --- evennia/contrib/tests.py | 93 +++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 0e61a66f33..6841f92adb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1174,8 +1174,10 @@ class TestRandomStringGenerator(EvenniaTest): # Test of the Puzzles module +import itertools from evennia.contrib import puzzles from evennia.utils import search +from evennia.utils.utils import inherits_from class TestPuzzles(CommandTest): @@ -1193,23 +1195,23 @@ class TestPuzzles(CommandTest): matches.append(m) return matches - def _assert_recipe(self, name, parts, results, and_destroy_it=True): + def _assert_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): def _keys(items): return [item['key'] for item in items] 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)) - self.assertEqual(results, _keys(recipes[0].db.results)) + self.assertEqual(expected_count, len(recipes)) + self.assertEqual(name, recipes[expected_count-1].db.puzzle_name) + self.assertEqual(parts, _keys(recipes[expected_count-1].db.parts)) + self.assertEqual(results, _keys(recipes[expected_count-1].db.results)) self.assertEqual( puzzles._PUZZLES_TAG_RECIPE, - recipes[0].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY) + recipes[expected_count-1].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY) ) - recipe_dbref = recipes[0].dbref + recipe_dbref = recipes[expected_count-1].dbref if and_destroy_it: - recipes[0].delete() + recipes[expected_count-1].delete() return recipe_dbref if not and_destroy_it else None def _assert_no_recipes(self): @@ -1219,7 +1221,7 @@ class TestPuzzles(CommandTest): ) # good recipes - def _good_recipe(self, name, parts, results, and_destroy_it=True): + def _good_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1): regexs = [] for p in parts: regexs.append(r'^Part %s\(#\d+\)$' % (p)) @@ -1233,10 +1235,19 @@ class TestPuzzles(CommandTest): cmdstr, caller=self.char1 ) - recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it) + recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it, expected_count) matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) return recipe_dbref + def _check_room_contents(self, expected): + by_obj_key = lambda o: o.key + room1_contents = sorted(self.room1.contents, key=by_obj_key) + for key, grp in itertools.groupby(room1_contents, by_obj_key): + if key in expected: + grp = list(grp) + self.assertEqual(expected[key], len(grp), + "Expected %d but got %d for %s" % (expected[key], len(grp), key)) + def _arm(self, recipe_dbref, name, parts): regexs = [ r"^Puzzle Recipe %s\(#\d+\) '%s' found.$" % (name, name), @@ -1318,12 +1329,19 @@ class TestPuzzles(CommandTest): ) recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire', 'stone', 'flint'], and_destroy_it=False) - # goo arm + + # delete proto parts and proto result + self.stone.delete() + self.flint.delete() + self.fire.delete() + + # good arm self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._check_room_contents({'stone': 1, 'flint': 1}) def test_cmd_use(self): - def _use(cmdstr, msg): - msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, msg, caller=self.char1) + def _use(cmdstr, expmsg): + msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, expmsg, caller=self.char1) return msg _use('', 'Use what?') @@ -1333,6 +1351,8 @@ class TestPuzzles(CommandTest): _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) + recipe2_dbref = self._good_recipe('makefire2', ['stone', 'flint'], ['fire'] , and_destroy_it=False, + expected_count=2) # although there is stone and flint # those aren't valid puzzle parts because @@ -1340,24 +1360,57 @@ class TestPuzzles(CommandTest): _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, 'makefire', ['stone', 'flint']) + self._check_room_contents({'stone': 2, 'flint': 2}) # 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 + _use('stone', 'Which stone. There are many') + _use('flint', 'Which flint. There are many') + + # delete proto parts self.stone.delete() self.flint.delete() + # delete proto result + self.fire.delete() - msg = _use('stone, flint', None) - self.assertIsNotNone(re.match(r"^You are a Genius.*", msg)) + # solve puzzle + _use('stone, flint', 'You are a Genius') + self.assertEqual(1, + len(list(filter( + lambda o: o.key == 'fire' \ + and inherits_from(o,'evennia.contrib.puzzles.PuzzlePartObject'), + self.room1.contents)))) + self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 1}) # 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') + # arm same puzzle twice so there are duplicated parts + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}) + + # try solving with multiple parts but incomplete set + _use('1-stone, 2-stone', 'As you try to utilize these, nothing happens.') + + # arm the other puzzle. Their parts are identical + self._arm(recipe2_dbref, 'makefire2', ['stone', 'flint']) + self._check_room_contents({'stone': 3, 'flint': 3, 'fire': 1}) + + # solve with multiple parts for + # multiple puzzles. Both can be solved but + # only one is. + _use( + '1-stone, 2-flint, 3-stone, 3-flint', + 'Your gears start turning and a bunch of ideas come to your mind ... ') + self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}) + + # solve all + _use('1-stone, 1-flint', 'You are a Genius') + _use('stone, flint', 'You are a Genius') + self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}) + def test_lspuzzlerecipes_lsarmedpuzzles(self): msg = self.call( puzzles.CmdListPuzzleRecipes(), From c5b2e49ffae0cfbcf62fdaf7e417718008eb1bd8 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 15 Sep 2018 12:02:22 -0500 Subject: [PATCH 13/37] E2E tests for puzzles --- evennia/contrib/tests.py | 126 ++++++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 6841f92adb..a9396cc78e 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1263,6 +1263,9 @@ class TestPuzzles(CommandTest): ) matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) + def test_cmdset_puzzle(self): + self.char1.cmdset.add('evennia.contrib.puzzles.PuzzleSystemCmdSet') + def test_cmd_puzzle(self): self._assert_no_recipes() @@ -1339,16 +1342,22 @@ class TestPuzzles(CommandTest): self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) self._check_room_contents({'stone': 1, 'flint': 1}) - def test_cmd_use(self): - def _use(cmdstr, expmsg): - msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, expmsg, caller=self.char1) - return msg + def _use(self, cmdstr, expmsg): + msg = self.call( + puzzles.CmdUsePuzzleParts(), + cmdstr, + expmsg, + 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') + def test_cmd_use(self): + + self._use('', 'Use what?') + self._use('something', 'There is no something around.') + self._use('stone', 'You have no idea how this can be used') + self._use('stone flint', 'There is no stone flint around.') + self._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) recipe2_dbref = self._good_recipe('makefire2', ['stone', 'flint'], ['fire'] , and_destroy_it=False, @@ -1357,14 +1366,14 @@ class TestPuzzles(CommandTest): # 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._use('stone', 'You have no idea how this can be used') + self._use('stone, flint', 'You have no idea how these can be used') self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) self._check_room_contents({'stone': 2, 'flint': 2}) # there are duplicated objects now - _use('stone', 'Which stone. There are many') - _use('flint', 'Which flint. There are many') + self._use('stone', 'Which stone. There are many') + self._use('flint', 'Which flint. There are many') # delete proto parts self.stone.delete() @@ -1373,7 +1382,7 @@ class TestPuzzles(CommandTest): self.fire.delete() # solve puzzle - _use('stone, flint', 'You are a Genius') + self._use('stone, flint', 'You are a Genius') self.assertEqual(1, len(list(filter( lambda o: o.key == 'fire' \ @@ -1383,8 +1392,8 @@ class TestPuzzles(CommandTest): # 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') + self._use('stone, flint', 'There is no stone around') + self._use('flint, stone', 'There is no flint around') # arm same puzzle twice so there are duplicated parts self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) @@ -1392,7 +1401,7 @@ class TestPuzzles(CommandTest): self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}) # try solving with multiple parts but incomplete set - _use('1-stone, 2-stone', 'As you try to utilize these, nothing happens.') + self._use('1-stone, 2-stone', 'As you try to utilize these, nothing happens.') # arm the other puzzle. Their parts are identical self._arm(recipe2_dbref, 'makefire2', ['stone', 'flint']) @@ -1401,14 +1410,14 @@ class TestPuzzles(CommandTest): # solve with multiple parts for # multiple puzzles. Both can be solved but # only one is. - _use( + self._use( '1-stone, 2-flint, 3-stone, 3-flint', 'Your gears start turning and a bunch of ideas come to your mind ... ') self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}) # solve all - _use('1-stone, 1-flint', 'You are a Genius') - _use('stone, flint', 'You are a Genius') + self._use('1-stone, 1-flint', 'You are a Genius') + self._use('stone, flint', 'You are a Genius') self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}) def test_lspuzzlerecipes_lsarmedpuzzles(self): @@ -1484,3 +1493,80 @@ class TestPuzzles(CommandTest): ], re.MULTILINE | re.DOTALL ) + + def test_e2e(self): + + def _destroy_objs_in_room(keys): + for obj in self.room1.contents: + if obj.key in keys: + obj.delete() + + # parts don't survive resolution + # but produce a large result set + tree = create_object(key='tree', location=self.char1.location) + axe = create_object(key='axe', location=self.char1.location) + sweat = create_object(key='sweat', location=self.char1.location) + dull_axe = create_object(key='dull axe', location=self.char1.location) + timber = create_object(key='timber', location=self.char1.location) + log = create_object(key='log', location=self.char1.location) + parts = ['tree', 'axe'] + results = (['sweat'] * 10) + ['dull axe'] + (['timber'] * 20) + (['log'] * 50) + recipe_dbref = self._good_recipe( + 'lumberjack', + parts, results, + and_destroy_it=False + ) + + _destroy_objs_in_room(set(parts + results)) + + sps = sorted(parts) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + expected.update({r: 0 for r in set(results)}) + + self._arm(recipe_dbref, 'lumberjack', parts) + self._check_room_contents(expected) + + self._use(','.join(parts), 'You are a Genius') + srs = sorted(set(results)) + expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} + expected.update({p: 0 for p in set(parts)}) + self._check_room_contents(expected) + + # parts also appear in results + # causing a new puzzle to be armed 'automatically' + # i.e. the puzzle is self-sustaining + hole = create_object(key='hole', location=self.char1.location) + shovel = create_object(key='shovel', location=self.char1.location) + dirt = create_object(key='dirt', location=self.char1.location) + + parts = ['shovel', 'hole'] + results = ['dirt', 'hole', 'shovel'] + recipe_dbref = self._good_recipe( + 'digger', + parts, results, + and_destroy_it=False, + expected_count=2 + ) + + _destroy_objs_in_room(set(parts + results)) + + nresolutions = 0 + + sps = sorted(set(parts)) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + expected.update({'dirt': nresolutions}) + + self._arm(recipe_dbref, 'digger', parts) + self._check_room_contents(expected) + + for i in range(10): + self._use(','.join(parts), 'You are a Genius') + nresolutions += 1 + expected.update({'dirt': nresolutions}) + self._check_room_contents(expected) + + # TODO: results has Exit + + # TODO: results has NPC + + # TODO: results has Room From 4f5c2f51adc7bf0b8162dad17033c49c92099155 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 15 Sep 2018 21:08:48 -0500 Subject: [PATCH 14/37] Add @puzzleedit and puzzle.db.use_success_message so puzzle resolution message can be customized by builder --- evennia/contrib/puzzles.py | 101 ++++++++++++++++++++++++++++++++----- evennia/contrib/tests.py | 45 +++++++++++++++-- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index cf89a4834c..4f95062c5e 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -87,6 +87,9 @@ _PUZZLES_TAG_RECIPE = 'puzzle_recipe' # puzzle part and puzzle result _PUZZLES_TAG_MEMBER = 'puzzle_member' +_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = 'You try to utilize %s but nothing happens ... something amiss?' +_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = 'You are a Genius!!!' + # ----------- UTILITY FUNCTIONS ------------ def proto_def(obj, with_tags=True): @@ -106,6 +109,15 @@ def proto_def(obj, with_tags=True): del(protodef['tags']) return protodef +# Colorize the default success message +_i = 0 +_colors = ['|r', '|g', '|y'] +_msg = [] +for l in _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE: + _msg += _colors[_i] + l + _i = (_i + 1) % len(_colors) +_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = ''.join(_msg) + '|n' + # ------------------------------------------ class PuzzlePartObject(DefaultObject): @@ -138,6 +150,7 @@ class PuzzleRecipe(DefaultScript): self.db.parts = tuple(parts) self.db.results = tuple(results) self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) + self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE class CmdCreatePuzzleRecipe(MuxCommand): @@ -246,12 +259,80 @@ class CmdCreatePuzzleRecipe(MuxCommand): caller.msg( 'You may now dispose all parts and results. ' 'Typically, results and parts are useless afterwards.\n' + 'Remember to add a "success message" via:\n' + ' @puzzleedit #dbref/use_success_message = \n' 'You are now able to arm this puzzle using Builder command:\n' - ' @armpuzzle \n\n' - 'Or programmatically.\n' + ' @armpuzzle \n' ) +class CmdEditPuzzle(MuxCommand): + """ + Edits puzzle properties + + Usage: + @puzzleedit[/delete] <#dbref> + @puzzleedit <#dbref>/use_success_message = + + Switches: + delete - deletes the recipe. Existing parts and results aren't modified + + """ + + key = '@puzzleedit' + # FIXME: permissions for scripts? + locks = 'cmd:perm(puzzleedit) or perm(Builder)' + help_category = 'Puzzles' + + def func(self): + _USAGE = "Usage: @puzzleedit[/switches] [/attribute = ]" + caller = self.caller + + if not self.lhslist: + caller.msg(_USAGE) + return + + if '/' in self.lhslist[0]: + recipe_dbref, attr = self.lhslist[0].split('/') + else: + recipe_dbref = self.lhslist[0] + + if not utils.dbref(recipe_dbref): + caller.msg("A puzzle recipe's #dbref must be specified.\n" + _USAGE) + return + + puzzle = search.search_script(recipe_dbref) + if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe): + caller.msg('Invalid puzzle %r' % (recipe_dbref)) + return + + puzzle = puzzle[0] + puzzle_name_id = '%s(%s)' % (puzzle.name, puzzle.dbref) + + if 'delete' in self.switches: + if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'delete')): + caller.msg("You don't have permission to delete %s." % puzzle_name_id) + return + + puzzle.delete() + caller.msg('%s was deleted' % puzzle_name_id) + return + + else: + # edit attributes + + if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'edit')): + caller.msg("You don't have permission to edit %s." % puzzle_name_id) + return + + if attr == 'use_success_message': + puzzle.db.use_success_message = self.rhs + caller.msg( + "%s use_success_message = %s\n" % (puzzle_name_id, puzzle.db.use_success_message) + ) + return + + class CmdArmPuzzle(MuxCommand): """ Arms a puzzle by spawning all its parts @@ -396,10 +477,10 @@ class CmdUsePuzzleParts(MuxCommand): matched_puzzles[puzzle.dbref] = matched_dbrefparts if len(matched_puzzles) == 0: - # FIXME: Add more random messages + # TODO: we could use part.fail_message instead, if any # random part falls and lands on your feet # random part hits you square on the face - caller.msg("As you try to utilize %s, nothing happens." % (many)) + caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many)) return puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True) @@ -440,17 +521,11 @@ class CmdUsePuzzleParts(MuxCommand): for dbref in matched_dbrefparts: parts_dict[dbref].delete() - # FIXME: Add random messages - # You are a genius ... no matter what your 2nd grade teacher told you - # You hear thunders and a cloud of dust raises leaving result_names = ', '.join(result_names) - caller.msg( - "You are a |wG|re|wn|ri|wu|rs|n!!!\nYou just created %s" % ( - result_names - )) + caller.msg(puzzle.db.use_success_message) caller.location.msg_contents( "|c%s|n performs some kind of tribal dance" - " and seems to create |y%s|n from thin air" % ( + " and |y%s|n seems to appear from thin air" % ( caller, result_names), exclude=(caller,) ) @@ -479,6 +554,7 @@ class CmdListPuzzleRecipes(MuxCommand): msgf_item = "%2s|c%15s|n: |w%s|n" for recipe in recipes: text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref)) + text.append('Success message: ' + recipe.db.use_success_message) text.append('Parts') for protopart in recipe.db.parts[:]: mark = '-' @@ -542,6 +618,7 @@ class PuzzleSystemCmdSet(CmdSet): super(PuzzleSystemCmdSet, self).at_cmdset_creation() self.add(CmdCreatePuzzleRecipe()) + self.add(CmdEditPuzzle()) self.add(CmdArmPuzzle()) self.add(CmdListPuzzleRecipes()) self.add(CmdListArmedPuzzles()) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index a9396cc78e..efd2a3cdb0 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1265,6 +1265,7 @@ class TestPuzzles(CommandTest): def test_cmdset_puzzle(self): self.char1.cmdset.add('evennia.contrib.puzzles.PuzzleSystemCmdSet') + # FIXME: testing nothing, this is just to bump up coverage def test_cmd_puzzle(self): self._assert_no_recipes() @@ -1375,10 +1376,9 @@ class TestPuzzles(CommandTest): self._use('stone', 'Which stone. There are many') self._use('flint', 'Which flint. There are many') - # delete proto parts + # delete proto parts and proto results self.stone.delete() self.flint.delete() - # delete proto result self.fire.delete() # solve puzzle @@ -1401,7 +1401,7 @@ class TestPuzzles(CommandTest): self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}) # try solving with multiple parts but incomplete set - self._use('1-stone, 2-stone', 'As you try to utilize these, nothing happens.') + self._use('1-stone, 2-stone', 'You try to utilize these but nothing happens ... something amiss?') # arm the other puzzle. Their parts are identical self._arm(recipe2_dbref, 'makefire2', ['stone', 'flint']) @@ -1420,6 +1420,44 @@ class TestPuzzles(CommandTest): self._use('stone, flint', 'You are a Genius') self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}) + def test_puzzleedit(self): + recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) + + # delete proto parts and proto results + self.stone.delete() + self.flint.delete() + self.fire.delete() + + def _puzzleedit(swt, dbref, args, expmsg): + self.call( + puzzles.CmdEditPuzzle(), + '%s %s%s' % (swt, dbref, args), + expmsg, + caller=self.char1 + ) + + # bad syntax + _puzzleedit('', '1', '', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") + _puzzleedit('', '', '', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") + _puzzleedit('', recipe_dbref, 'dummy', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") + + # 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 + _puzzleedit('', recipe_dbref, '/use_success_message = Yes!', 'makefire(%s) use_success_message = Yes!' % recipe_dbref) + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._use('stone, flint', 'Yes!') + + # delete + _puzzleedit('/delete', recipe_dbref, '', 'makefire(%s) was deleted' % recipe_dbref) + self._assert_no_recipes() + def test_lspuzzlerecipes_lsarmedpuzzles(self): msg = self.call( puzzles.CmdListPuzzleRecipes(), @@ -1449,6 +1487,7 @@ class TestPuzzles(CommandTest): [ r"^-+$", r"^Puzzle 'makefire'.*$", + r"^Success message: .*$", r"^Parts$", r"^.*key: stone$", r"^.*key: flint$", From 1bdd7ce174bbeec66d8327e94fec80fdea2c9b51 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 16 Sep 2018 17:25:57 -0500 Subject: [PATCH 15/37] Remove one-to-one part/result to puzzle relationship based on puzzle_name. Instead, Tags puzzle_name:_PUZZLES_TAG_CATEGORY are used for matching. This allows to use older PuzzlePartObjects in newly created puzzles by adding the new puzzles' puzzle_name:_PUZZLES_TAG_CATEGORY tag to the old objects. When creating proto parts and results, honor obj.home, obj.permissions, and obj.locks, and obj.tags --- evennia/contrib/puzzles.py | 90 ++++++++++++++++---------------- evennia/contrib/tests.py | 104 ++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 47 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 4f95062c5e..19a291358d 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -98,15 +98,19 @@ def proto_def(obj, with_tags=True): and compare recipe with candidate part """ protodef = { - # FIXME: Don't we need to honor ALL properties? locks, perms, etc. + # FIXME: Don't we need to honor ALL properties? attributes, contents, etc. 'key': obj.key, 'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass 'desc': obj.db.desc, 'location': obj.location, - 'tags': [(_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)], + 'home': obj.home, + 'locks': ';'.join(obj.locks.all()), + 'permissions': obj.permissions.all()[:], } - if not with_tags: - del(protodef['tags']) + if with_tags: + tags = obj.tags.all()[:] + tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)) + protodef['tags'] = tags return protodef # Colorize the default success message @@ -128,16 +132,10 @@ class PuzzlePartObject(DefaultObject): def mark_as_puzzle_member(self, puzzle_name): """ Marks this object as a member of puzzle named - puzzle_name + 'puzzle_name' """ - # FIXME: if multiple puzzles have the same - # puzzle_name, their ingredients may be - # combined but leave other parts orphan - # Similarly, if a puzzle_name were changed, - # its parts will become orphan - # Perhaps we should use #dbref but that will - # force specific parts to be combined self.db.puzzle_name = puzzle_name + self.tags.add(puzzle_name, category=_PUZZLES_TAG_CATEGORY) class PuzzleRecipe(DefaultScript): @@ -184,6 +182,10 @@ class CmdCreatePuzzleRecipe(MuxCommand): caller.msg('Invalid puzzle name %r.' % puzzle_name) return + # TODO: if there is another puzzle with same name + # warn user that parts and results will be + # interchangable + def is_valid_obj_location(obj): valid = True # Valid locations are: room, ... @@ -422,24 +424,24 @@ class CmdUsePuzzleParts(MuxCommand): # a valid part parts.append(part) - # Create lookup dict - parts_dict = dict((part.dbref, part) for part in parts) - - # Group parts by their puzzle name + # Create lookup dicts by part's dbref and by puzzle_name(tags) + parts_dict = dict() + puzzlename_tags_dict = dict() puzzle_ingredients = dict() for part in parts: - puzzle_name = part.db.puzzle_name - if puzzle_name not in puzzle_ingredients: - puzzle_ingredients[puzzle_name] = [] - puzzle_ingredients[puzzle_name].append( - (part.dbref, proto_def(part, with_tags=False)) - ) + parts_dict[part.dbref] = part + puzzle_ingredients[part.dbref] = proto_def(part, with_tags=False) + tags_categories = part.tags.all(return_key_and_category=True) + for tag, category in tags_categories: + if category != _PUZZLES_TAG_CATEGORY: + continue + if tag not in puzzlename_tags_dict: + puzzlename_tags_dict[tag] = [] + puzzlename_tags_dict[tag].append(part.dbref) - - # 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? + # Find all puzzles by puzzle name (i.e. tag name) puzzles = [] - for puzzle_name, parts in puzzle_ingredients.items(): + for puzzle_name, parts in puzzlename_tags_dict.items(): _puzzles = search.search_script_attribute( key='puzzle_name', value=puzzle_name @@ -450,30 +452,26 @@ class CmdUsePuzzleParts(MuxCommand): else: puzzles.extend(_puzzles) - logger.log_info("PUZZLES %r" % ([p.dbref for p in puzzles])) + logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles])) - # Create lookup dict + # Create lookup dict of puzzles by dbref 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 = 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() - while pz < len(puzzleparts) and p < len(parts): - puzzlepart = puzzleparts[pz] - if 'tags' in puzzlepart: - # remove 'tags' as they will prevent equality - del(puzzlepart['tags']) - dbref, part = parts[p] - if part == puzzlepart: - pz += 1 - matched_dbrefparts.add(dbref) - p += 1 + puzzle_protoparts = list(puzzle.db.parts[:]) + # remove tags as they prevent equality + for puzzle_protopart in puzzle_protoparts: + del(puzzle_protopart['tags']) + matched_dbrefparts = [] + parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name] + for part_dbref in parts_dbrefs: + protopart = puzzle_ingredients[part_dbref] + if protopart in puzzle_protoparts: + puzzle_protoparts.remove(protopart) + matched_dbrefparts.append(part_dbref) else: - if len(puzzleparts) == len(matched_dbrefparts): + if len(puzzle_protoparts) == 0: matched_puzzles[puzzle.dbref] = matched_dbrefparts if len(matched_puzzles) == 0: @@ -506,7 +504,6 @@ class CmdUsePuzzleParts(MuxCommand): caller.msg("You try %s ..." % (puzzle.db.puzzle_name)) # got one, spawn its results - # FIXME: DRY with parts result_names = [] for proto_result in puzzle.db.results: result = spawn(proto_result)[0] @@ -523,6 +520,7 @@ class CmdUsePuzzleParts(MuxCommand): result_names = ', '.join(result_names) caller.msg(puzzle.db.use_success_message) + # TODO: allow custom message for location and channels caller.location.msg_contents( "|c%s|n performs some kind of tribal dance" " and |y%s|n seems to appear from thin air" % ( @@ -554,7 +552,7 @@ class CmdListPuzzleRecipes(MuxCommand): msgf_item = "%2s|c%15s|n: |w%s|n" for recipe in recipes: text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref)) - text.append('Success message: ' + recipe.db.use_success_message) + text.append('Success message:\n' + recipe.db.use_success_message + '\n') text.append('Parts') for protopart in recipe.db.parts[:]: mark = '-' diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index efd2a3cdb0..8620a28326 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1487,7 +1487,7 @@ class TestPuzzles(CommandTest): [ r"^-+$", r"^Puzzle 'makefire'.*$", - r"^Success message: .*$", + r"^Success message:$", r"^Parts$", r"^.*key: stone$", r"^.*key: flint$", @@ -1609,3 +1609,105 @@ class TestPuzzles(CommandTest): # 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(key='egg', location=self.char1.location) + flour = create_object(key='flour', location=self.char1.location) + boiling_water = create_object(key='boiling water', location=self.char1.location) + boiled_egg = create_object(key='boiled egg', location=self.char1.location) + dough = create_object(key='dough', location=self.char1.location) + pasta = create_object(key='pasta', location=self.char1.location) + + # Three recipes: + # 1. breakfast: egg + boiling water = boiled egg & boiling water + # 2. dough: egg + flour = dough + # 3. entree: dough + boiling water = pasta & boiling water + # tag interchangeable parts according to their puzzles' name + egg.tags.add('breakfast', category=puzzles._PUZZLES_TAG_CATEGORY) + egg.tags.add('dough', category=puzzles._PUZZLES_TAG_CATEGORY) + dough.tags.add('entree', category=puzzles._PUZZLES_TAG_CATEGORY) + boiling_water.tags.add('breakfast', category=puzzles._PUZZLES_TAG_CATEGORY) + boiling_water.tags.add('entree', category=puzzles._PUZZLES_TAG_CATEGORY) + + # create recipes + recipe1_dbref = self._good_recipe('breakfast', ['egg', 'boiling water'], ['boiled egg', 'boiling water'] , and_destroy_it=False) + recipe2_dbref = self._good_recipe('dough', ['egg', 'flour'], ['dough'] , and_destroy_it=False, expected_count=2) + recipe3_dbref = self._good_recipe('entree', ['dough', 'boiling water'], ['pasta', 'boiling water'] , and_destroy_it=False, expected_count=3) + + # delete protoparts + for obj in [egg, flour, boiling_water, + boiled_egg, dough, pasta]: + obj.delete() + + # arm each puzzle and group its parts + def _group_parts(parts, excluding=set()): + group = dict() + dbrefs = dict() + for o in self.room1.contents: + if o.key in parts and o.dbref not in excluding: + if o.key not in group: + group[o.key] = [] + group[o.key].append(o.dbref) + dbrefs[o.dbref] = o + return group, dbrefs + + self._arm(recipe1_dbref, 'breakfast', ['egg', 'boiling water']) + breakfast_parts, breakfast_dbrefs = _group_parts(['egg', 'boiling water']) + self._arm(recipe2_dbref, 'dough', ['egg', 'flour']) + dough_parts, dough_dbrefs = _group_parts(['egg', 'flour'], excluding=breakfast_dbrefs.keys()) + self._arm(recipe3_dbref, 'entree', ['dough', 'boiling water']) + entree_parts, entree_dbrefs = _group_parts(['dough', 'boiling water'], excluding=set(breakfast_dbrefs.keys() + dough_dbrefs.keys())) + + # create a box so we can put all objects in + # so that they can't be found during puzzle resolution + self.box = create_object(key='box', location=self.char1.location) + def _box_all(): + # print "boxing all\n", "-"*20 + for o in self.room1.contents: + if o not in [self.char1, self.char2, self.exit, + self.obj1, self.obj2, self.box]: + o.location = self.box + # print o.key, o.dbref, "boxed" + else: + # print "skipped", o.key, o.dbref + pass + + def _unbox(dbrefs): + # print "unboxing", dbrefs, "\n", "-"*20 + for o in self.box.contents: + if o.dbref in dbrefs: + o.location = self.room1 + # print "unboxed", o.key, o.dbref + + # solve dough puzzle using breakfast's egg + # and dough's flour. A new dough will be created + _box_all() + _unbox(breakfast_parts.pop('egg') + dough_parts.pop('flour')) + self._use('egg, flour', 'You are a Genius') + + # solve entree puzzle with newly created dough + # and breakfast's boiling water. A new + # boiling water and pasta will be created + _unbox(breakfast_parts.pop('boiling water')) + self._use('boiling water, dough', 'You are a Genius') + + # solve breakfast puzzle with dough's egg + # and newly created boiling water. A new + # boiling water and boiled egg will be created + _unbox(dough_parts.pop('egg')) + self._use('boiling water, egg', 'You are a Genius') + + # solve entree puzzle using entree's dough + # and newly created boiling water. A new + # boiling water and pasta will be created + _unbox(entree_parts.pop('dough')) + self._use('boiling water, dough', 'You are a Genius') + + self._check_room_contents({ + 'boiling water': 1, + 'pasta': 2, + 'boiled egg': 1, + }) From 109ca821750ae7e9cf3af9815452abe586b11716 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 16 Sep 2018 17:59:47 -0500 Subject: [PATCH 16/37] Honor proto parts and results tags --- evennia/contrib/puzzles.py | 2 +- evennia/contrib/tests.py | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 19a291358d..39c8a69765 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -108,7 +108,7 @@ def proto_def(obj, with_tags=True): 'permissions': obj.permissions.all()[:], } if with_tags: - tags = obj.tags.all()[:] + tags = obj.tags.all(return_key_and_category=True) tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)) protodef['tags'] = tags return protodef diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8620a28326..13e06f209f 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1186,6 +1186,12 @@ class TestPuzzles(CommandTest): self.stone = create_object(key='stone', location=self.char1.location) self.flint = create_object(key='flint', location=self.char1.location) self.fire = create_object(key='fire', location=self.char1.location) + self.stone.tags.add('tag-stone') + self.stone.tags.add('tag-stone', category='tagcat') + self.flint.tags.add('tag-flint') + self.flint.tags.add('tag-flint', category='tagcat') + self.fire.tags.add('tag-fire') + self.fire.tags.add('tag-fire', category='tagcat') def _assert_msg_matched(self, msg, regexs, re_flags=0): matches = [] @@ -1239,7 +1245,7 @@ class TestPuzzles(CommandTest): matches = self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL) return recipe_dbref - def _check_room_contents(self, expected): + def _check_room_contents(self, expected, check_test_tags=False): by_obj_key = lambda o: o.key room1_contents = sorted(self.room1.contents, key=by_obj_key) for key, grp in itertools.groupby(room1_contents, by_obj_key): @@ -1247,6 +1253,11 @@ class TestPuzzles(CommandTest): grp = list(grp) self.assertEqual(expected[key], len(grp), "Expected %d but got %d for %s" % (expected[key], len(grp), key)) + if check_test_tags: + for gi in grp: + tags = gi.tags.all(return_key_and_category=True) + self.assertIn(('tag-' + gi.key, None), tags) + self.assertIn(('tag-' + gi.key, 'tagcat'), tags) def _arm(self, recipe_dbref, name, parts): regexs = [ @@ -1341,7 +1352,7 @@ class TestPuzzles(CommandTest): # good arm self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 1, 'flint': 1}) + self._check_room_contents({'stone': 1, 'flint': 1}, check_test_tags=True) def _use(self, cmdstr, expmsg): msg = self.call( @@ -1370,7 +1381,7 @@ class TestPuzzles(CommandTest): self._use('stone', 'You have no idea how this can be used') self._use('stone, flint', 'You have no idea how these can be used') self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 2, 'flint': 2}) + self._check_room_contents({'stone': 2, 'flint': 2}, check_test_tags=True) # there are duplicated objects now self._use('stone', 'Which stone. There are many') @@ -1388,7 +1399,7 @@ class TestPuzzles(CommandTest): lambda o: o.key == 'fire' \ and inherits_from(o,'evennia.contrib.puzzles.PuzzlePartObject'), self.room1.contents)))) - self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 1}) + self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 1}, check_test_tags=True) # trying again will fail as it was resolved already # and the parts were destroyed @@ -1398,14 +1409,14 @@ class TestPuzzles(CommandTest): # arm same puzzle twice so there are duplicated parts self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}) + self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}, check_test_tags=True) # try solving with multiple parts but incomplete set self._use('1-stone, 2-stone', 'You try to utilize these but nothing happens ... something amiss?') # arm the other puzzle. Their parts are identical self._arm(recipe2_dbref, 'makefire2', ['stone', 'flint']) - self._check_room_contents({'stone': 3, 'flint': 3, 'fire': 1}) + self._check_room_contents({'stone': 3, 'flint': 3, 'fire': 1}, check_test_tags=True) # solve with multiple parts for # multiple puzzles. Both can be solved but @@ -1413,12 +1424,12 @@ class TestPuzzles(CommandTest): self._use( '1-stone, 2-flint, 3-stone, 3-flint', 'Your gears start turning and a bunch of ideas come to your mind ... ') - self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}) + self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}, check_test_tags=True) # solve all self._use('1-stone, 1-flint', 'You are a Genius') self._use('stone, flint', 'You are a Genius') - self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}) + self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}, check_test_tags=True) def test_puzzleedit(self): recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) From ebcbbc2d0fe8502dac1bdef2cebad3c71cae0a82 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 16 Sep 2018 18:19:04 -0500 Subject: [PATCH 17/37] Bump up coverage for puzzles module --- evennia/contrib/puzzles.py | 2 +- evennia/contrib/tests.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 39c8a69765..0cab7859ac 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -305,7 +305,7 @@ class CmdEditPuzzle(MuxCommand): puzzle = search.search_script(recipe_dbref) if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe): - caller.msg('Invalid puzzle %r' % (recipe_dbref)) + caller.msg('%s(%s) is not a puzzle' % (puzzle[0].name, recipe_dbref)) return puzzle = puzzle[0] diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 13e06f209f..4810ec9d09 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1440,17 +1440,23 @@ class TestPuzzles(CommandTest): self.fire.delete() def _puzzleedit(swt, dbref, args, expmsg): + if (swt is None) and (dbref is None) and (args is None): + cmdstr = '' + else: + cmdstr = '%s %s%s' % (swt, dbref, args) self.call( puzzles.CmdEditPuzzle(), - '%s %s%s' % (swt, dbref, args), + cmdstr, expmsg, caller=self.char1 ) # bad syntax + _puzzleedit(None, None, None, "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") _puzzleedit('', '1', '', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") _puzzleedit('', '', '', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") _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") From a6918561c524819bc946f84b7b9ab7bec092f873 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 16 Sep 2018 19:04:07 -0500 Subject: [PATCH 18/37] Add total recipes/armed-puzzles to @lspuzzlerecipes and @lsarmedpuzzles --- evennia/contrib/puzzles.py | 6 ++++++ evennia/contrib/tests.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 0cab7859ac..2d317efe46 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -565,6 +565,9 @@ class CmdListPuzzleRecipes(MuxCommand): for k, v in protoresult.items(): text.append(msgf_item % (mark, k, v)) mark = '' + else: + text.append(div) + text.append('%d puzzle(s).' % (len(recipes))) text.append(div) caller.msg('\n'.join(text)) @@ -601,6 +604,9 @@ class CmdListArmedPuzzles(MuxCommand): text.append(msgf_item % ( item.name, item.dbref, item.location.name, item.location.dbref)) + else: + text.append(div) + text.append('%d armed puzzle(s).' % (len(armed_puzzles))) text.append(div) caller.msg('\n'.join(text)) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 4810ec9d09..2aaa614ae8 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1485,7 +1485,8 @@ class TestPuzzles(CommandTest): msg, [ r"^-+$", - r"^-+$", + r"^0 puzzle\(s\)\.$", + r"-+$", ], re.MULTILINE | re.DOTALL ) @@ -1513,6 +1514,8 @@ class TestPuzzles(CommandTest): r"^.*key: stone$", r"^.*key: flint$", r"^-+$", + r"^1 puzzle\(s\)\.$", + r"^-+$", ], re.MULTILINE | re.DOTALL ) @@ -1526,6 +1529,8 @@ class TestPuzzles(CommandTest): msg, [ r"^-+$", + r"^-+$", + r"^0 armed puzzle\(s\)\.$", r"^-+$" ], re.MULTILINE | re.DOTALL @@ -1545,6 +1550,7 @@ class TestPuzzles(CommandTest): r"^Puzzle name: makefire$", r"^.*stone.* at \s+ Room.*$", r"^.*flint.* at \s+ Room.*$", + r"^1 armed puzzle\(s\)\.$", r"^-+$", ], re.MULTILINE | re.DOTALL From ee0a22757fc5634379e0173bd79dd3ef635a5a1c Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 16 Sep 2018 19:06:48 -0500 Subject: [PATCH 19/37] Minor msg editing/formatting --- evennia/contrib/puzzles.py | 4 ++-- evennia/contrib/tests.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 2d317efe46..4816bdde0a 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -567,7 +567,7 @@ class CmdListPuzzleRecipes(MuxCommand): mark = '' else: text.append(div) - text.append('%d puzzle(s).' % (len(recipes))) + text.append('Found |r%d|n puzzle(s).' % (len(recipes))) text.append(div) caller.msg('\n'.join(text)) @@ -606,7 +606,7 @@ class CmdListArmedPuzzles(MuxCommand): item.location.name, item.location.dbref)) else: text.append(div) - text.append('%d armed puzzle(s).' % (len(armed_puzzles))) + text.append('Found |r%d|n armed puzzle(s).' % (len(armed_puzzles))) text.append(div) caller.msg('\n'.join(text)) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 2aaa614ae8..a32d338b15 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1485,7 +1485,7 @@ class TestPuzzles(CommandTest): msg, [ r"^-+$", - r"^0 puzzle\(s\)\.$", + r"^Found 0 puzzle\(s\)\.$", r"-+$", ], re.MULTILINE | re.DOTALL @@ -1514,7 +1514,7 @@ class TestPuzzles(CommandTest): r"^.*key: stone$", r"^.*key: flint$", r"^-+$", - r"^1 puzzle\(s\)\.$", + r"^Found 1 puzzle\(s\)\.$", r"^-+$", ], re.MULTILINE | re.DOTALL @@ -1530,7 +1530,7 @@ class TestPuzzles(CommandTest): [ r"^-+$", r"^-+$", - r"^0 armed puzzle\(s\)\.$", + r"^Found 0 armed puzzle\(s\)\.$", r"^-+$" ], re.MULTILINE | re.DOTALL @@ -1550,7 +1550,7 @@ class TestPuzzles(CommandTest): r"^Puzzle name: makefire$", r"^.*stone.* at \s+ Room.*$", r"^.*flint.* at \s+ Room.*$", - r"^1 armed puzzle\(s\)\.$", + r"^Found 1 armed puzzle\(s\)\.$", r"^-+$", ], re.MULTILINE | re.DOTALL From 3c532f075f46616bd06980927f6f9fa680659425 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Mon, 17 Sep 2018 20:57:11 -0500 Subject: [PATCH 20/37] Add use_success_location_message so contents of room 'see' puzzle solver succeeding. Add parts are interchangeable warning confirmation when new puzzle-recipe matches existing one. --- evennia/contrib/puzzles.py | 64 ++++++++++++++++++++++++++++---------- evennia/contrib/tests.py | 11 +++++-- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 4816bdde0a..47166c88f9 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -89,6 +89,7 @@ _PUZZLES_TAG_MEMBER = 'puzzle_member' _PUZZLE_DEFAULT_FAIL_USE_MESSAGE = 'You try to utilize %s but nothing happens ... something amiss?' _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = 'You are a Genius!!!' +_PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE = "|c{caller}|n performs some kind of tribal dance and |y{result_names}|n seems to appear from thin air" # ----------- UTILITY FUNCTIONS ------------ @@ -114,13 +115,17 @@ def proto_def(obj, with_tags=True): return protodef # Colorize the default success message -_i = 0 -_colors = ['|r', '|g', '|y'] -_msg = [] -for l in _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE: - _msg += _colors[_i] + l - _i = (_i + 1) % len(_colors) -_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = ''.join(_msg) + '|n' +def _colorize_message(msg): + _i = 0 + _colors = ['|r', '|g', '|y'] + _msg = [] + for l in msg: + _msg += _colors[_i] + l + _i = (_i + 1) % len(_colors) + msg = ''.join(_msg) + '|n' + return msg + +_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE) # ------------------------------------------ @@ -149,6 +154,7 @@ class PuzzleRecipe(DefaultScript): self.db.results = tuple(results) 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 class CmdCreatePuzzleRecipe(MuxCommand): @@ -168,6 +174,9 @@ class CmdCreatePuzzleRecipe(MuxCommand): locks = 'cmd:perm(puzzle) or perm(Builder)' help_category = 'Puzzles' + confirm = True + default_confirm = 'no' + def func(self): caller = self.caller @@ -182,9 +191,25 @@ class CmdCreatePuzzleRecipe(MuxCommand): caller.msg('Invalid puzzle name %r.' % puzzle_name) return - # TODO: if there is another puzzle with same name + # if there is another puzzle with same name # warn user that parts and results will be # interchangable + _puzzles = search.search_script_attribute( + key='puzzle_name', + value=puzzle_name + ) + _puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles)) + if _puzzles: + confirm = 'There are %d puzzles with the same name.\n' % len(_puzzles) \ + + 'Its parts and results will be interchangeable.\n' \ + + 'Continue yes/[no]? ' + answer = '' + while answer.strip().lower() not in ('y', 'yes', 'n', 'no'): + answer = yield(confirm) + answer = self.default_confirm if answer == '' else answer + if answer.strip().lower() in ('n', 'no'): + caller.msg('Cancelled: no puzzle created.') + return def is_valid_obj_location(obj): valid = True @@ -275,14 +300,16 @@ class CmdEditPuzzle(MuxCommand): Usage: @puzzleedit[/delete] <#dbref> @puzzleedit <#dbref>/use_success_message = + @puzzleedit <#dbref>/use_success_location_message = Switches: delete - deletes the recipe. Existing parts and results aren't modified + use_success_location_message containing {result_names} and {caller} will automatically be replaced with correct values. Both are optional. + """ key = '@puzzleedit' - # FIXME: permissions for scripts? locks = 'cmd:perm(puzzleedit) or perm(Builder)' help_category = 'Puzzles' @@ -332,7 +359,13 @@ class CmdEditPuzzle(MuxCommand): caller.msg( "%s use_success_message = %s\n" % (puzzle_name_id, puzzle.db.use_success_message) ) - return + return + elif attr == 'use_success_location_message': + puzzle.db.use_success_location_message = self.rhs + caller.msg( + "%s use_success_location_message = %s\n" % (puzzle_name_id, puzzle.db.use_success_location_message) + ) + return class CmdArmPuzzle(MuxCommand): @@ -341,7 +374,6 @@ class CmdArmPuzzle(MuxCommand): """ key = '@armpuzzle' - # FIXME: permissions for scripts? locks = 'cmd:perm(armpuzzle) or perm(Builder)' help_category = 'Puzzles' @@ -520,11 +552,10 @@ class CmdUsePuzzleParts(MuxCommand): result_names = ', '.join(result_names) caller.msg(puzzle.db.use_success_message) - # TODO: allow custom message for location and channels caller.location.msg_contents( - "|c%s|n performs some kind of tribal dance" - " and |y%s|n seems to appear from thin air" % ( - caller, result_names), exclude=(caller,) + puzzle.db.use_success_location_message.format( + caller=caller, result_names=result_names), + exclude=(caller,) ) @@ -552,7 +583,8 @@ class CmdListPuzzleRecipes(MuxCommand): msgf_item = "%2s|c%15s|n: |w%s|n" for recipe in recipes: text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref)) - text.append('Success message:\n' + recipe.db.use_success_message + '\n') + 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('Parts') for protopart in recipe.db.parts[:]: mark = '-' diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index a32d338b15..744251774c 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1426,8 +1426,11 @@ class TestPuzzles(CommandTest): 'Your gears start turning and a bunch of ideas come to your mind ... ') self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}, check_test_tags=True) + self.room1.msg_contents = Mock() + # solve all self._use('1-stone, 1-flint', 'You are a Genius') + self.room1.msg_contents.assert_called_once_with('|cChar|n performs some kind of tribal dance and |yfire|n seems to appear from thin air', exclude=(self.char1,)) self._use('stone, flint', 'You are a Genius') self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}, check_test_tags=True) @@ -1466,10 +1469,13 @@ class TestPuzzles(CommandTest): puzzle = search.search_script(recipe_dbref)[0] puzzle.locks.add('control:id(%s)' % self.char1.dbref[1:]) - # edit use_success_message + # 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) self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + 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,)) # delete _puzzleedit('/delete', recipe_dbref, '', 'makefire(%s) was deleted' % recipe_dbref) @@ -1505,7 +1511,8 @@ class TestPuzzles(CommandTest): [ r"^-+$", r"^Puzzle 'makefire'.*$", - r"^Success message:$", + r"^Success Caller message:$", + r"^Success Location message:$", r"^Parts$", r"^.*key: stone$", r"^.*key: flint$", From 555e8e5ac94d03fb8bec6d478384b6cbfc1a81ad Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 18 Sep 2018 22:46:01 -0500 Subject: [PATCH 21/37] Add add/del(part) and add/del(results) switches to @puzzleedit to modify recipes --- evennia/contrib/puzzles.py | 96 ++++++++++++++++++++++++++++++++++++-- evennia/contrib/tests.py | 72 ++++++++++++++++++++++++++-- 2 files changed, 160 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 47166c88f9..1738954066 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -301,12 +301,22 @@ class CmdEditPuzzle(MuxCommand): @puzzleedit[/delete] <#dbref> @puzzleedit <#dbref>/use_success_message = @puzzleedit <#dbref>/use_success_location_message = + @puzzleedit[/addpart] <#dbref> = + @puzzleedit[/delpart] <#dbref> = + @puzzleedit[/addresult] <#dbref> = + @puzzleedit[/delresult] <#dbref> = Switches: + addpart - adds parts to the puzzle + delpart - removes parts from the puzzle + addresult - adds results to the puzzle + delresult - removes results from the puzzle delete - deletes the recipe. Existing parts and results aren't modified 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. + """ key = '@puzzleedit' @@ -314,11 +324,11 @@ class CmdEditPuzzle(MuxCommand): help_category = 'Puzzles' def func(self): - _USAGE = "Usage: @puzzleedit[/switches] [/attribute = ]" + self._USAGE = "Usage: @puzzleedit[/switches] [/attribute = ]" caller = self.caller if not self.lhslist: - caller.msg(_USAGE) + caller.msg(self._USAGE) return if '/' in self.lhslist[0]: @@ -327,7 +337,7 @@ class CmdEditPuzzle(MuxCommand): recipe_dbref = self.lhslist[0] if not utils.dbref(recipe_dbref): - caller.msg("A puzzle recipe's #dbref must be specified.\n" + _USAGE) + caller.msg("A puzzle recipe's #dbref must be specified.\n" + self._USAGE) return puzzle = search.search_script(recipe_dbref) @@ -347,6 +357,34 @@ class CmdEditPuzzle(MuxCommand): caller.msg('%s was deleted' % puzzle_name_id) return + elif 'addpart' in self.switches: + objs = self._get_objs() + if objs: + added = self._add_parts(objs, puzzle) + caller.msg('%s were added to parts' % (', '.join(added))) + return + + elif 'delpart' in self.switches: + objs = self._get_objs() + if objs: + removed = self._remove_parts(objs, puzzle) + caller.msg('%s were removed from parts' % (', '.join(removed))) + return + + elif 'addresult' in self.switches: + objs = self._get_objs() + if objs: + added = self._add_results(objs, puzzle) + caller.msg('%s were added to results' % (', '.join(added))) + return + + elif 'delresult' in self.switches: + objs = self._get_objs() + if objs: + removed = self._remove_results(objs, puzzle) + caller.msg('%s were removed from results' % (', '.join(removed))) + return + else: # edit attributes @@ -367,6 +405,58 @@ class CmdEditPuzzle(MuxCommand): ) return + def _get_objs(self): + if not self.rhslist: + self.caller.msg(self._USAGE) + return + objs = [] + for o in self.rhslist: + obj = self.caller.search(o) + if obj: + objs.append(obj) + return objs + + def _add_objs_to(self, objs, to): + """Adds propto objs to the given set (parts or results)""" + added = [] + toobjs = list(to[:]) + for obj in objs: + protoobj = proto_def(obj) + toobjs.append(protoobj) + added.append(obj.key) + return added, toobjs + + def _remove_objs_from(self, objs, frm): + """Removes propto objs from the given set (parts or results)""" + removed = [] + fromobjs = list(frm[:]) + for obj in objs: + protoobj = proto_def(obj) + if protoobj in fromobjs: + fromobjs.remove(protoobj) + removed.append(obj.key) + return removed, fromobjs + + def _add_parts(self, objs, puzzle): + added, toobjs = self._add_objs_to(objs, puzzle.db.parts) + puzzle.db.parts = tuple(toobjs) + return added + + def _remove_parts(self, objs, puzzle): + removed, fromobjs = self._remove_objs_from(objs, puzzle.db.parts) + puzzle.db.parts = tuple(fromobjs) + return removed + + def _add_results(self, objs, puzzle): + added, toobjs = self._add_objs_to(objs, puzzle.db.results) + puzzle.db.results = tuple(toobjs) + return added + + def _remove_results(self, objs, puzzle): + removed, fromobjs = self._remove_objs_from(objs, puzzle.db.results) + puzzle.db.results = tuple(fromobjs) + return removed + class CmdArmPuzzle(MuxCommand): """ diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 744251774c..6eca07289b 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1437,11 +1437,6 @@ class TestPuzzles(CommandTest): def test_puzzleedit(self): recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) - # delete proto parts and proto results - self.stone.delete() - self.flint.delete() - self.fire.delete() - def _puzzleedit(swt, dbref, args, expmsg): if (swt is None) and (dbref is None) and (args is None): cmdstr = '' @@ -1454,6 +1449,11 @@ class TestPuzzles(CommandTest): caller=self.char1 ) + # delete proto parts and proto results + self.stone.delete() + self.flint.delete() + self.fire.delete() + # bad syntax _puzzleedit(None, None, None, "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") _puzzleedit('', '1', '', "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit") @@ -1472,6 +1472,7 @@ class TestPuzzles(CommandTest): # 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) + self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) self.room1.msg_contents = Mock() self._use('stone, flint', 'Yes!') @@ -1481,6 +1482,67 @@ class TestPuzzles(CommandTest): _puzzleedit('/delete', recipe_dbref, '', 'makefire(%s) was deleted' % recipe_dbref) self._assert_no_recipes() + def test_puzzleedit_add_remove_parts_results(self): + recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) + + def _puzzleedit(swt, dbref, rhslist, expmsg): + cmdstr = '%s %s = %s' % (swt, dbref, ', '.join(rhslist)) + self.call( + puzzles.CmdEditPuzzle(), + cmdstr, + expmsg, + caller=self.char1 + ) + + red_stone = create_object(key='red stone', location=self.char1.location) + smoke = create_object(key='smoke', location=self.char1.location) + + _puzzleedit('/addresult', recipe_dbref, ['smoke'], 'smoke were added to results') + _puzzleedit('/addpart', recipe_dbref, ['red stone', 'stone'], 'red stone, stone were added to parts') + + # create a box so we can put all objects in + # so that they can't be found during puzzle resolution + self.box = create_object(key='box', location=self.char1.location) + def _box_all(): + for o in self.room1.contents: + if o not in [self.char1, self.char2, self.exit, + self.obj1, self.obj2, self.box]: + o.location = self.box + _box_all() + + self._arm(recipe_dbref, 'makefire', ['stone', 'flint', 'red stone', 'stone']) + self._check_room_contents({ + 'stone': 2, + 'red stone': 1, + 'flint': 1, + }) + self._use('1-stone, flint', 'You try to utilize these but nothing happens ... something amiss?') + self._use('1-stone, flint, red stone, 3-stone', 'You are a Genius') + self._check_room_contents({ + 'smoke': 1, + 'fire': 1 + }) + _box_all() + + self.fire.location = self.room1 + self.stone.location = self.room1 + + _puzzleedit('/delresult', recipe_dbref, ['fire'], 'fire were removed from results') + _puzzleedit('/delpart', recipe_dbref, ['stone', 'stone'], 'stone, stone were removed from parts') + + _box_all() + + self._arm(recipe_dbref, 'makefire', ['flint', 'red stone']) + self._check_room_contents({ + 'red stone': 1, + 'flint': 1, + }) + self._use('red stone, flint', 'You are a Genius') + self._check_room_contents({ + 'smoke': 1, + 'fire': 0 + }) + def test_lspuzzlerecipes_lsarmedpuzzles(self): msg = self.call( puzzles.CmdListPuzzleRecipes(), From 2d7d6a8b783b66b9a1741348ca011baf582ca33d Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 2 Oct 2018 15:17:15 +0100 Subject: [PATCH 22/37] Remove PuzzlePartObject and typeclass dependency --- evennia/contrib/puzzles.py | 45 ++++++++---------- evennia/contrib/tests.py | 93 ++++++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 48 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 1738954066..c4654483ca 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -49,8 +49,8 @@ command). Once the recipe is created, all parts and result can be disposed (i.e. destroyed). At a later time, a Builder or a Script can arm the puzzle -and spawn all puzzle parts (PuzzlePartObject) in their -respective locations (See @armpuzzle). +and spawn all puzzle parts in their respective +locations (See @armpuzzle). A regular player can collect the puzzle parts and combine them (See use command). If player has specified @@ -101,7 +101,7 @@ def proto_def(obj, with_tags=True): protodef = { # FIXME: Don't we need to honor ALL properties? attributes, contents, etc. 'key': obj.key, - 'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass + 'typeclass': obj.typeclass_path, 'desc': obj.db.desc, 'location': obj.location, 'home': obj.home, @@ -129,20 +129,6 @@ _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_ # ------------------------------------------ -class PuzzlePartObject(DefaultObject): - """ - Puzzle Part, typically used by @armpuzzle command - """ - - def mark_as_puzzle_member(self, puzzle_name): - """ - Marks this object as a member of puzzle named - 'puzzle_name' - """ - self.db.puzzle_name = puzzle_name - self.tags.add(puzzle_name, category=_PUZZLES_TAG_CATEGORY) - - class PuzzleRecipe(DefaultScript): """ Definition of a Puzzle Recipe @@ -161,12 +147,20 @@ class CmdCreatePuzzleRecipe(MuxCommand): """ Creates a puzzle recipe. - Each part and result must exist and be placed in their corresponding location. - All parts and results are left intact. Caller must explicitly - destroy them. + Each part and result must exist and be placed in their + corresponding location. + + They are all left intact and Caller should explicitly destroy + them. If the /arm switch is used, the specified objects become + puzzle parts ready to be combined and spawn a new result. + + Switches: + arm - the specified objects become puzzle parts as if the puzzle + had been armed explicitly. The results are left intact so + they must be explicitly destroyed. Usage: - @puzzle name,] = + @puzzle[/arm] name,] = """ key = '@puzzle' @@ -487,7 +481,8 @@ class CmdArmPuzzle(MuxCommand): for proto_part in puzzle.db.parts: part = spawn(proto_part)[0] caller.msg("Part %s(%s) spawned and placed at %s(%s)" % (part.name, part.dbref, part.location, part.location.dbref)) - part.mark_as_puzzle_member(puzzle.db.puzzle_name) + part.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY) + part.db.puzzle_name = puzzle.db.puzzle_name caller.msg("Puzzle armed |gsuccessfully|n.") @@ -536,8 +531,7 @@ class CmdUsePuzzleParts(MuxCommand): if not part: return - if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY) \ - or not inherits_from(part, PuzzlePartObject): + if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY): # not a puzzle part ... abort caller.msg('You have no idea how %s can be used' % (many)) @@ -629,7 +623,8 @@ class CmdUsePuzzleParts(MuxCommand): result_names = [] for proto_result in puzzle.db.results: result = spawn(proto_result)[0] - result.mark_as_puzzle_member(puzzle.db.puzzle_name) + result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY) + result.db.puzzle_name = puzzle.db.puzzle_name result_names.append(result.name) # FIXME: add 'ramdon' messages: # Hmmm ... did I search result.location? diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 6eca07289b..62d5dc81ae 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1183,9 +1183,15 @@ class TestPuzzles(CommandTest): def setUp(self): super(TestPuzzles, self).setUp() - self.stone = create_object(key='stone', location=self.char1.location) - self.flint = create_object(key='flint', location=self.char1.location) - self.fire = create_object(key='fire', location=self.char1.location) + self.stone = create_object( + self.object_typeclass, + key='stone', location=self.char1.location) + self.flint = create_object( + self.object_typeclass, + key='flint', location=self.char1.location) + self.fire = create_object( + self.object_typeclass, + key='fire', location=self.char1.location) self.stone.tags.add('tag-stone') self.stone.tags.add('tag-stone', category='tagcat') self.flint.tags.add('tag-flint') @@ -1397,7 +1403,10 @@ class TestPuzzles(CommandTest): self.assertEqual(1, len(list(filter( lambda o: o.key == 'fire' \ - and inherits_from(o,'evennia.contrib.puzzles.PuzzlePartObject'), + and ('makefire', puzzles._PUZZLES_TAG_CATEGORY) \ + in o.tags.all(return_key_and_category=True) \ + and (puzzles._PUZZLES_TAG_MEMBER, puzzles._PUZZLES_TAG_CATEGORY) \ + in o.tags.all(return_key_and_category=True), self.room1.contents)))) self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 1}, check_test_tags=True) @@ -1494,15 +1503,21 @@ class TestPuzzles(CommandTest): caller=self.char1 ) - red_stone = create_object(key='red stone', location=self.char1.location) - smoke = create_object(key='smoke', location=self.char1.location) + red_stone = create_object( + self.object_typeclass, + key='red stone', location=self.char1.location) + smoke = create_object( + self.object_typeclass, + key='smoke', location=self.char1.location) _puzzleedit('/addresult', recipe_dbref, ['smoke'], 'smoke were added to results') _puzzleedit('/addpart', recipe_dbref, ['red stone', 'stone'], 'red stone, stone were added to parts') # create a box so we can put all objects in # so that they can't be found during puzzle resolution - self.box = create_object(key='box', location=self.char1.location) + self.box = create_object( + self.object_typeclass, + key='box', location=self.char1.location) def _box_all(): for o in self.room1.contents: if o not in [self.char1, self.char2, self.exit, @@ -1634,12 +1649,24 @@ class TestPuzzles(CommandTest): # parts don't survive resolution # but produce a large result set - tree = create_object(key='tree', location=self.char1.location) - axe = create_object(key='axe', location=self.char1.location) - sweat = create_object(key='sweat', location=self.char1.location) - dull_axe = create_object(key='dull axe', location=self.char1.location) - timber = create_object(key='timber', location=self.char1.location) - log = create_object(key='log', location=self.char1.location) + tree = create_object( + self.object_typeclass, + key='tree', location=self.char1.location) + axe = create_object( + self.object_typeclass, + key='axe', location=self.char1.location) + sweat = create_object( + self.object_typeclass, + key='sweat', location=self.char1.location) + dull_axe = create_object( + self.object_typeclass, + key='dull axe', location=self.char1.location) + timber = create_object( + self.object_typeclass, + key='timber', location=self.char1.location) + log = create_object( + self.object_typeclass, + key='log', location=self.char1.location) parts = ['tree', 'axe'] results = (['sweat'] * 10) + ['dull axe'] + (['timber'] * 20) + (['log'] * 50) recipe_dbref = self._good_recipe( @@ -1666,9 +1693,15 @@ class TestPuzzles(CommandTest): # parts also appear in results # causing a new puzzle to be armed 'automatically' # i.e. the puzzle is self-sustaining - hole = create_object(key='hole', location=self.char1.location) - shovel = create_object(key='shovel', location=self.char1.location) - dirt = create_object(key='dirt', location=self.char1.location) + hole = create_object( + self.object_typeclass, + key='hole', location=self.char1.location) + shovel = create_object( + self.object_typeclass, + key='shovel', location=self.char1.location) + dirt = create_object( + self.object_typeclass, + key='dirt', location=self.char1.location) parts = ['shovel', 'hole'] results = ['dirt', 'hole', 'shovel'] @@ -1706,12 +1739,24 @@ class TestPuzzles(CommandTest): def test_e2e_interchangeable_parts_and_results(self): # Parts and Results can be used in multiple puzzles - egg = create_object(key='egg', location=self.char1.location) - flour = create_object(key='flour', location=self.char1.location) - boiling_water = create_object(key='boiling water', location=self.char1.location) - boiled_egg = create_object(key='boiled egg', location=self.char1.location) - dough = create_object(key='dough', location=self.char1.location) - pasta = create_object(key='pasta', location=self.char1.location) + egg = create_object( + self.object_typeclass, + key='egg', location=self.char1.location) + flour = create_object( + self.object_typeclass, + key='flour', location=self.char1.location) + boiling_water = create_object( + self.object_typeclass, + key='boiling water', location=self.char1.location) + boiled_egg = create_object( + self.object_typeclass, + key='boiled egg', location=self.char1.location) + dough = create_object( + self.object_typeclass, + key='dough', location=self.char1.location) + pasta = create_object( + self.object_typeclass, + key='pasta', location=self.char1.location) # Three recipes: # 1. breakfast: egg + boiling water = boiled egg & boiling water @@ -1755,7 +1800,9 @@ class TestPuzzles(CommandTest): # create a box so we can put all objects in # so that they can't be found during puzzle resolution - self.box = create_object(key='box', location=self.char1.location) + self.box = create_object( + self.object_typeclass, + key='box', location=self.char1.location) def _box_all(): # print "boxing all\n", "-"*20 for o in self.room1.contents: From 67fecbe3fcbb4ff7acaef09cf9bfa98b234cf950 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 6 Oct 2018 08:00:23 -0500 Subject: [PATCH 23/37] 0.8.0 port --- evennia/contrib/puzzles.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index c4654483ca..4b84e4ef27 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -79,7 +79,7 @@ from evennia import DefaultExit from evennia.commands.default.muxcommand import MuxCommand from evennia.utils.utils import inherits_from from evennia.utils import search, utils, logger -from evennia.utils.spawner import spawn +from evennia.prototypes.spawner import spawn # Tag used by puzzles _PUZZLES_TAG_CATEGORY = 'puzzles' @@ -100,6 +100,7 @@ def proto_def(obj, with_tags=True): """ protodef = { # FIXME: Don't we need to honor ALL properties? attributes, contents, etc. + 'prototype_key': '%s(%s)' % (obj.key, obj.dbref), 'key': obj.key, 'typeclass': obj.typeclass_path, 'desc': obj.db.desc, From a3b37d60edcf648836707bba4e239f66c0523abe Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 6 Oct 2018 19:29:32 -0500 Subject: [PATCH 24/37] Debug code: comparing .tags.all() vs tags.get() --- evennia/contrib/puzzles.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 4b84e4ef27..663ecea1a6 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -79,7 +79,11 @@ from evennia import DefaultExit from evennia.commands.default.muxcommand import MuxCommand from evennia.utils.utils import inherits_from from evennia.utils import search, utils, logger -from evennia.prototypes.spawner import spawn +from evennia.prototypes.spawner import ( + spawn, + # prototype_from_object, + # prototype_diff +) # Tag used by puzzles _PUZZLES_TAG_CATEGORY = 'puzzles' @@ -110,8 +114,12 @@ def proto_def(obj, with_tags=True): 'permissions': obj.permissions.all()[:], } if with_tags: + # import ipdb + # ipdb.set_trace() + _tags = obj.tags.get(return_tagobj=True, return_list=True) tags = obj.tags.all(return_key_and_category=True) - tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY)) + tags = [(t[0], t[1], None) for t in tags] + tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None)) protodef['tags'] = tags return protodef From 41bfea38f9bb690e23abbf7485e51bca69a94726 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 7 Oct 2018 11:30:42 -0500 Subject: [PATCH 25/37] Prototypes automatically adds 'prototype_key' and such prevents objects/puzzle-parts matching during puzzle resolution. Fix tags processing in spawner --- evennia/contrib/puzzles.py | 17 +++++++---------- evennia/prototypes/spawner.py | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 663ecea1a6..3c79326e58 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -79,11 +79,7 @@ from evennia import DefaultExit from evennia.commands.default.muxcommand import MuxCommand from evennia.utils.utils import inherits_from from evennia.utils import search, utils, logger -from evennia.prototypes.spawner import ( - spawn, - # prototype_from_object, - # prototype_diff -) +from evennia.prototypes.spawner import spawn # Tag used by puzzles _PUZZLES_TAG_CATEGORY = 'puzzles' @@ -114,9 +110,6 @@ def proto_def(obj, with_tags=True): 'permissions': obj.permissions.all()[:], } if with_tags: - # import ipdb - # ipdb.set_trace() - _tags = obj.tags.get(return_tagobj=True, return_list=True) tags = obj.tags.all(return_key_and_category=True) tags = [(t[0], t[1], None) for t in tags] tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None)) @@ -555,7 +548,10 @@ class CmdUsePuzzleParts(MuxCommand): puzzle_ingredients = dict() for part in parts: parts_dict[part.dbref] = part - puzzle_ingredients[part.dbref] = proto_def(part, with_tags=False) + protodef = proto_def(part, with_tags=False) + # remove 'prototype_key' as it will prevent equality + del(protodef['prototype_key']) + puzzle_ingredients[part.dbref] = protodef tags_categories = part.tags.all(return_key_and_category=True) for tag, category in tags_categories: if category != _PUZZLES_TAG_CATEGORY: @@ -585,9 +581,10 @@ class CmdUsePuzzleParts(MuxCommand): matched_puzzles = dict() for puzzle in puzzles: puzzle_protoparts = list(puzzle.db.parts[:]) - # remove tags as they prevent equality + # remove tags and prototype_key as they prevent equality for puzzle_protopart in puzzle_protoparts: del(puzzle_protopart['tags']) + del(puzzle_protopart['prototype_key']) matched_dbrefparts = [] parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name] for part_dbref in parts_dbrefs: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index ce08944139..fd9b4545e6 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -714,8 +714,8 @@ def spawn(*prototypes, **kwargs): val = prot.pop("tags", []) tags = [] - for (tag, category, data) in tags: - tags.append((init_spawn_value(val, str), category, data)) + for (tag, category, data) in val: + tags.append((init_spawn_value(tag, str), category, data)) prototype_key = prototype.get('prototype_key', None) if prototype_key: From b8633646350c4361a1e56a5357da62a07ce260ff Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 7 Oct 2018 11:50:59 -0500 Subject: [PATCH 26/37] Replace FIXMEs with TODOs --- evennia/contrib/puzzles.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 3c79326e58..4922c9191a 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -99,7 +99,7 @@ def proto_def(obj, with_tags=True): and compare recipe with candidate part """ protodef = { - # FIXME: Don't we need to honor ALL properties? attributes, contents, etc. + # TODO: Don't we need to honor ALL properties? attributes, contents, etc. 'prototype_key': '%s(%s)' % (obj.key, obj.dbref), 'key': obj.key, 'typeclass': obj.typeclass_path, @@ -615,8 +615,8 @@ class CmdUsePuzzleParts(MuxCommand): # if there are more than one, ... if len(largest_puzzles) > 1: - # FIXME: pick a random one or let user choose? - # FIXME: do we show the puzzle name or something else? + # TODO: pick a random one or let user choose? + # TODO: do we show the puzzle name or something else? caller.msg( 'Your gears start turning and a bunch of ideas come to your mind ...\n%s' % ( ' ...\n'.join([puzzles_dict[lp[0]].db.puzzle_name for lp in largest_puzzles])) @@ -632,7 +632,7 @@ class CmdUsePuzzleParts(MuxCommand): result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY) result.db.puzzle_name = puzzle.db.puzzle_name result_names.append(result.name) - # FIXME: add 'ramdon' messages: + # TODO: add 'ramdon' messages: # Hmmm ... did I search result.location? # What was that? ... I heard something in result.location? # Eureka! you built a result From 62f94f1cbdcc8fd0ced2fca53aecad844a8f8a76 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sat, 13 Oct 2018 16:47:06 -0500 Subject: [PATCH 27/37] force PuzzleRecipe.db.puzzle_name to be stored as 'str' instead of unicode. Corresponding testcase --- evennia/contrib/puzzles.py | 2 +- evennia/contrib/tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 4922c9191a..9db9fa356a 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -137,7 +137,7 @@ class PuzzleRecipe(DefaultScript): """ def save_recipe(self, puzzle_name, parts, results): - self.db.puzzle_name = puzzle_name + self.db.puzzle_name = str(puzzle_name) self.db.parts = tuple(parts) self.db.results = tuple(results) self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index e1b808df4b..bdf06aa05c 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1922,7 +1922,7 @@ class TestPuzzles(CommandTest): self._use('stone flint', 'There is no stone flint around.') self._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) + recipe_dbref = self._good_recipe(unicode('makefire'), ['stone', 'flint'], ['fire'] , and_destroy_it=False) recipe2_dbref = self._good_recipe('makefire2', ['stone', 'flint'], ['fire'] , and_destroy_it=False, expected_count=2) From 97dfcd81b59687aef481fd82c0b74cd4357c8c34 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 28 Oct 2018 10:53:42 -0500 Subject: [PATCH 28/37] e2e of puzzle that consumes all parts and produces no results --- evennia/contrib/tests.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index bdf06aa05c..118f0692f6 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2274,6 +2274,33 @@ class TestPuzzles(CommandTest): expected.update({'dirt': nresolutions}) self._check_room_contents(expected) + # parts don't survive resolution + # and no result is produced + balloon = create_object( + self.object_typeclass, + key='Balloon', location=self.char1.location) + parts = ['Balloon'] + results = ['Balloon'] # FIXME: we don't want results + recipe_dbref = self._good_recipe( + 'boom!!!', + parts, results, + and_destroy_it=False, + expected_count=3 + ) + + _destroy_objs_in_room(set(parts + results)) + + sps = sorted(parts) + expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} + + self._arm(recipe_dbref, 'boom!!!', parts) + self._check_room_contents(expected) + + self._use(','.join(parts), 'You are a Genius') + srs = sorted(set(results)) + 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 From d46472d91bd3d7088bf9dc0710d3ecbb9bd95fa3 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 28 Oct 2018 10:56:15 -0500 Subject: [PATCH 29/37] Puzzle recipe with uppercase letter FAILS to be resolved --- evennia/contrib/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 118f0692f6..3912bd2dab 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2282,7 +2282,7 @@ class TestPuzzles(CommandTest): parts = ['Balloon'] results = ['Balloon'] # FIXME: we don't want results recipe_dbref = self._good_recipe( - 'boom!!!', + 'Boom!!!', parts, results, and_destroy_it=False, expected_count=3 @@ -2293,7 +2293,7 @@ class TestPuzzles(CommandTest): sps = sorted(parts) expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} - self._arm(recipe_dbref, 'boom!!!', parts) + self._arm(recipe_dbref, 'Boom!!!', parts) self._check_room_contents(expected) self._use(','.join(parts), 'You are a Genius') From 6254762b413022e775c1f26bdaace40300bafe1c Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Mon, 5 Nov 2018 18:41:25 -0600 Subject: [PATCH 30/37] Minor cleanup and when more than one puzzle can be resolved, don't show their names but just how many --- evennia/contrib/puzzles.py | 23 ++++++++--------------- evennia/contrib/tests.py | 6 +++--- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 9db9fa356a..a44329d40a 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -501,11 +501,6 @@ class CmdUsePuzzleParts(MuxCommand): use ] """ - # TODO: consider allowing builder to provide - # messages and "hooks" that can be displayed - # and/or fired whenever the resolver of the puzzle - # enters the location where a result was spawned - key = 'use' aliases = 'combine' locks = 'cmd:pperm(use) or pperm(Player)' @@ -597,7 +592,7 @@ class CmdUsePuzzleParts(MuxCommand): matched_puzzles[puzzle.dbref] = matched_dbrefparts if len(matched_puzzles) == 0: - # TODO: we could use part.fail_message instead, if any + # TODO: we could use part.fail_message instead, if there was one # random part falls and lands on your feet # random part hits you square on the face caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many)) @@ -613,13 +608,15 @@ class CmdUsePuzzleParts(MuxCommand): puzzle = puzzles_dict[puzzledbref] largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples)) - # if there are more than one, ... + # if there are more than one, choose one at random. + # we could show the names of all those that can be resolved + # but that would give away that there are other puzzles that + # can be resolved with the same parts. + # just hint how many. if len(largest_puzzles) > 1: - # TODO: pick a random one or let user choose? - # TODO: do we show the puzzle name or something else? caller.msg( - 'Your gears start turning and a bunch of ideas come to your mind ...\n%s' % ( - ' ...\n'.join([puzzles_dict[lp[0]].db.puzzle_name for lp in largest_puzzles])) + 'Your gears start turning and %d different ideas come to your mind ...\n' + % (len(largest_puzzles)) ) puzzletuple = choice(largest_puzzles) puzzle = puzzles_dict[puzzletuple[0]] @@ -632,10 +629,6 @@ class CmdUsePuzzleParts(MuxCommand): result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY) result.db.puzzle_name = puzzle.db.puzzle_name result_names.append(result.name) - # TODO: add 'ramdon' messages: - # Hmmm ... did I search result.location? - # What was that? ... I heard something in result.location? - # Eureka! you built a result # Destroy all parts used for dbref in matched_dbrefparts: diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 3912bd2dab..71236a14df 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1977,7 +1977,7 @@ class TestPuzzles(CommandTest): # only one is. self._use( '1-stone, 2-flint, 3-stone, 3-flint', - 'Your gears start turning and a bunch of ideas come to your mind ... ') + 'Your gears start turning and 2 different ideas come to your mind ... ') self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}, check_test_tags=True) self.room1.msg_contents = Mock() @@ -2282,7 +2282,7 @@ class TestPuzzles(CommandTest): parts = ['Balloon'] results = ['Balloon'] # FIXME: we don't want results recipe_dbref = self._good_recipe( - 'Boom!!!', + 'boom!!!', # FIXME: uppercase name fails parts, results, and_destroy_it=False, expected_count=3 @@ -2293,7 +2293,7 @@ class TestPuzzles(CommandTest): sps = sorted(parts) expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)} - self._arm(recipe_dbref, 'Boom!!!', parts) + self._arm(recipe_dbref, 'boom!!!', parts) self._check_room_contents(expected) self._use(','.join(parts), 'You are a Genius') From e52e68a08e7051091eb97f69ad34555460fffc35 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 11 Nov 2018 18:54:15 -0600 Subject: [PATCH 31/37] Addition of mask to puzzles. Mask allows to mask-out part attributes during parts-and-recipes matching --- evennia/contrib/puzzles.py | 30 +++++++++++++++++++++++++++++- evennia/contrib/tests.py | 31 +++++++++++++++---------------- 2 files changed, 44 insertions(+), 17 deletions(-) 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( From 1e4cfafdaa56ae111aefca1071e6938e4d3f92d2 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 11 Nov 2018 19:17:52 -0600 Subject: [PATCH 32/37] Cleanup --- evennia/contrib/tests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 6dfcab5d7f..e519a4dd40 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2281,13 +2281,12 @@ class TestPuzzles(CommandTest): expected.update({'dirt': nresolutions}) self._check_room_contents(expected) - # parts don't survive resolution - # and no result is produced + # Uppercase puzzle name balloon = create_object( self.object_typeclass, key='Balloon', location=self.char1.location) parts = ['Balloon'] - results = ['Balloon'] # FIXME: we don't want results + results = ['Balloon'] recipe_dbref = self._good_recipe( 'boom!!!', # FIXME: uppercase name fails parts, results, From b10049c0dfd3a93075f13bcad929fbb237f5ebd0 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Dec 2018 11:15:50 -0600 Subject: [PATCH 33/37] Cleanup --- evennia/contrib/puzzles.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 6e093c7455..4653ec195f 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -222,8 +222,8 @@ class CmdCreatePuzzleRecipe(MuxCommand): def is_valid_obj_location(obj): valid = True - # Valid locations are: room, ... - # TODO: other valid locations must be added here + # Rooms are the only valid locations. + # TODO: other valid locations could be added here. # Certain locations can be handled accordingly: e.g, # a part is located in a character's inventory, # perhaps will translate into the player character @@ -231,7 +231,6 @@ class CmdCreatePuzzleRecipe(MuxCommand): # located in the same room where the builder was # located. # Parts and results may have different valid locations - # TODO: handle contents of a given part if not inherits_from(obj.location, DefaultRoom): caller.msg('Invalid location for %s' % (obj.key)) valid = False From 45e123fe78476e0abf1cea600cc1922466337de7 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 2 Dec 2018 12:09:00 -0600 Subject: [PATCH 34/37] Replace word stone with word steel --- evennia/contrib/tests.py | 142 +++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index e519a4dd40..931743428e 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1728,17 +1728,17 @@ class TestPuzzles(CommandTest): def setUp(self): super(TestPuzzles, self).setUp() - self.stone = create_object( + self.steel = create_object( self.object_typeclass, - key='stone', location=self.char1.location) + key='steel', location=self.char1.location) self.flint = create_object( self.object_typeclass, key='flint', location=self.char1.location) self.fire = create_object( self.object_typeclass, key='fire', location=self.char1.location) - self.stone.tags.add('tag-stone') - self.stone.tags.add('tag-stone', category='tagcat') + self.steel.tags.add('tag-steel') + self.steel.tags.add('tag-steel', category='tagcat') self.flint.tags.add('tag-flint') self.flint.tags.add('tag-flint', category='tagcat') self.fire.tags.add('tag-fire') @@ -1852,9 +1852,9 @@ class TestPuzzles(CommandTest): self._assert_no_recipes() - 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']) + self._good_recipe('makefire', ['steel', 'flint'], ['fire', 'steel', 'flint']) + self._good_recipe('hot steels', ['steel', 'fire'], ['steel', 'fire']) + self._good_recipe('furnace', ['steel', 'steel', 'fire'], ['steel', 'steel', 'fire', 'fire', 'fire', 'fire']) # bad recipes def _bad_recipe(name, parts, results, fail_regex): @@ -1869,11 +1869,11 @@ class TestPuzzles(CommandTest): self.assertIsNotNone(re.match(fail_regex, msg), msg) _bad_recipe('name', ['nothing'], ['neither'], r"Could not find 'nothing'.") - _bad_recipe('name', ['stone'], ['nothing'], r"Could not find 'nothing'.") - _bad_recipe('', ['stone', 'fire'], ['stone', 'fire'], r"^Invalid puzzle name ''.") - self.stone.location = self.char1 - _bad_recipe('name', ['stone'], ['fire'], r"^Invalid location for stone$") - _bad_recipe('name', ['flint'], ['stone'], r"^Invalid location for stone$") + _bad_recipe('name', ['steel'], ['nothing'], r"Could not find 'nothing'.") + _bad_recipe('', ['steel', 'fire'], ['steel', 'fire'], r"^Invalid puzzle name ''.") + self.steel.location = self.char1 + _bad_recipe('name', ['steel'], ['fire'], r"^Invalid location for steel$") + _bad_recipe('name', ['flint'], ['steel'], r"^Invalid location for steel$") _bad_recipe('name', ['self'], ['fire'], r"^Invalid typeclass for Char$") _bad_recipe('name', ['here'], ['fire'], r"^Invalid typeclass for Room$") @@ -1894,16 +1894,16 @@ class TestPuzzles(CommandTest): caller=self.char1 ) - recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire', 'stone', 'flint'], and_destroy_it=False) + recipe_dbref = self._good_recipe('makefire', ['steel', 'flint'], ['fire', 'steel', 'flint'], and_destroy_it=False) # delete proto parts and proto result - self.stone.delete() + self.steel.delete() self.flint.delete() self.fire.delete() # good arm - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 1, 'flint': 1}, check_test_tags=True) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) + self._check_room_contents({'steel': 1, 'flint': 1}, check_test_tags=True) def _use(self, cmdstr, expmsg): msg = self.call( @@ -1918,33 +1918,33 @@ class TestPuzzles(CommandTest): self._use('', 'Use what?') self._use('something', 'There is no something around.') - self._use('stone', 'You have no idea how this can be used') - self._use('stone flint', 'There is no stone flint around.') - self._use('stone, flint', 'You have no idea how these can be used') + self._use('steel', 'You have no idea how this can be used') + self._use('steel flint', 'There is no steel flint around.') + self._use('steel, flint', 'You have no idea how these can be used') - recipe_dbref = self._good_recipe(unicode('makefire'), ['stone', 'flint'], ['fire'] , and_destroy_it=False) - recipe2_dbref = self._good_recipe('makefire2', ['stone', 'flint'], ['fire'] , and_destroy_it=False, + recipe_dbref = self._good_recipe(unicode('makefire'), ['steel', 'flint'], ['fire'] , and_destroy_it=False) + recipe2_dbref = self._good_recipe('makefire2', ['steel', 'flint'], ['fire'] , and_destroy_it=False, expected_count=2) - # although there is stone and flint + # although there is steel and flint # those aren't valid puzzle parts because # the puzzle hasn't been armed - self._use('stone', 'You have no idea how this can be used') - self._use('stone, flint', 'You have no idea how these can be used') - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 2, 'flint': 2}, check_test_tags=True) + self._use('steel', 'You have no idea how this can be used') + self._use('steel, flint', 'You have no idea how these can be used') + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) + self._check_room_contents({'steel': 2, 'flint': 2}, check_test_tags=True) # there are duplicated objects now - self._use('stone', 'Which stone. There are many') + self._use('steel', 'Which steel. There are many') self._use('flint', 'Which flint. There are many') # delete proto parts and proto results - self.stone.delete() + self.steel.delete() self.flint.delete() self.fire.delete() # solve puzzle - self._use('stone, flint', 'You are a Genius') + self._use('steel, flint', 'You are a Genius') self.assertEqual(1, len(list(filter( lambda o: o.key == 'fire' \ @@ -1953,43 +1953,43 @@ class TestPuzzles(CommandTest): and (puzzles._PUZZLES_TAG_MEMBER, puzzles._PUZZLES_TAG_CATEGORY) \ in o.tags.all(return_key_and_category=True), self.room1.contents)))) - self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 1}, check_test_tags=True) + self._check_room_contents({'steel': 0, 'flint': 0, 'fire': 1}, check_test_tags=True) # trying again will fail as it was resolved already # and the parts were destroyed - self._use('stone, flint', 'There is no stone around') - self._use('flint, stone', 'There is no flint around') + self._use('steel, flint', 'There is no steel around') + self._use('flint, steel', 'There is no flint around') # arm same puzzle twice so there are duplicated parts - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) - self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 1}, check_test_tags=True) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) + self._check_room_contents({'steel': 2, 'flint': 2, 'fire': 1}, check_test_tags=True) # try solving with multiple parts but incomplete set - self._use('1-stone, 2-stone', 'You try to utilize these but nothing happens ... something amiss?') + self._use('1-steel, 2-steel', 'You try to utilize these but nothing happens ... something amiss?') # arm the other puzzle. Their parts are identical - self._arm(recipe2_dbref, 'makefire2', ['stone', 'flint']) - self._check_room_contents({'stone': 3, 'flint': 3, 'fire': 1}, check_test_tags=True) + self._arm(recipe2_dbref, 'makefire2', ['steel', 'flint']) + self._check_room_contents({'steel': 3, 'flint': 3, 'fire': 1}, check_test_tags=True) # solve with multiple parts for # multiple puzzles. Both can be solved but # only one is. self._use( - '1-stone, 2-flint, 3-stone, 3-flint', + '1-steel, 2-flint, 3-steel, 3-flint', 'Your gears start turning and 2 different ideas come to your mind ... ') - self._check_room_contents({'stone': 2, 'flint': 2, 'fire': 2}, check_test_tags=True) + self._check_room_contents({'steel': 2, 'flint': 2, 'fire': 2}, check_test_tags=True) self.room1.msg_contents = Mock() # solve all - self._use('1-stone, 1-flint', 'You are a Genius') + self._use('1-steel, 1-flint', 'You are a Genius') self.room1.msg_contents.assert_called_once_with('|cChar|n performs some kind of tribal dance and |yfire|n seems to appear from thin air', exclude=(self.char1,)) - self._use('stone, flint', 'You are a Genius') - self._check_room_contents({'stone': 0, 'flint': 0, 'fire': 4}, check_test_tags=True) + self._use('steel, flint', 'You are a Genius') + self._check_room_contents({'steel': 0, 'flint': 0, 'fire': 4}, check_test_tags=True) def test_puzzleedit(self): - recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) + recipe_dbref = self._good_recipe('makefire', ['steel', 'flint'], ['fire'] , and_destroy_it=False) def _puzzleedit(swt, dbref, args, expmsg): if (swt is None) and (dbref is None) and (args is None): @@ -2004,7 +2004,7 @@ class TestPuzzles(CommandTest): ) # delete proto parts and proto results - self.stone.delete() + self.steel.delete() self.flint.delete() self.fire.delete() @@ -2019,9 +2019,9 @@ class TestPuzzles(CommandTest): _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) - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) self.room1.msg_contents = Mock() - self._use('stone, flint', 'Yes!') + self._use('steel, flint', 'Yes!') self.room1.msg_contents.assert_called_once_with('fire Yeah baby! Char', exclude=(self.char1,)) self.room1.msg_contents.reset_mock() @@ -2029,13 +2029,13 @@ class TestPuzzles(CommandTest): _puzzleedit('', recipe_dbref, '/mask = location,desc', "makefire(%s) mask = ('location', 'desc')" % recipe_dbref) - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._arm(recipe_dbref, 'makefire', ['steel', '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('steel').db.desc = 'A solid bar of steel' + self.char1.search('steel').location = self.char1 + self.char1.search('flint').db.desc = 'A flint steel' self.char1.search('flint').location = self.char1 - self._use('stone, flint', 'Yes!') + self._use('steel, flint', 'Yes!') self.room1.msg_contents.assert_called_once_with('fire Yeah baby! Char', exclude=(self.char1,)) # delete @@ -2043,7 +2043,7 @@ class TestPuzzles(CommandTest): self._assert_no_recipes() def test_puzzleedit_add_remove_parts_results(self): - recipe_dbref = self._good_recipe('makefire', ['stone', 'flint'], ['fire'] , and_destroy_it=False) + recipe_dbref = self._good_recipe('makefire', ['steel', 'flint'], ['fire'] , and_destroy_it=False) def _puzzleedit(swt, dbref, rhslist, expmsg): cmdstr = '%s %s = %s' % (swt, dbref, ', '.join(rhslist)) @@ -2054,15 +2054,15 @@ class TestPuzzles(CommandTest): caller=self.char1 ) - red_stone = create_object( + red_steel = create_object( self.object_typeclass, - key='red stone', location=self.char1.location) + key='red steel', location=self.char1.location) smoke = create_object( self.object_typeclass, key='smoke', location=self.char1.location) _puzzleedit('/addresult', recipe_dbref, ['smoke'], 'smoke were added to results') - _puzzleedit('/addpart', recipe_dbref, ['red stone', 'stone'], 'red stone, stone were added to parts') + _puzzleedit('/addpart', recipe_dbref, ['red steel', 'steel'], 'red steel, steel were added to parts') # create a box so we can put all objects in # so that they can't be found during puzzle resolution @@ -2076,14 +2076,14 @@ class TestPuzzles(CommandTest): o.location = self.box _box_all() - self._arm(recipe_dbref, 'makefire', ['stone', 'flint', 'red stone', 'stone']) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint', 'red steel', 'steel']) self._check_room_contents({ - 'stone': 2, - 'red stone': 1, + 'steel': 2, + 'red steel': 1, 'flint': 1, }) - self._use('1-stone, flint', 'You try to utilize these but nothing happens ... something amiss?') - self._use('1-stone, flint, red stone, 3-stone', 'You are a Genius') + self._use('1-steel, flint', 'You try to utilize these but nothing happens ... something amiss?') + self._use('1-steel, flint, red steel, 3-steel', 'You are a Genius') self._check_room_contents({ 'smoke': 1, 'fire': 1 @@ -2091,19 +2091,19 @@ class TestPuzzles(CommandTest): _box_all() self.fire.location = self.room1 - self.stone.location = self.room1 + self.steel.location = self.room1 _puzzleedit('/delresult', recipe_dbref, ['fire'], 'fire were removed from results') - _puzzleedit('/delpart', recipe_dbref, ['stone', 'stone'], 'stone, stone were removed from parts') + _puzzleedit('/delpart', recipe_dbref, ['steel', 'steel'], 'steel, steel were removed from parts') _box_all() - self._arm(recipe_dbref, 'makefire', ['flint', 'red stone']) + self._arm(recipe_dbref, 'makefire', ['flint', 'red steel']) self._check_room_contents({ - 'red stone': 1, + 'red steel': 1, 'flint': 1, }) - self._use('red stone, flint', 'You are a Genius') + self._use('red steel, flint', 'You are a Genius') self._check_room_contents({ 'smoke': 1, 'fire': 0 @@ -2126,7 +2126,7 @@ class TestPuzzles(CommandTest): ) recipe_dbref = self._good_recipe( - 'makefire', ['stone', 'flint'], ['fire'], + 'makefire', ['steel', 'flint'], ['fire'], and_destroy_it=False) msg = self.call( @@ -2143,11 +2143,11 @@ class TestPuzzles(CommandTest): r"^Success Location message:$", r"^Mask:$", r"^Parts$", - r"^.*key: stone$", + r"^.*key: steel$", r"^.*key: flint$", r"^Results$", r"^.*key: fire$", - r"^.*key: stone$", + r"^.*key: steel$", r"^.*key: flint$", r"^-+$", r"^Found 1 puzzle\(s\)\.$", @@ -2172,7 +2172,7 @@ class TestPuzzles(CommandTest): re.MULTILINE | re.DOTALL ) - self._arm(recipe_dbref, 'makefire', ['stone', 'flint']) + self._arm(recipe_dbref, 'makefire', ['steel', 'flint']) msg = self.call( puzzles.CmdListArmedPuzzles(), @@ -2184,7 +2184,7 @@ class TestPuzzles(CommandTest): [ r"^-+$", r"^Puzzle name: makefire$", - r"^.*stone.* at \s+ Room.*$", + r"^.*steel.* at \s+ Room.*$", r"^.*flint.* at \s+ Room.*$", r"^Found 1 armed puzzle\(s\)\.$", r"^-+$", From 9d5c84f3d6a1c503e66fa3f6fa0bbb9b7dab6137 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Sun, 30 Dec 2018 21:36:42 -0600 Subject: [PATCH 35/37] Cleanup --- evennia/contrib/puzzles.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 4653ec195f..5c9f0b96a1 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -757,8 +757,6 @@ class CmdListArmedPuzzles(MuxCommand): class PuzzleSystemCmdSet(CmdSet): """ CmdSet to create, arm and resolve Puzzles - - Add with @py self.cmdset.add("evennia.contrib.puzzles.PuzzlesCmdSet") """ def at_cmdset_creation(self): From 54e68f99c48d7cbddc6915c4436aae1775ef92e7 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 1 Jan 2019 23:05:41 -0600 Subject: [PATCH 36/37] Refactor use command to abstract puzzle matching functionality into unit-testable functions. More tests --- evennia/contrib/puzzles.py | 117 +++++++++-------- evennia/contrib/tests.py | 251 +++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 49 deletions(-) diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 5c9f0b96a1..56f157abe5 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -510,6 +510,69 @@ class CmdArmPuzzle(MuxCommand): caller.msg("Puzzle armed |gsuccessfully|n.") +def _lookups_parts_puzzlenames_protodefs(parts): + # Create lookup dicts by part's dbref and by puzzle_name(tags) + parts_dict = dict() + puzzlename_tags_dict = dict() + puzzle_ingredients = dict() + for part in parts: + parts_dict[part.dbref] = part + protodef = proto_def(part, with_tags=False) + # remove 'prototype_key' as it will prevent equality + del(protodef['prototype_key']) + puzzle_ingredients[part.dbref] = protodef + tags_categories = part.tags.all(return_key_and_category=True) + for tag, category in tags_categories: + if category != _PUZZLES_TAG_CATEGORY: + continue + if tag not in puzzlename_tags_dict: + puzzlename_tags_dict[tag] = [] + puzzlename_tags_dict[tag].append(part.dbref) + return parts_dict, puzzlename_tags_dict, puzzle_ingredients + + +def _puzzles_by_names(names): + # Find all puzzles by puzzle name (i.e. tag name) + puzzles = [] + for puzzle_name in names: + _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: + puzzles.extend(_puzzles) + return puzzles + +def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients): + # Check if parts can be combined to solve a puzzle + 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 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) + else: + if len(puzzle_protoparts) == 0: + matched_puzzles[puzzle.dbref] = matched_dbrefparts + return matched_puzzles + + class CmdUsePuzzleParts(MuxCommand): """ Searches for all puzzles whose parts @@ -559,63 +622,19 @@ class CmdUsePuzzleParts(MuxCommand): parts.append(part) # Create lookup dicts by part's dbref and by puzzle_name(tags) - parts_dict = dict() - puzzlename_tags_dict = dict() - puzzle_ingredients = dict() - for part in parts: - parts_dict[part.dbref] = part - protodef = proto_def(part, with_tags=False) - # remove 'prototype_key' as it will prevent equality - del(protodef['prototype_key']) - puzzle_ingredients[part.dbref] = protodef - tags_categories = part.tags.all(return_key_and_category=True) - for tag, category in tags_categories: - if category != _PUZZLES_TAG_CATEGORY: - continue - if tag not in puzzlename_tags_dict: - puzzlename_tags_dict[tag] = [] - puzzlename_tags_dict[tag].append(part.dbref) + parts_dict, puzzlename_tags_dict, puzzle_ingredients = \ + _lookups_parts_puzzlenames_protodefs(parts) # Find all puzzles by puzzle name (i.e. tag name) - puzzles = [] - for puzzle_name, parts in puzzlename_tags_dict.items(): - _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: - puzzles.extend(_puzzles) + puzzles = _puzzles_by_names(puzzlename_tags_dict.keys()) logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles])) # Create lookup dict of puzzles by dbref 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: - puzzle_protoparts = list(puzzle.db.parts[:]) - puzzle_mask = puzzle.db.mask[:] - # remove tags and prototype_key as they prevent equality - 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) - else: - if len(puzzle_protoparts) == 0: - matched_puzzles[puzzle.dbref] = matched_dbrefparts + matched_puzzles = _matching_puzzles( + puzzles, puzzlename_tags_dict, puzzle_ingredients) if len(matched_puzzles) == 0: # TODO: we could use part.fail_message instead, if there was one diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 931743428e..d67512c5cb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2307,6 +2307,257 @@ class TestPuzzles(CommandTest): expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)} self._check_room_contents(expected) + def test_e2e_accumulative(self): + flashlight = create_object( + self.object_typeclass, + key='flashlight', location=self.char1.location) + flashlight_w_1 = create_object( + self.object_typeclass, + key='flashlight-w-1', location=self.char1.location) + flashlight_w_2 = create_object( + self.object_typeclass, + key='flashlight-w-2', location=self.char1.location) + flashlight_w_3 = create_object( + self.object_typeclass, + key='flashlight-w-3', + location=self.char1.location) + battery = create_object( + self.object_typeclass, + key='battery', location=self.char1.location) + + battery.tags.add('flashlight-1', category=puzzles._PUZZLES_TAG_CATEGORY) + battery.tags.add('flashlight-2', category=puzzles._PUZZLES_TAG_CATEGORY) + battery.tags.add('flashlight-3', category=puzzles._PUZZLES_TAG_CATEGORY) + + # TODO: instead of tagging each flashlight, + # arm and resolve each puzzle in order so they all + # are tagged correctly + # it will be necessary to add/remove parts/results because + # each battery is supposed to be consumed during resolution + # as the new flashlight has one more battery than before + flashlight_w_1.tags.add('flashlight-2', category=puzzles._PUZZLES_TAG_CATEGORY) + flashlight_w_2.tags.add('flashlight-3', category=puzzles._PUZZLES_TAG_CATEGORY) + + recipe_fl1_dbref = self._good_recipe('flashlight-1', ['flashlight', 'battery'], ['flashlight-w-1'] , and_destroy_it=False, + expected_count=1) + recipe_fl2_dbref = self._good_recipe('flashlight-2', ['flashlight-w-1', 'battery'], ['flashlight-w-2'] , and_destroy_it=False, + expected_count=2) + recipe_fl3_dbref = self._good_recipe('flashlight-3', ['flashlight-w-2', 'battery'], ['flashlight-w-3'] , and_destroy_it=False, + expected_count=3) + + # delete protoparts + for obj in [battery, flashlight, flashlight_w_1, + flashlight_w_2, flashlight_w_3]: + obj.delete() + + def _group_parts(parts, excluding=set()): + group = dict() + dbrefs = dict() + for o in self.room1.contents: + if o.key in parts and o.dbref not in excluding: + if o.key not in group: + group[o.key] = [] + group[o.key].append(o.dbref) + dbrefs[o.dbref] = o + return group, dbrefs + + # arm each puzzle and group its parts + self._arm(recipe_fl1_dbref, 'flashlight-1', ['battery', 'flashlight']) + fl1_parts, fl1_dbrefs = _group_parts(['battery', 'flashlight']) + self._arm(recipe_fl2_dbref, 'flashlight-2', ['battery', 'flashlight-w-1']) + fl2_parts, fl2_dbrefs = _group_parts(['battery', 'flashlight-w-1'], excluding=fl1_dbrefs.keys()) + self._arm(recipe_fl3_dbref, 'flashlight-3', ['battery', 'flashlight-w-2']) + fl3_parts, fl3_dbrefs = _group_parts(['battery', 'flashlight-w-2'], excluding=set(fl1_dbrefs.keys() + fl2_dbrefs.keys())) + + self._check_room_contents({ + 'battery': 3, + 'flashlight': 1, + 'flashlight-w-1': 1, + 'flashlight-w-2': 1, + 'flashlight-w-3': 0 + }) + + # all batteries have identical protodefs + battery_1 = fl1_dbrefs[fl1_parts['battery'][0]] + battery_2 = fl2_dbrefs[fl2_parts['battery'][0]] + battery_3 = fl3_dbrefs[fl3_parts['battery'][0]] + protodef_battery_1 = puzzles.proto_def(battery_1, with_tags=False) + del(protodef_battery_1['prototype_key']) + protodef_battery_2 = puzzles.proto_def(battery_2, with_tags=False) + del(protodef_battery_2['prototype_key']) + protodef_battery_3 = puzzles.proto_def(battery_3, with_tags=False) + del(protodef_battery_3['prototype_key']) + assert protodef_battery_1 == protodef_battery_2 == protodef_battery_3 + + # each battery can be used in every other puzzle + + b1_parts_dict, b1_puzzlenames, b1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([battery_1]) + _puzzles = puzzles._puzzles_by_names(b1_puzzlenames.keys()) + assert set(['flashlight-1', 'flashlight-2', 'flashlight-3']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b1_puzzlenames, b1_protodefs) + assert 0 == len(matched_puzzles) + + b2_parts_dict, b2_puzzlenames, b2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([battery_2]) + _puzzles = puzzles._puzzles_by_names(b2_puzzlenames.keys()) + assert set(['flashlight-1', 'flashlight-2', 'flashlight-3']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b2_puzzlenames, b2_protodefs) + assert 0 == len(matched_puzzles) + b3_parts_dict, b3_puzzlenames, b3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([battery_3]) + _puzzles = puzzles._puzzles_by_names(b3_puzzlenames.keys()) + assert set(['flashlight-1', 'flashlight-2', 'flashlight-3']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, b3_puzzlenames, b3_protodefs) + assert 0 == len(matched_puzzles) + + assert battery_1 == b1_parts_dict.values()[0] + assert battery_2 == b2_parts_dict.values()[0] + assert battery_3 == b3_parts_dict.values()[0] + assert b1_puzzlenames.keys() == b2_puzzlenames.keys() == b3_puzzlenames.keys() + for puzzle_name in ['flashlight-1', 'flashlight-2', 'flashlight-3']: + assert puzzle_name in b1_puzzlenames + assert puzzle_name in b2_puzzlenames + assert puzzle_name in b3_puzzlenames + assert b1_protodefs.values()[0] == b2_protodefs.values()[0] == b3_protodefs.values()[0] \ + == protodef_battery_1 == protodef_battery_2 == protodef_battery_3 + + # all flashlights have similar protodefs except their key + flashlight_1 = fl1_dbrefs[fl1_parts['flashlight'][0]] + flashlight_2 = fl2_dbrefs[fl2_parts['flashlight-w-1'][0]] + flashlight_3 = fl3_dbrefs[fl3_parts['flashlight-w-2'][0]] + protodef_flashlight_1 = puzzles.proto_def(flashlight_1, with_tags=False) + del(protodef_flashlight_1['prototype_key']) + assert protodef_flashlight_1['key'] == 'flashlight' + del(protodef_flashlight_1['key']) + protodef_flashlight_2 = puzzles.proto_def(flashlight_2, with_tags=False) + del(protodef_flashlight_2['prototype_key']) + assert protodef_flashlight_2['key'] == 'flashlight-w-1' + del(protodef_flashlight_2['key']) + protodef_flashlight_3 = puzzles.proto_def(flashlight_3, with_tags=False) + del(protodef_flashlight_3['prototype_key']) + assert protodef_flashlight_3['key'] == 'flashlight-w-2' + del(protodef_flashlight_3['key']) + assert protodef_flashlight_1 == protodef_flashlight_2 == protodef_flashlight_3 + + # each flashlight can only be used in its own puzzle + + f1_parts_dict, f1_puzzlenames, f1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([flashlight_1]) + _puzzles = puzzles._puzzles_by_names(f1_puzzlenames.keys()) + assert set(['flashlight-1']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f1_puzzlenames, f1_protodefs) + assert 0 == len(matched_puzzles) + f2_parts_dict, f2_puzzlenames, f2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([flashlight_2]) + _puzzles = puzzles._puzzles_by_names(f2_puzzlenames.keys()) + assert set(['flashlight-2']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f2_puzzlenames, f2_protodefs) + assert 0 == len(matched_puzzles) + f3_parts_dict, f3_puzzlenames, f3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs([flashlight_3]) + _puzzles = puzzles._puzzles_by_names(f3_puzzlenames.keys()) + assert set(['flashlight-3']) \ + == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, f3_puzzlenames, f3_protodefs) + assert 0 == len(matched_puzzles) + + assert flashlight_1 == f1_parts_dict.values()[0] + assert flashlight_2 == f2_parts_dict.values()[0] + assert flashlight_3 == f3_parts_dict.values()[0] + for puzzle_name in set(f1_puzzlenames.keys() + f2_puzzlenames.keys() + f3_puzzlenames.keys()): + assert puzzle_name in ['flashlight-1', 'flashlight-2', 'flashlight-3', 'puzzle_member'] + protodef_flashlight_1['key'] = 'flashlight' + assert f1_protodefs.values()[0] == protodef_flashlight_1 + protodef_flashlight_2['key'] = 'flashlight-w-1' + assert f2_protodefs.values()[0] == protodef_flashlight_2 + protodef_flashlight_3['key'] = 'flashlight-w-2' + assert f3_protodefs.values()[0] == protodef_flashlight_3 + + # each battery can be matched with every other flashlight + # to potentially resolve each puzzle + for batt in [battery_1, battery_2, battery_3]: + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs([batt, flashlight_1]) + assert set([batt.dbref, flashlight_1.dbref]) == set(puzzlenames['flashlight-1']) + assert set([batt.dbref]) == set(puzzlenames['flashlight-2']) + assert set([batt.dbref]) == set(puzzlenames['flashlight-3']) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(['flashlight-1', 'flashlight-2', 'flashlight-3']) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs([batt, flashlight_2]) + assert set([batt.dbref]) == set(puzzlenames['flashlight-1']) + assert set([batt.dbref, flashlight_2.dbref]) == set(puzzlenames['flashlight-2']) + assert set([batt.dbref]) == set(puzzlenames['flashlight-3']) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(['flashlight-1','flashlight-2', 'flashlight-3']) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs([batt, flashlight_3]) + assert set([batt.dbref]) == set(puzzlenames['flashlight-1']) + assert set([batt.dbref]) == set(puzzlenames['flashlight-2']) + assert set([batt.dbref, flashlight_3.dbref]) == set(puzzlenames['flashlight-3']) + _puzzles = puzzles._puzzles_by_names(puzzlenames.keys()) + assert set(['flashlight-1','flashlight-2', 'flashlight-3']) == set([p.db.puzzle_name for p in _puzzles]) + matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs) + assert 1 == len(matched_puzzles) + + # delete all parts + for part in fl1_dbrefs.values() + fl2_dbrefs.values() + fl3_dbrefs.values(): + part.delete() + + self._check_room_contents({ + 'battery': 0, + 'flashlight': 0, + 'flashlight-w-1': 0, + 'flashlight-w-2': 0, + 'flashlight-w-3': 0 + }) + + # arm first puzzle 3 times and group its parts so we can solve + # all puzzles with the parts from the 1st armed + for i in range(3): + self._arm(recipe_fl1_dbref, 'flashlight-1', ['battery', 'flashlight']) + fl1_parts, fl1_dbrefs = _group_parts(['battery', 'flashlight']) + + # delete the 2 extra flashlights so we can start solving + for flashlight_dbref in fl1_parts['flashlight'][1:]: + fl1_dbrefs[flashlight_dbref].delete() + + self._check_room_contents({ + 'battery': 3, + 'flashlight': 1, + 'flashlight-w-1': 0, + 'flashlight-w-2': 0, + 'flashlight-w-3': 0 + }) + + self._use('1-battery, flashlight', 'You are a Genius') + self._check_room_contents({ + 'battery': 2, + 'flashlight': 0, + 'flashlight-w-1': 1, + 'flashlight-w-2': 0, + 'flashlight-w-3': 0 + }) + + self._use('1-battery, flashlight-w-1', 'You are a Genius') + self._check_room_contents({ + 'battery': 1, + 'flashlight': 0, + 'flashlight-w-1': 0, + 'flashlight-w-2': 1, + 'flashlight-w-3': 0 + }) + + self._use('battery, flashlight-w-2', 'You are a Genius') + self._check_room_contents({ + 'battery': 0, + 'flashlight': 0, + 'flashlight-w-1': 0, + 'flashlight-w-2': 0, + 'flashlight-w-3': 1 + }) + def test_e2e_interchangeable_parts_and_results(self): # Parts and Results can be used in multiple puzzles egg = create_object( From 74279720b608626a5651b9e701e1d0de410b41f6 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Thu, 3 Jan 2019 20:07:27 -0600 Subject: [PATCH 37/37] Remove FIXME:. Scripts names are case-insensitive --- evennia/contrib/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index d67512c5cb..6e3f689985 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -2288,7 +2288,7 @@ class TestPuzzles(CommandTest): parts = ['Balloon'] results = ['Balloon'] recipe_dbref = self._good_recipe( - 'boom!!!', # FIXME: uppercase name fails + 'boom!!!', parts, results, and_destroy_it=False, expected_count=3