mirror of
https://github.com/evennia/evennia.git
synced 2026-04-03 14:37:17 +02:00
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:
parent
1c87107b9d
commit
b466177fc6
2 changed files with 147 additions and 47 deletions
|
|
@ -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 = '-'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue