mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 00:06:30 +01:00
Merge branch 'puzzles' of https://github.com/Henddher/evennia into Henddher-puzzles
This commit is contained in:
commit
ef0832707d
2 changed files with 1745 additions and 1 deletions
789
evennia/contrib/puzzles.py
Normal file
789
evennia/contrib/puzzles.py
Normal 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())
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue