Merge branch 'puzzles' of https://github.com/Henddher/evennia into Henddher-puzzles

This commit is contained in:
Griatch 2019-01-17 20:45:16 +01:00
commit ef0832707d
2 changed files with 1745 additions and 1 deletions

789
evennia/contrib/puzzles.py Normal file
View file

@ -0,0 +1,789 @@
"""
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
@create/drop fruit smoothie
@puzzle smoothie, orange, mango, yogurt, blender = fruit smoothie
...
Puzzle smoothie(#1234) created successfuly.
@destroy/force orange, mango, yogurt, blender, fruit smoothie
@armpuzzle #1234
Part orange is spawned at ...
Part mango is spawned at ...
....
Puzzle smoothie(#1234) has been armed successfully
As Player:
use orange, mango, yogurt, blender
...
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 and results. These prototypes become the
puzzle recipe. (See PuzzleRecipe 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 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 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 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
# Tag used by puzzles
_PUZZLES_TAG_CATEGORY = 'puzzles'
_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!!!'
_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 ------------
def proto_def(obj, with_tags=True):
"""
Basic properties needed to spawn
and compare recipe with candidate part
"""
protodef = {
# 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,
'desc': obj.db.desc,
'location': obj.location,
'home': obj.home,
'locks': ';'.join(obj.locks.all()),
'permissions': obj.permissions.all()[:],
}
if with_tags:
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))
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
_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)
# ------------------------------------------
class PuzzleRecipe(DefaultScript):
"""
Definition of a Puzzle Recipe
"""
def save_recipe(self, puzzle_name, parts, results):
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
class CmdCreatePuzzleRecipe(MuxCommand):
"""
Creates a puzzle recipe.
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[/arm] name,<part1[,part2,...>] = <result1[,result2,...]>
"""
key = '@puzzle'
aliases = '@puzzlerecipe'
locks = 'cmd:perm(puzzle) or perm(Builder)'
help_category = 'Puzzles'
confirm = True
default_confirm = 'no'
def func(self):
caller = self.caller
if len(self.lhslist) < 2 \
or not self.rhs:
string = "Usage: @puzzle name,<part1[,...]> = <result1[,...]>"
caller.msg(string)
return
puzzle_name = self.lhslist[0]
if len(puzzle_name) == 0:
caller.msg('Invalid puzzle name %r.' % puzzle_name)
return
# 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
# 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
# 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
if not inherits_from(obj.location, DefaultRoom):
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)
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(obj):
return
parts.append(obj)
results = []
for objname in self.rhslist:
obj = caller.search(objname)
if not obj:
return
if not is_valid_result(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_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."
% (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'
'Remember to add a "success message" via:\n'
' @puzzleedit #dbref/use_success_message = <Your custom success message>\n'
'You are now able to arm this puzzle using Builder command:\n'
' @armpuzzle <puzzle #dbref>\n'
)
class CmdEditPuzzle(MuxCommand):
"""
Edits puzzle properties
Usage:
@puzzleedit[/delete] <#dbref>
@puzzleedit <#dbref>/use_success_message = <Your custom message>
@puzzleedit <#dbref>/use_success_location_message = <Your custom message from {caller} producing {result_names}>
@puzzleedit <#dbref>/mask = attr1[,attr2,...]>
@puzzleedit[/addpart] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/delpart] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/addresult] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/delresult] <#dbref> = <obj[,obj2,...]>
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
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.
"""
key = '@puzzleedit'
locks = 'cmd:perm(puzzleedit) or perm(Builder)'
help_category = 'Puzzles'
def func(self):
self._USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"
caller = self.caller
if not self.lhslist:
caller.msg(self._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" + self._USAGE)
return
puzzle = search.search_script(recipe_dbref)
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
caller.msg('%s(%s) is not a puzzle' % (puzzle[0].name, 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
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
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
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
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:
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):
"""
Arms a puzzle by spawning all its parts
"""
key = '@armpuzzle'
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 = 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)))
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.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
part.db.puzzle_name = puzzle.db.puzzle_name
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
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 <part1[,part2,...>]
"""
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):
# 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 dicts by part's dbref and by puzzle_name(tags)
parts_dict, puzzlename_tags_dict, puzzle_ingredients = \
_lookups_parts_puzzlenames_protodefs(parts)
# Find all puzzles by puzzle name (i.e. tag name)
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 = _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
# random part falls and lands on your feet
# random part hits you square on the face
caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many))
return
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, 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:
caller.msg(
'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]]
caller.msg("You try %s ..." % (puzzle.db.puzzle_name))
# got one, spawn its results
result_names = []
for proto_result in puzzle.db.results:
result = spawn(proto_result)[0]
result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
result.db.puzzle_name = puzzle.db.puzzle_name
result_names.append(result.name)
# Destroy all parts used
for dbref in matched_dbrefparts:
parts_dict[dbref].delete()
result_names = ', '.join(result_names)
caller.msg(puzzle.db.use_success_message)
caller.location.msg_contents(
puzzle.db.use_success_location_message.format(
caller=caller, result_names=result_names),
exclude=(caller,)
)
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
recipes = search.search_script_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('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 = '-'
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 = ''
else:
text.append(div)
text.append('Found |r%d|n puzzle(s).' % (len(recipes)))
text.append(div)
caller.msg('\n'.join(text))
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
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))
else:
text.append(div)
text.append('Found |r%d|n armed puzzle(s).' % (len(armed_puzzles)))
text.append(div)
caller.msg('\n'.join(text))
class PuzzleSystemCmdSet(CmdSet):
"""
CmdSet to create, arm and resolve Puzzles
"""
def at_cmdset_creation(self):
super(PuzzleSystemCmdSet, self).at_cmdset_creation()
self.add(CmdCreatePuzzleRecipe())
self.add(CmdEditPuzzle())
self.add(CmdArmPuzzle())
self.add(CmdListPuzzleRecipes())
self.add(CmdListArmedPuzzles())
self.add(CmdUsePuzzleParts())

