Files
pyoo/pyoo/things.py

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__())