When creating proto parts and results, honor obj.home, obj.permissions, and obj.locks, and obj.tags
623 lines
20 KiB
Python
623 lines
20 KiB
Python
"""
|
|
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 (PuzzlePartObject) 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.utils.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!!!'
|
|
|
|
# ----------- UTILITY FUNCTIONS ------------
|
|
|
|
def proto_def(obj, with_tags=True):
|
|
"""
|
|
Basic properties needed to spawn
|
|
and compare recipe with candidate part
|
|
"""
|
|
protodef = {
|
|
# FIXME: Don't we need to honor ALL properties? attributes, contents, etc.
|
|
'key': obj.key,
|
|
'typeclass': 'evennia.contrib.puzzles.PuzzlePartObject', # FIXME: what if obj is another typeclass
|
|
'desc': obj.db.desc,
|
|
'location': obj.location,
|
|
'home': obj.home,
|
|
'locks': ';'.join(obj.locks.all()),
|
|
'permissions': obj.permissions.all()[:],
|
|
}
|
|
if with_tags:
|
|
tags = obj.tags.all()[:]
|
|
tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY))
|
|
protodef['tags'] = tags
|
|
return protodef
|
|
|
|
# Colorize the default success message
|
|
_i = 0
|
|
_colors = ['|r', '|g', '|y']
|
|
_msg = []
|
|
for l in _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE:
|
|
_msg += _colors[_i] + l
|
|
_i = (_i + 1) % len(_colors)
|
|
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = ''.join(_msg) + '|n'
|
|
|
|
# ------------------------------------------
|
|
|
|
class PuzzlePartObject(DefaultObject):
|
|
"""
|
|
Puzzle Part, typically used by @armpuzzle command
|
|
"""
|
|
|
|
def mark_as_puzzle_member(self, puzzle_name):
|
|
"""
|
|
Marks this object as a member of puzzle named
|
|
'puzzle_name'
|
|
"""
|
|
self.db.puzzle_name = puzzle_name
|
|
self.tags.add(puzzle_name, category=_PUZZLES_TAG_CATEGORY)
|
|
|
|
|
|
class PuzzleRecipe(DefaultScript):
|
|
"""
|
|
Definition of a Puzzle Recipe
|
|
"""
|
|
|
|
def save_recipe(self, puzzle_name, parts, results):
|
|
self.db.puzzle_name = puzzle_name
|
|
self.db.parts = tuple(parts)
|
|
self.db.results = tuple(results)
|
|
self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
|
|
self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE
|
|
|
|
|
|
class CmdCreatePuzzleRecipe(MuxCommand):
|
|
"""
|
|
Creates a puzzle recipe.
|
|
|
|
Each part and result must exist and be placed in their corresponding location.
|
|
All parts and results are left intact. Caller must explicitly
|
|
destroy them.
|
|
|
|
Usage:
|
|
@puzzle name,<part1[,part2,...>] = <result1[,result2,...]>
|
|
"""
|
|
|
|
key = '@puzzle'
|
|
aliases = '@puzzlerecipe'
|
|
locks = 'cmd:perm(puzzle) or perm(Builder)'
|
|
help_category = 'Puzzles'
|
|
|
|
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
|
|
|
|
# TODO: if there is another puzzle with same name
|
|
# warn user that parts and results will be
|
|
# interchangable
|
|
|
|
def is_valid_obj_location(obj):
|
|
valid = True
|
|
# Valid locations are: room, ...
|
|
# TODO: other valid locations must 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
|
|
# TODO: handle contents of a given part
|
|
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)
|
|
|
|
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>
|
|
|
|
Switches:
|
|
delete - deletes the recipe. Existing parts and results aren't modified
|
|
|
|
"""
|
|
|
|
key = '@puzzleedit'
|
|
# FIXME: permissions for scripts?
|
|
locks = 'cmd:perm(puzzleedit) or perm(Builder)'
|
|
help_category = 'Puzzles'
|
|
|
|
def func(self):
|
|
_USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"
|
|
caller = self.caller
|
|
|
|
if not self.lhslist:
|
|
caller.msg(_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" + _USAGE)
|
|
return
|
|
|
|
puzzle = search.search_script(recipe_dbref)
|
|
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
|
|
caller.msg('Invalid puzzle %r' % (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
|
|
|
|
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
|
|
|
|
|
|
class CmdArmPuzzle(MuxCommand):
|
|
"""
|
|
Arms a puzzle by spawning all its parts
|
|
"""
|
|
|
|
key = '@armpuzzle'
|
|
# FIXME: permissions for scripts?
|
|
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.mark_as_puzzle_member(puzzle.db.puzzle_name)
|
|
|
|
caller.msg("Puzzle armed |gsuccessfully|n.")
|
|
|
|
|
|
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,...>]
|
|
"""
|
|
|
|
# TODO: consider allowing builder to provide
|
|
# messages and "hooks" that can be displayed
|
|
# and/or fired whenever the resolver of the puzzle
|
|
# enters the location where a result was spawned
|
|
|
|
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) \
|
|
or not inherits_from(part, PuzzlePartObject):
|
|
|
|
# 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 = dict()
|
|
puzzlename_tags_dict = dict()
|
|
puzzle_ingredients = dict()
|
|
for part in parts:
|
|
parts_dict[part.dbref] = part
|
|
puzzle_ingredients[part.dbref] = proto_def(part, with_tags=False)
|
|
tags_categories = part.tags.all(return_key_and_category=True)
|
|
for tag, category in tags_categories:
|
|
if category != _PUZZLES_TAG_CATEGORY:
|
|
continue
|
|
if tag not in puzzlename_tags_dict:
|
|
puzzlename_tags_dict[tag] = []
|
|
puzzlename_tags_dict[tag].append(part.dbref)
|
|
|
|
# Find all puzzles by puzzle name (i.e. tag name)
|
|
puzzles = []
|
|
for puzzle_name, parts in puzzlename_tags_dict.items():
|
|
_puzzles = search.search_script_attribute(
|
|
key='puzzle_name',
|
|
value=puzzle_name
|
|
)
|
|
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
|
|
if not _puzzles:
|
|
continue
|
|
else:
|
|
puzzles.extend(_puzzles)
|
|
|
|
logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles]))
|
|
|
|
# Create lookup dict of puzzles by dbref
|
|
puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles)
|
|
# Check if parts can be combined to solve a puzzle
|
|
matched_puzzles = dict()
|
|
for puzzle in puzzles:
|
|
puzzle_protoparts = list(puzzle.db.parts[:])
|
|
# remove tags as they prevent equality
|
|
for puzzle_protopart in puzzle_protoparts:
|
|
del(puzzle_protopart['tags'])
|
|
matched_dbrefparts = []
|
|
parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name]
|
|
for part_dbref in parts_dbrefs:
|
|
protopart = puzzle_ingredients[part_dbref]
|
|
if protopart in puzzle_protoparts:
|
|
puzzle_protoparts.remove(protopart)
|
|
matched_dbrefparts.append(part_dbref)
|
|
else:
|
|
if len(puzzle_protoparts) == 0:
|
|
matched_puzzles[puzzle.dbref] = matched_dbrefparts
|
|
|
|
if len(matched_puzzles) == 0:
|
|
# TODO: we could use part.fail_message instead, if any
|
|
# 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, ...
|
|
if len(largest_puzzles) > 1:
|
|
# FIXME: pick a random one or let user choose?
|
|
# FIXME: do we show the puzzle name or something else?
|
|
caller.msg(
|
|
'Your gears start turning and a bunch of ideas come to your mind ...\n%s' % (
|
|
' ...\n'.join([puzzles_dict[lp[0]].db.puzzle_name for lp in 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.mark_as_puzzle_member(puzzle.db.puzzle_name)
|
|
result_names.append(result.name)
|
|
# FIXME: add 'ramdon' messages:
|
|
# Hmmm ... did I search result.location?
|
|
# What was that? ... I heard something in result.location?
|
|
# Eureka! you built a result
|
|
|
|
# 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)
|
|
# TODO: allow custom message for location and channels
|
|
caller.location.msg_contents(
|
|
"|c%s|n performs some kind of tribal dance"
|
|
" and |y%s|n seems to appear from thin air" % (
|
|
caller, 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 message:\n' + recipe.db.use_success_message + '\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 = ''
|
|
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))
|
|
text.append(div)
|
|
caller.msg('\n'.join(text))
|
|
|
|
|
|
class PuzzleSystemCmdSet(CmdSet):
|
|
"""
|
|
CmdSet to create, arm and resolve Puzzles
|
|
|
|
Add with @py self.cmdset.add("evennia.contrib.puzzles.PuzzlesCmdSet")
|
|
"""
|
|
|
|
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())
|