Start tutorialmenu

This commit is contained in:
Griatch 2020-09-27 23:54:10 +02:00
parent 236dadf9f1
commit a8ab09e72d
4 changed files with 178 additions and 386 deletions

View file

@ -1,383 +0,0 @@
"""
Game tutor
Evennia contrib - Griatch 2020
This contrib is a system for easily adding a tutor/tutorial for your game
(something that should be considered a necessity for any game ...).
It consists of a single room that will be created for each player/character
wanting to go through the tutorial. The text is presented as a menu of
self-sustained 'lessons' that the user can either jump freely between or step
through wizard-style. In each lesson, the tutor will track progress (for
example the user may be asked to try out a certain command, and the tutor will
not move on until that command has been tried).
::
# node Start
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
## options
1: first option -> node1
2: second option -> node2
3: node3 -> gotonode3()
next;n: node2
top: start
>input: return to go back -> start
>input foo*: foo()
>input bar*: bar()
# node node1
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
...
"""
import sys
import re
from ast import literal_eval
from evennia import EvMenu
from fnmatch import fnmatch
# i18n
from django.utils.translation import gettext as _
_RE_NODE = re.compile(r"#\s*?NODE\s+?(?P<nodename>\S+?)$", re.I + re.M)
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS*?\s*?$", re.I + re.M)
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
_RE_CALLABLE = re.compile(
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?=[\S\s]+?)\)|\(\))", re.I + re.M
)
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
_OPTION_INPUT_MARKER = ">"
_OPTION_ALIAS_MARKER = ";"
_OPTION_SEP_MARKER = ":"
_OPTION_CALL_MARKER = "->"
_OPTION_COMMENT_START = "#"
# Input/option/goto handler functions that allows for dynamically generated
# nodes read from the menu template.
def _generated_goto_func(caller, raw_string, **kwargs):
goto = kwargs["goto"]
goto_callables = kwargs["goto_callables"]
current_nodename = kwargs["current_nodename"]
if _RE_CALLABLE.match(goto):
gotofunc = goto.strip()[:-2]
if gotofunc in goto_callables:
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
if goto is None:
return goto, {"generated_nodename": current_nodename}
caller.msg(_HELP_NO_OPTION_MATCH)
return goto, {"generated_nodename": goto}
def _generated_input_goto_func(caller, raw_string, **kwargs):
gotomap = kwargs["gotomap"]
goto_callables = kwargs["goto_callables"]
current_nodename = kwargs["current_nodename"]
# start with glob patterns
for pattern, goto in gotomap.items():
if fnmatch(raw_string.lower(), pattern):
match = _RE_CALLABLE.match(goto)
print(f"goto {goto} -> match: {match}")
if match:
gotofunc = match.group("funcname")
gotokwargs = match.group("kwargs") or ""
print(f"gotofunc: {gotofunc}, {gotokwargs}")
if gotofunc in goto_callables:
for kwarg in gotokwargs.split(","):
if kwarg and "=" in kwarg:
print(f"kwarg {kwarg}")
key, value = [part.strip() for part in kwarg.split("=", 1)]
try:
key = literal_eval(key)
except ValueError:
pass
try:
value = literal_eval(value)
except ValueError:
pass
kwargs[key] = value
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
if goto is None:
return goto, {"generated_nodename": current_nodename}
return goto, {"generated_nodename": goto}
# no glob pattern match; try regex
for pattern, goto in gotomap.items():
if re.match(pattern, raw_string.lower(), flags=re.I + re.M):
if _RE_CALLABLE.match(goto):
gotofunc = goto.strip()[:-2]
if gotofunc in goto_callables:
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
if goto is None:
return goto, {"generated_nodename": current_nodename}
return goto, {"generated_nodename": goto}
# no match, rerun current node
caller.msg(_HELP_NO_OPTION_MATCH)
return None, {"generated_nodename": current_nodename}
def _generated_node(caller, raw_string, **kwargs):
text, options = caller.db._generated_menu_contents[kwargs["_current_nodename"]]
return text, options
def parse_menu_template(caller, menu_template, goto_callables=None):
"""
Parse menu-template string
Args:
caller (Object or Account): Entity using the menu.
menu_template (str): Menu described using the templating format.
goto_callables (dict, optional): Mapping between call-names and callables
on the form `callable(caller, raw_string, **kwargs)`. These are what is
available to use in the `menu_template` string.
"""
def _parse_options(nodename, optiontxt, goto_callables):
"""
Parse option section into option dict.
"""
options = []
optiontxt = optiontxt[0].strip() if optiontxt else ""
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
inputparsemap = {}
for inum, optline in enumerate(optionlist):
if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
# skip comments or invalid syntax
continue
key = ""
desc = ""
pattern = None
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
# desc -> goto
if _OPTION_CALL_MARKER in goto:
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
# parse key [;aliases|pattern]
key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)]
if not key:
# fall back to this being the Nth option
key = [f"{inum + 1}"]
main_key = key[0]
if main_key.startswith(_OPTION_INPUT_MARKER):
# if we have a pattern, build the arguments for _default later
pattern = main_key[len(_OPTION_INPUT_MARKER) :].strip()
inputparsemap[pattern] = goto
print(f"registering input goto {pattern} -> {goto}")
else:
# a regular goto string/callable target
option = {
"key": key,
"goto": (
_generated_goto_func,
{
"goto": goto,
"current_nodename": nodename,
"goto_callables": goto_callables,
},
),
}
if desc:
option["desc"] = desc
options.append(option)
if inputparsemap:
# if this exists we must create a _default entry too
options.append(
{
"key": "_default",
"goto": (
_generated_input_goto_func,
{
"gotomap": inputparsemap,
"current_nodename": nodename,
"goto_callables": goto_callables,
},
),
}
)
return options
def _parse(caller, menu_template, goto_callables):
"""
Parse the menu string format into a node tree.
"""
nodetree = {}
splits = _RE_NODE.split(menu_template)
splits = splits[1:] if splits else []
# from evennia import set_trace;set_trace(term_size=(140,120))
content_map = {}
for node_ind in range(0, len(splits), 2):
nodename, nodetxt = splits[node_ind], splits[node_ind + 1]
text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2)
options = _parse_options(nodename, optiontxt, goto_callables)
content_map[nodename] = (text, options)
nodetree[nodename] = _generated_node
caller.db._generated_menu_contents = content_map
return nodetree
return _parse(caller, menu_template, goto_callables)
def template2menu(
caller,
menu_template,
goto_callables=None,
startnode="start",
persistent=False,
**kwargs,
):
"""
Helper function to generate and start an EvMenu based on a menu template
string.
Args:
caller (Object or Account): The entity using the menu.
menu_template (str): The menu-template string describing the content
and structure of the menu. It can also be the python-path to, or a module
containing a `MENU_TEMPLATE` global variable with the template.
goto_callables (dict, optional): Mapping of callable-names to
module-global objects to reference by name in the menu-template.
Must be on the form `callable(caller, raw_string, **kwargs)`.
startnode (str, optional): The name of the startnode, if not 'start'.
persistent (bool, optional): If the generated menu should be persistent.
**kwargs: All kwargs will be passed into EvMenu.
"""
goto_callables = goto_callables or {}
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
EvMenu(
caller,
menu_tree,
persistent=persistent,
**kwargs,
)
def gotonode3(caller, raw_string, **kwargs):
print("in gotonode3", caller, raw_string, kwargs)
return None
def foo(caller, raw_string, **kwargs):
print("in foo", caller, raw_string, kwargs)
return "node2"
def bar(caller, raw_string, **kwargs):
print("in bar", caller, raw_string, kwargs)
return "bar"
def customcall(caller, raw_string, **kwargs):
return "start"
def customnode(caller, raw_string, **kwargs):
text = "This is a custom node!"
options = {
"desc": "Go back",
"goto": customcall
}
return text, options
def test_generator(caller):
MENU_TEMPLATE = """
# node start
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
## options
1: first option -> node1
2: second option -> node2
3: node3 -> gotonode3()
next;n: node2
top: start
> foo*: foo()
> bar*: bar(a=4, boo=groo)
> [5,6]0+?: foo()
> great: node2
> fail: bar()
# node node1
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
## options
back: start
to node 2: node2
run foo (rerun node): foo()
customnode: Go to custom node -> customnode
>: return to go back -> start
# node node2
In node 2!
## options
back: back to start -> start
# node bar
In node bar!
## options
back: back to start -> start
end: end
# node end
In node end!
"""
callables = {"gotonode3": gotonode3, "foo": foo, "bar": bar}
dct = parse_menu_template(caller, MENU_TEMPLATE, callables)
dct["customnode"] = customnode
EvMenu(caller, dct)
# template2menu(caller, MENU_TEMPLATE, callables)
if __name__ == "__main__":
test_generator("<GriatchCaller>")

