"""
textgame.parser
=====================
This module's main class is :class:`textgame.parser.Parser`. The parser can take
user input, call a function that's associated to the input and return to the user a
message describing what happened.
Use ``actionmap`` and ``legal_verbs`` to define how verbs should be mapped to functions, eg:
.. code-block:: python
parser.actionmap.update({
"scream": player.scream
})
parser.legal_verbs.update({
"scream": "scream",
"shout": "scream"
})
You can use ``legal_nouns`` to define synonyms for nouns.
A parser is the only thing needed to during the main loop of a game:
.. code-block:: python
parser = textgame.parser.Parser(player)
while player.status["alive"]:
response = parser.understand( input("> ") )
print(response)
This module also provides :class:`textgame.parser.EnterYesNoLoop`. If a function
called by the parser returns an ``EnterYesNoLoop`` instead of a string, the parser falls
into a mode where it only allows 'yes' and 'no' as an answer. An object of type
``EnterYesNoLoop`` also provides strings/functions to print/call for each case.
Example: a player method that saves the user from drinking poison
.. code-block:: python
@action_method
def drink(self, noun):
if noun == "poison":
def actually_do_it():
self.status["alive"] = False
return "You drink the poison and die."
return textgame.parser.EnterYesNoLoop(
question = "Do you really want to drink poison?",
yes = actually_do_it,
no = "You stay alive")
else:
# ...
"""
from collections import namedtuple
import logging
logger = logging.getLogger("textgame.parser")
logger.addHandler(logging.NullHandler())
from textgame.globals import INFO
[docs]class EnterYesNoLoop:
"""
:param question: a yes/no question
:type question: string
:param yes: string to return or a function with signature ``f() -> str`` or ``f() -> EnterYesNoLoop`` that should get called if player answeres 'yes' to the question
:param no: same as yes
"""
def __init__(self, question, yes, no):
self.question = question
self._yes = yes
self._no = no
[docs] def yes(self):
"""
if yes is callable, return its result, else return it
"""
if callable(self._yes):
return self._yes()
return self._yes
[docs] def no(self):
"""
if no is callable, return its result, else return it
"""
if callable(self._no):
return self._no()
return self._no
[docs]class Parser:
"""
:param player: :class:`textgame.player.Player` object
"""
def __init__(self, player):
self.player = player
self.in_yesno = False # are we inside a yes/no conversation?
# yesno_backup must be a function that takes a bool and returns
# user output. it will be executed if yes/no conversation ends with yes
self.yesno_backup = None
self.legal_verbs = {
"": "continue", # dont do anything on empty input
"attack": "attack",
"back": "back",
"close": "close",
"d": "down",
"down": "down",
"drop": "drop",
"e": "east",
"east": "east",
"enter": "go",
"go": "go",
"grab": "take",
"hear": "listen",
"hint": "hint",
"inventory": "inventory",
"kill": "attack",
"listen": "listen",
"lock": "close",
"look": "look",
"n": "north",
"north": "north",
"open": "open",
"s": "south",
"score": "score",
"south": "south",
"take": "take",
"u": "up",
"up": "up",
"w": "west",
"walk": "go",
"west": "west",
}
# this may be used to define synonyms
self.legal_nouns = {
"d": "down",
"e": "east",
"n": "north",
"s": "south",
"u": "up",
"w": "west",
}
# the lambdas are there because the values in this dict must be
# callable with exactly one argument
self.actionmap = {
"attack": player.attack,
"back": lambda x: player.go("back"),
"continue": lambda x: "",
"down": lambda x: player.go("down"),
"drop": player.drop,
"east": lambda x: player.go("east"),
"go": player.go,
"hint": player.ask_hint,
"inventory": player.list_inventory,
"listen": player.listen,
"look": player.look,
"close": player.close,
"north": lambda x: player.go("north"),
"open": player.open,
"score": player.show_score,
"south": lambda x: player.go("south"),
"take": player.take,
"up": lambda x: player.go("up"),
"west": lambda x: player.go("west"),
}
self.check()
[docs] def lookup_verb(self, verb):
return self.legal_verbs.get(verb)
[docs] def lookup_noun(self, noun):
return self.legal_nouns.get(noun)
[docs] def check(self):
"""
check if every verb in self.legal_verbs has a function mapped to.
if not, the game will crash on the input of this verb
logs the error
"""
for verb in set(self.legal_verbs.values()):
if verb not in self.actionmap:
logger.error("{} is a legal verb but has no definition"
"in actionmap".format(verb))
[docs] def check_result(self, result):
"""
checks if result is EnterYesNoLoop or str, if it's EnterYesNoLoop,
return the question and fall back to yes/no mode
"""
if type(result) is str:
return result
else:
# assume that result is of type enteryesnoloop
self.in_yesno = True
self.yesno_backup = result
return result.question
[docs] def do(self, verb, noun):
"""
call function associated with verb with noun as argument
"""
return self.actionmap[verb](noun)
def _split_input(self, input):
"""
take input and return verb and noun
"""
args = input.split()
if len(args) > 2:
# this gets catched in Parser.understand
raise ValueError()
elif len(args) == 2:
verb, noun = args
elif len(args) == 1:
verb = args[0]
noun = ""
else:
verb = ""
noun = ""
return verb, noun
[docs] def understand(self, input):
"""
based on the input, perform player method and return its output
the return value is what can be printed to the user
"""
try:
verb, noun = self._split_input(input)
except ValueError:
return INFO.TOO_MANY_ARGUMENTS
# if a yes/no conversation is going on, only allow yes/no as answers
if self.in_yesno:
if verb != "yes" and verb != "no":
return INFO.YES_NO
elif verb == "yes":
self.in_yesno = False
# return the yes case
result = self.yesno_backup.yes()
return self.check_result(result)
else:
self.in_yesno = False
# return the no case
result = self.yesno_backup.no()
return self.check_result(result)
commandverb = self.lookup_verb(verb)
commandnoun = self.lookup_noun(noun)
# if noun is illegal, reset to it's original value and feed it to
# the actionmethods. More creative output if erronous input :)
if not commandnoun:
commandnoun = noun
logger.debug("I understood: verb={} noun={}".format(repr(commandverb), repr(commandnoun)))
# illegal nouns are okay but illegal verbs are not
if not commandverb:
return INFO.NOT_UNDERSTOOD
# perform the associated method
result = self.do(commandverb, commandnoun)
return self.check_result(result)