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
This commit is contained in:
Henddher Pedroza 2018-09-16 17:25:57 -05:00
parent 4f5c2f51ad
commit 1bdd7ce174
2 changed files with 147 additions and 47 deletions

View file

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

View file

@ -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,
})