242 lines
8.4 KiB
Python
242 lines
8.4 KiB
Python
"""Base (Pyoo) classes which implement basic protocols."""
|
|
|
|
import fnmatch
|
|
import itertools
|
|
|
|
from typing import cast, List, Optional, Tuple, Set, Dict
|
|
|
|
from .base import make_verb, PyooVerbNotFound, InterpreterProtocol, SingleMultiString, Verb, VerbCallFrame
|
|
|
|
|
|
class Thing:
|
|
"""The base of all Pyoo objects."""
|
|
|
|
def __init__(self, name: str, description: SingleMultiString = "A nondescript object."):
|
|
"""Create a Thing
|
|
|
|
Arguments:
|
|
name (str): A string of comma-separated names for this object.
|
|
description (str): A string that describes this object.
|
|
"""
|
|
names = [x.strip() for x in name.split(",")]
|
|
self.name = names[0]
|
|
self.names: Tuple[str, ...] = tuple(names)
|
|
self.description = description
|
|
self.location: Optional["Container"] = None
|
|
self.interpreter: Optional[InterpreterProtocol] = None
|
|
|
|
def tell(self, message: SingleMultiString) -> None:
|
|
print("<tell stub>", self, message)
|
|
|
|
def verbs(self) -> List[Verb]:
|
|
"""Return a list of bound methods which denote themselves as verbs."""
|
|
verbs: List[Verb] = []
|
|
for item in dir(self):
|
|
try:
|
|
v = self.__getattribute__(item)
|
|
if v.is_verb:
|
|
verbs.append(v)
|
|
except AttributeError:
|
|
continue
|
|
return verbs
|
|
|
|
def verb_globs(self) -> List[Tuple[str, Tuple, Verb, "Thing"]]:
|
|
"""Return a list of (globstr, bound method) where commands matching globstr should call method (given that
|
|
'that' matches an object in the soup).
|
|
"""
|
|
verbglobs: List[Tuple[str, Tuple, Verb, "Thing"]] = []
|
|
for vrb in self.verbs():
|
|
vvars: List[Tuple] = [tuple(vrb.names)]
|
|
if vrb.callspec[0] == "this":
|
|
vvars.append(self.names)
|
|
elif vrb.callspec[0] in ("that", "any"):
|
|
vvars.append(("*",))
|
|
|
|
if vrb.callspec[1] != ["none"]:
|
|
vvars.append(tuple(vrb.callspec[1]))
|
|
|
|
if vrb.callspec[2] == "this":
|
|
vvars.append(self.names)
|
|
elif vrb.callspec[2] in ("that", "any"):
|
|
vvars.append(("*",))
|
|
|
|
for combo in itertools.product(*vvars):
|
|
globstr = " ".join(combo)
|
|
verbglobs.append((globstr, tuple(combo), vrb, self))
|
|
|
|
return verbglobs
|
|
|
|
def handle_move(self, newlocation: "Container") -> None:
|
|
"""Handle moving this object to a new location.
|
|
|
|
Acts as a way for children to hook this occurance too.
|
|
"""
|
|
self.location = newlocation
|
|
|
|
def handle_remove(self, oldlocation: "Container") -> None:
|
|
"""Handle removing this object from a container.
|
|
|
|
Acts as a way for children to hook this occurance too.
|
|
"""
|
|
self.location = None
|
|
|
|
def __repr__(self) -> str:
|
|
return "<Thing '%s' object at 0x%x>" % (self.name, self.__hash__())
|
|
|
|
|
|
class Container(Thing):
|
|
"""A Pyoo object which contains other Pyoo objects."""
|
|
|
|
def __init__(self, names: str, description: SingleMultiString = ""):
|
|
"""Create a Container.
|
|
|
|
Arguments:
|
|
names (str): A comma-separated list of names.
|
|
description (str): A description for this object.
|
|
|
|
"""
|
|
super().__init__(names, description)
|
|
self.contents: Set[Thing] = set()
|
|
self.name_cache: List[Tuple[str, Thing]] = []
|
|
self.command_cache: List[Tuple[str, Tuple, Verb, Thing]] = []
|
|
|
|
def update_caches(self) -> None:
|
|
"""Update the internal cache of contained object's verbs and names."""
|
|
self.name_cache = []
|
|
self.command_cache = []
|
|
for obj in self.contents:
|
|
for name in obj.names:
|
|
self.name_cache.append((name, obj))
|
|
for verbglob in obj.verb_globs():
|
|
if verbglob[0][0] == "#":
|
|
continue
|
|
self.command_cache.append(verbglob)
|
|
for verbglob in self.verb_globs():
|
|
if verbglob[0][0] == "#":
|
|
continue
|
|
self.command_cache.append(verbglob)
|
|
|
|
def get_command_matches(self, command_spec: str) -> List[Tuple[str, Tuple, Verb, Thing]]:
|
|
"""Return a list of commands which match a command_spec ordered by specificity.
|
|
|
|
Arguments:
|
|
command_spec (str): A command
|
|
|
|
Returns:
|
|
array[tuple]: A list of command specifiers.
|
|
|
|
"""
|
|
res = [x for x in self.command_cache if fnmatch.fnmatch(command_spec, x[0])]
|
|
# sort by ambiguity (percentage of *)
|
|
res.sort(key=lambda a: a[0].count("*") / float(len(a[0])))
|
|
if (len(res) < 1):
|
|
raise PyooVerbNotFound
|
|
return res
|
|
|
|
def get_name_matches(self, name: str) -> List[Tuple[str, Thing]]:
|
|
"""Return a list of objects which match a name spec.
|
|
|
|
Arguments:
|
|
name (str): A name of an object
|
|
|
|
Returns:
|
|
array[tuple]: A list of name,thing tuples.
|
|
|
|
"""
|
|
return [x for x in self.name_cache if fnmatch.fnmatch(name, x[0])]
|
|
|
|
def handle_exit(self, oldobj: Thing) -> None:
|
|
"""Handle an object leaving the container.
|
|
|
|
This allows children to hook this occurence also.
|
|
|
|
Arguments:
|
|
oldobj (`pyoo.things.Thing`): The object which is exiting.
|
|
|
|
"""
|
|
self.contents.remove(oldobj)
|
|
oldobj.handle_remove(self)
|
|
|
|
def handle_enter(self, newobj: Thing) -> None:
|
|
"""Handle an object entering the container.
|
|
|
|
This allows children to hook this occurence also.
|
|
|
|
Arguments:
|
|
newobj (`pyoo.things.Thing`): The object which is entering.
|
|
|
|
"""
|
|
self.contents.add(newobj)
|
|
newobj.handle_move(self)
|
|
try:
|
|
cast("Container", newobj).update_caches()
|
|
except AttributeError:
|
|
pass
|
|
self.update_caches()
|
|
|
|
def handle_tell(self, msg: SingleMultiString, who: Set[Thing]) -> None:
|
|
"""Handle processing a tell to a list of objects.
|
|
|
|
"""
|
|
for obj in who:
|
|
obj.tell(msg)
|
|
|
|
def tell(self, msg: SingleMultiString) -> None:
|
|
"""Handle telling all content objects."""
|
|
self.handle_tell(msg, self.contents)
|
|
|
|
def __repr__(self) -> str:
|
|
return "<Container '%s' object at 0x%x>" % (self.name, self.__hash__())
|
|
|
|
|
|
class Place(Container):
|
|
"""A specialized container which models a place."""
|
|
|
|
def __init__(self, names: str, description: str = "") -> None:
|
|
"""Create a place."""
|
|
super().__init__(names, description)
|
|
self.ways: Dict[str, Container] = dict()
|
|
|
|
def tell_only(self, message: SingleMultiString, verb_callframe: VerbCallFrame) -> None:
|
|
self.handle_tell(message, self.contents - {verb_callframe.player})
|
|
|
|
# this verb expects to be annotated from update_go. We never want it ot be at the top of a match list by its deault
|
|
# name either
|
|
@make_verb("#go", "none", "none", "none")
|
|
def go(self, verb_callframe: VerbCallFrame) -> None:
|
|
self.do_go(verb_callframe.verbname, verb_callframe)
|
|
|
|
@make_verb("go,move,walk,run", "any", "none", "none")
|
|
def go_dir(self, verb_callframe: VerbCallFrame) -> None:
|
|
self.do_go(verb_callframe.dobjstr, verb_callframe)
|
|
|
|
def do_go(self, direction: str, verb_callframe: VerbCallFrame) -> None:
|
|
if direction in self.ways:
|
|
self.tell_only("{} moves {}".format(verb_callframe.player.name, direction), verb_callframe)
|
|
verb_callframe.player.tell("You move {}".format(direction))
|
|
verb_callframe.environment.handle_move(self.ways[direction], verb_callframe.player)
|
|
|
|
def handle_enter(self, newobj: Thing) -> None:
|
|
super().handle_enter(newobj)
|
|
self.handle_tell("{} arrives".format(newobj.name), self.contents - {newobj})
|
|
|
|
def update_go(self) -> None:
|
|
# note does no actually remove items from the go verb in case the descender is overloading.
|
|
# also note, the interpreter needs to have update() called after this is called.
|
|
for direction in self.ways:
|
|
if direction not in self.go.names:
|
|
self.go.names.append(direction)
|
|
if self.interpreter:
|
|
self.interpreter.update()
|
|
|
|
def __repr__(self) -> str:
|
|
return "<Place '%s' object at 0x%x>" % (self.name, self.__hash__())
|
|
|
|
|
|
class Player(Container):
|
|
def __init__(self, names: str, description: str = ""):
|
|
super().__init__(names, description)
|
|
|
|
def __repr__(self) -> str:
|
|
return "<Player '%s' object at 0x%x>" % (self.name, self.__hash__())
|