View file

@ -1583,7 +1583,7 @@ class TestFieldFillFunc(EvenniaTest):
def test_field_functions(self):
self.assertTrue(fieldfill.form_template_to_dict(FIELD_TEST_TEMPLATE) == FIELD_TEST_DATA)
# Test of the unixcommand module
from evennia.contrib.unixcommand import UnixCommand
@ -1717,6 +1717,961 @@ class TestRandomStringGenerator(EvenniaTest):
SIMPLE_GENERATOR.get()
# 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):
def setUp(self):
super(TestPuzzles, self).setUp()
self.steel = create_object(
self.object_typeclass,
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.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')
self.fire.tags.add('tag-fire', category='tagcat')
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, 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(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[expected_count-1].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY)
)
recipe_dbref = recipes[expected_count-1].dbref
if and_destroy_it:
recipes[expected_count-1].delete()
return recipe_dbref if not and_destroy_it else None
def _assert_no_recipes(self):
self.assertEqual(
0,
len(search.search_script_tag('', category=puzzles._PUZZLES_TAG_CATEGORY))
)
# good recipes
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))
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
)
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, 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):
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))
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 = [
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
)
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')
# FIXME: testing nothing, this is just to bump up coverage
def test_cmd_puzzle(self):
self._assert_no_recipes()
# bad syntax
def _bad_syntax(cmdstr):
self.call(
puzzles.CmdCreatePuzzleRecipe(),
cmdstr,
'Usage: @puzzle name,<part1[,...]> = <result1[,...]>',
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 =')
self._assert_no_recipes()
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):
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', ['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$")
self._assert_no_recipes()
def test_cmd_armpuzzle(self):
# 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', ['steel', 'flint'], ['fire', 'steel', 'flint'], and_destroy_it=False)
# delete proto parts and proto result
self.steel.delete()
self.flint.delete()
self.fire.delete()
# good arm
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(
puzzles.CmdUsePuzzleParts(),
cmdstr,
expmsg,
caller=self.char1
)
return msg
def test_cmd_use(self):
self._use('', 'Use what?')
self._use('something', 'There is no something around.')
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'), ['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 steel and flint
# those aren't valid puzzle parts because
# the puzzle hasn't been armed
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('steel', 'Which steel. There are many')
self._use('flint', 'Which flint. There are many')
# delete proto parts and proto results
self.steel.delete()
self.flint.delete()
self.fire.delete()
# solve puzzle
self._use('steel, flint', 'You are a Genius')
self.assertEqual(1,
len(list(filter(
lambda o: o.key == 'fire' \
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({'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('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', ['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-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', ['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-steel, 2-flint, 3-steel, 3-flint',
'Your gears start turning and 2 different ideas come to your mind ... ')
self._check_room_contents({'steel': 2, 'flint': 2, 'fire': 2}, check_test_tags=True)
self.room1.msg_contents = Mock()
# solve all
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('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', ['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):
cmdstr = ''
else:
cmdstr = '%s %s%s' % (swt, dbref, args)
self.call(
puzzles.CmdEditPuzzle(),
cmdstr,
expmsg,
caller=self.char1
)
# delete proto parts and proto results
self.steel.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")
_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')
# 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', ['steel', 'flint'])
self.room1.msg_contents = Mock()
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()
# 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', ['steel', 'flint'])
# change location and desc
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('steel, 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)
self._assert_no_recipes()
def test_puzzleedit_add_remove_parts_results(self):
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))
self.call(
puzzles.CmdEditPuzzle(),
cmdstr,
expmsg,
caller=self.char1
)
red_steel = create_object(
self.object_typeclass,
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 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
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,
self.obj1, self.obj2, self.box]:
o.location = self.box
_box_all()
self._arm(recipe_dbref, 'makefire', ['steel', 'flint', 'red steel', 'steel'])
self._check_room_contents({
'steel': 2,
'red steel': 1,
'flint': 1,
})
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
})
_box_all()
self.fire.location = self.room1
self.steel.location = self.room1
_puzzleedit('/delresult', recipe_dbref, ['fire'], 'fire were removed from results')
_puzzleedit('/delpart', recipe_dbref, ['steel', 'steel'], 'steel, steel were removed from parts')
_box_all()
self._arm(recipe_dbref, 'makefire', ['flint', 'red steel'])
self._check_room_contents({
'red steel': 1,
'flint': 1,
})
self._use('red steel, flint', 'You are a Genius')
self._check_room_contents({
'smoke': 1,
'fire': 0
})
def test_lspuzzlerecipes_lsarmedpuzzles(self):
msg = self.call(
puzzles.CmdListPuzzleRecipes(),
'',
caller=self.char1
)
self._assert_msg_matched(
msg,
[
r"^-+$",
r"^Found 0 puzzle\(s\)\.$",
r"-+$",
],
re.MULTILINE | re.DOTALL
)
recipe_dbref = self._good_recipe(
'makefire', ['steel', 'flint'], ['fire'],
and_destroy_it=False)
msg = self.call(
puzzles.CmdListPuzzleRecipes(),
'',
caller=self.char1
)
self._assert_msg_matched(
msg,
[
r"^-+$",
r"^Puzzle 'makefire'.*$",
r"^Success Caller message:$",
r"^Success Location message:$",
r"^Mask:$",
r"^Parts$",
r"^.*key: steel$",
r"^.*key: flint$",
r"^Results$",
r"^.*key: fire$",
r"^.*key: steel$",
r"^.*key: flint$",
r"^-+$",
r"^Found 1 puzzle\(s\)\.$",
r"^-+$",
],
re.MULTILINE | re.DOTALL
)
msg = self.call(
puzzles.CmdListArmedPuzzles(),
'',
caller=self.char1
)
self._assert_msg_matched(
msg,
[
r"^-+$",
r"^-+$",
r"^Found 0 armed puzzle\(s\)\.$",
r"^-+$"
],
re.MULTILINE | re.DOTALL
)
self._arm(recipe_dbref, 'makefire', ['steel', 'flint'])
msg = self.call(
puzzles.CmdListArmedPuzzles(),
'',
caller=self.char1
)
self._assert_msg_matched(
msg,
[
r"^-+$",
r"^Puzzle name: makefire$",
r"^.*steel.* at \s+ Room.*$",
r"^.*flint.* at \s+ Room.*$",
r"^Found 1 armed puzzle\(s\)\.$",
r"^-+$",
],
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(
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(
'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(
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']
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)
# Uppercase puzzle name
balloon = create_object(
self.object_typeclass,
key='Balloon', location=self.char1.location)
parts = ['Balloon']
results = ['Balloon']
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)
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(
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
# 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(
self.object_typeclass,
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,
})
# Tests for the building_menu contrib
from evennia.contrib.building_menu import BuildingMenu, CmdNoInput, CmdNoMatch