View file

@ -0,0 +1,174 @@
"""
Game tutor
Evennia contrib - Griatch 2020
This contrib is a tutorial menu using the EvMenu menu-templating system.
"""
from evennia.utils.evmenu import parse_menu_template, EvMenu
# goto callables
def command_passthrough(caller, raw_string, **kwargs):
cmd = kwargs.get("cmd")
on_success = kwargs.get('on_success')
if cmd:
caller.execute_cmd(cmd)
else:
caller.execute_cmd(raw_string)
return on_success
def do_nothing(caller, raw_string, **kwargs):
return None
def send_testing_tagged(caller, raw_string, **kwargs):
caller.msg(("This is a message tagged with 'testing' and "
"should appear in the pane you selected!\n "
f"You wrote: '{raw_string}'", {"type": "testing"}))
return None
def send_string(caller, raw_string, **kwargs):
caller.msg(raw_string)
return None
MENU_TEMPLATE = """
## NODE start
Welcome to |cEvennia|n! From this menu you can learn some more about the system and
also the basics of how to play a text-based game. You can exit this menu at
any time by using "q" or "quit".
Select an option you want to learn more about below.
## OPTIONS
1: About evennia -> about_evennia
2: What is a MUD/MU*? -> about_muds
3: Using the webclient -> using webclient
4: Command input -> command_input
# ---------------------------------------------------------------------------------
## NODE about_evennia
Evennia is a game engine for creating multiplayer online text-games.
## OPTIONS
back: start
next: about MUDs -> about_muds
>: about_muds
# ---------------------------------------------------------------------------------
## NODE about_muds
The term MUD stands for Multi-user-Dungeon or -Dimension. These are the precursor
to graphical MMORPG-style games like World of Warcraft.
## OPTIONS
back: about_evennia
next: using the webclient -> using webclient
back to top: start
>: using webclient
# ---------------------------------------------------------------------------------
## NODE using webclient
Evennia supports traditional telnet clients but also offers a HTML5 web client. It
is found (on a default install) by pointing your web browser to
|yhttp:localhost:4001/webclient|n
For a live example, the public Evennia demo can be found at
|yhttps://demo.evennia.com/webclient|n
The web client start out having two panes. The bottom one is where you insert commands
and the top one is where you see returns from the server.
- Use |y<Return>|n (or click the arrow on the right) to send your input.
- Use |yCtrl + <up-arrow>|n to step back and repeat a command you entered previously.
- Use |yCtrl + <Return>|n to add a new line to your input without sending.
If you want there is some |wextra|n info to learn about customizing the webclient.
## OPTIONS
back: about_muds
extra: learn more about customizing the webclient -> customizing the webclient
next: general command input -> command_input
back to top: start
>: back
# ---------------------------------------------------------------------------------
## NODE customizing the webclient
|y1)|n The panes of the webclient can be resized and you can create additional panes.
- Press the little plus (|w+|n) sign in the top left and a new tab will appear.
- Click and drag the tab and pull it far to the right and release when it creates two
panes next to each other.
|y2)|n You can have certain server output only appear in certain panes.
- In your new rightmost pane, click the diamond () symbol at the top.
- Unselect everything and make sure to select "testing".
- Click the diamond again so the menu closes.
- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane!
|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper
left corner. It allows to change things like font and if the client should play sound.
The "message routing" allows for rerouting text matching a certain regular expression (regex)
to a web client pane with a specific tag that you set yourself.
|y4)|n Close the right-hand pane with the |wX|n in the rop right corner.
## OPTIONS
back: using webclient
next: general command input -> command_input
back to top: start
> test *: send tagged message to new pane -> send_testing_tagged()
# ---------------------------------------------------------------------------------
## NODE command_input
The first thing to learn is to use the |yhelp|n command.
## OPTIONS
back: using webclient
next: (end) -> end
back to top: start
> h|help: command_passthrough(cmd=help)
# ---------------------------------------------------------------------------------
## NODE end
Thankyou for going through the tutorial!
"""
GOTO_CALLABLES = {
"command_passthrough": command_passthrough,
"send_testing_tagged": send_testing_tagged,
"do_nothing": do_nothing,
"send_string": send_string,
}
def testmenu(caller):
menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES)
# we'll use a custom EvMenu child later
EvMenu(caller, menutree, auto_help=False)

View file

@ -1584,7 +1584,7 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
#
# -------------------------------------------------------------
_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S+?)$", re.I + re.M)
_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$", re.I + re.M)
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M)
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
_RE_CALLABLE = re.compile(
@ -1650,7 +1650,7 @@ def _generated_input_goto_func(caller, raw_string, **kwargs):
return goto, {"generated_nodename": goto}
# no glob pattern match; try regex
for pattern, goto in gotomap.items():
if re.match(pattern, raw_string.lower(), flags=re.I + re.M):
if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M):
if _RE_CALLABLE.match(goto):
gotofunc = goto.strip()[:-2]
if gotofunc in goto_callables:
@ -1748,6 +1748,7 @@ def parse_menu_template(caller, menu_template, goto_callables=None):
}
)
print(f"nodename: {nodename}, options: {options}")
return options
def _parse(caller, menu_template, goto_callables):

View file

@ -6,7 +6,7 @@
let goldenlayout = (function () {
var myLayout;
var knownTypes = ["all", "untagged"];
var knownTypes = ["all", "untagged", "testing"];
var untagged = [];
var newTabConfig = {