Refactor use command to abstract puzzle matching functionality into unit-testable functions. More tests

This commit is contained in:
Henddher Pedroza 2019-01-01 23:05:41 -06:00
parent 9d5c84f3d6
commit 54e68f99c4
2 changed files with 319 additions and 49 deletions

View file

@ -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

View file

@ -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(