From 54e68f99c48d7cbddc6915c4436aae1775ef92e7 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 1 Jan 2019 23:05:41 -0600 Subject: [PATCH] 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(