From eacebb0106b08b384296432b685930a0e68758d3 Mon Sep 17 00:00:00 2001 From: Cassowary Date: Mon, 25 Sep 2023 18:29:07 -0700 Subject: [PATCH] Initial chekin post-discontinuity. --- .gitignore | 61 +++++++++++ README.md | 6 ++ TODO.md | 4 + license.txt | 25 +++++ pyoo/__init__.py | 0 pyoo/base.py | 122 ++++++++++++++++++++++ pyoo/interpret.py | 139 +++++++++++++++++++++++++ pyoo/placeloader.py | 87 ++++++++++++++++ pyoo/things.py | 241 ++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 0 roomtest.txt | 40 ++++++++ setup.py | 21 ++++ testrooms.py | 49 +++++++++ testverb.py | 91 +++++++++++++++++ tox.ini | 14 +++ 15 files changed, 900 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100644 license.txt create mode 100644 pyoo/__init__.py create mode 100644 pyoo/base.py create mode 100644 pyoo/interpret.py create mode 100644 pyoo/placeloader.py create mode 100644 pyoo/things.py create mode 100644 requirements.txt create mode 100644 roomtest.txt create mode 100644 setup.py create mode 100644 testrooms.py create mode 100644 testverb.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..275878b --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Emacs git ignore +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6723420 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +PYOO +===== + +A Python text-adventure engine inspired by MOO server. + +See testverb.py and testrooms.py for an example mini-adventures. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b73ec99 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +To Do +--------- + +--- Something someday. diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..932f4b2 --- /dev/null +++ b/license.txt @@ -0,0 +1,25 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Copyright (c) 2014, 2019, 2020, Cas Rusnov + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyoo/__init__.py b/pyoo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyoo/base.py b/pyoo/base.py new file mode 100644 index 0000000..4264514 --- /dev/null +++ b/pyoo/base.py @@ -0,0 +1,122 @@ +""" +Base data and types for supporting Pyoo operations. +""" + +from collections import namedtuple +from typing import Callable, List, Tuple, Type, Sequence, Union, Iterable, Protocol + + +PREPOSITIONS = ( + "with/using", + "at/to", + # 'in front of', + "in/inside/into", + # 'on top of/on/onto/upon', + "on/onto/upon", + # 'out of/from inside/from', + "from", + "over", + "through", + "under/underneath/beneath", + "behind", + "beside", + "for/about", + "is", + "as", + "off", +) +"""`tuple[str]`: A list of preposition strings (equivilancies are / separated)""" + +NORMALIZED_PREPS = tuple([x.split("/") for x in PREPOSITIONS]) +"""A list of prepositions with equiviliants split.""" + + +class PyooError(Exception): + """Generic Pyoo error.""" + + +class PyooVerbNotFound(PyooError): + """Pyoo Error which indicates a verb was not found.""" + + +class PyooObjectNotFound(PyooError): + """Pyoo Error which indicates an object was not found.""" + + +VerbCallFrame = namedtuple("VerbCallFrame", "environment,player,verbname,dobj,dobjstr,prepstr,iobj,iobjstr,argstr") +"""Namedtuple which contains the call frame (all arguments) for a verb call.""" + + +class Verb: + """This is a wrapper for a callable to give it the properties required for verbs.""" + def __init__(self, + function: Callable, + names: List[str], + callspec: Tuple[str, List[str], str]): + self.function = function + self.name = names[0] + self.names = names + self.callspec = callspec + + def __call__(self, *args, **kwargs) -> None: + return self.function(*args, **kwargs) + + def __get__(self, obj, objtype: Type): + class VerbFunction: + def __init__(self, func: Callable, obj, typ: Type, verb: "Verb"): + self._func = func + self._obj = obj + self._typ = typ + self.is_verb = True + self.name = verb.name + self.names = verb.names + self.callspec = verb.callspec + + def __call__(self, *args, **kwargs): + return self._func.__get__(self._obj, self._typ)(*args, **kwargs) + + return VerbFunction(self.function, obj, objtype, self) + + def __repr__(self) -> str: + return f"" + + +def make_verb(verbname: str, dobjspec: str, prepspec: str, iobjspec: str) -> Callable[..., Verb]: + """This simple decorator adds verb metadata to a method or function. + + Arguments: + verbname (str): List of verb names, comma separated, with wildcards. + dobjspec (str): 'this' or 'that' or 'none' or 'any' + (this = the object which defines the verb, that = an object in the soup, any = any string, + none = blank) + iobjspec (str): 'this' or 'that' or 'none' or 'any' (as above) + prepspec (str): one of prepositions strings (as in the PREPOSITIONS list) + """ + def verb_decorate(verbfunc: Callable[[VerbCallFrame, ], None]) -> Verb: + names = [x.strip() for x in verbname.split(",")] + ps: List[str] = [] + if prepspec.find("/") > 0: + ps = prepspec.split("/") + else: + for prep in NORMALIZED_PREPS: + if prepspec in prep: + ps = prep + break + ps = [prepspec] + return Verb(verbfunc, names, (dobjspec, ps, iobjspec)) + + return verb_decorate + + +# +# type things +# + + +class InterpreterProtocol(Protocol): + """Defines minimal protocol for interpreter objects.""" + def update(self) -> None: + ... + + +SingleMultiString = Union[Sequence[str], str, Iterable[str]] diff --git a/pyoo/interpret.py b/pyoo/interpret.py new file mode 100644 index 0000000..e5fab77 --- /dev/null +++ b/pyoo/interpret.py @@ -0,0 +1,139 @@ +"""This module defines the basic interpreter system.""" + +import fnmatch + +from typing import Optional, List, Set, Tuple, cast + +from .things import Thing, Container, Place, Player +from .base import VerbCallFrame, PyooVerbNotFound, PyooObjectNotFound + + +class Interpreter: + """Base interpreter class. + + Manages a collection of objects, and invoking verbs on them. + """ + def __init__(self, contents: Optional[List[Thing]] = None): + """Initialize an interpreter. + + Arguments: + contents (optional list of things): The initial contents to populate the interpreter with. + + """ + if contents is None: + contents = [] + self.contents: Set[Thing] = set(contents) + for cont in self.contents: + cont.interpreter = self + self.content_cache: List[Tuple[str, Thing]] = [] + self.update() + + def add_player(self, player_object: Player) -> None: + """Add a player to the interpreter.""" + self.contents.add(player_object) + player_object.interpreter = self + self.update() + + def remove_player(self, player_object: Player) -> None: + """Remove a player from the interpreter.""" + self.contents.remove(player_object) + self.update() + + def update(self) -> None: + """Do any updates required when objects are added or removed.""" + self.update_caches() + + def update_caches(self) -> None: + """Update the caches for objects contained in the interpreter, as well as updating all of the caches + on contained containers. + """ + self.content_cache = [] + for obj in self.contents: + for name in obj.names: + self.content_cache.append((name, obj)) + try: + cast(Container, obj).update_caches() + except AttributeError: + pass + + def handle_move(self, newroom: Place, player: Player) -> None: + """Move a player to a new contained room.""" + if player.location: + player.location.handle_exit(player) + newroom.handle_enter(player) + + def lookup_global_object(self, objstr: str) -> List[Tuple[str, Thing]]: + """Find an object within the soup by objstr.""" + return [x for x in self.content_cache if fnmatch.fnmatch(objstr, x[0])] + + def lookup_object(self, player: Player, objstr: str) -> Optional[Thing]: + """Find an object relative to a player + + This first checks the player's inventory, and then the container which contains the + player. + """ + m: Optional[List[Tuple[str, Thing]]] = None + try: + m = player.get_name_matches(objstr) + except PyooObjectNotFound: + m = None + if not m and player.location: + m = player.location.get_name_matches(objstr) + if m: + return m[0][1] + else: + return None + + def interpret(self, command: str, player: Player) -> None: + """Interpret a player's command by splitting it into components and finding matching verbs.""" + # FIXME make this better to support mulitple word objstrs and prepstr + if not command: + return + + cmd_comps = command.split() + verbstr = cmd_comps[0] + dobjstr = "" + prepstr = "" + iobjstr = "" + argstr = "" + + try: + argstr = " ".join(cmd_comps[1:]) + except IndexError: + pass + + try: + dobjstr = cmd_comps[1] + prepstr = cmd_comps[2] + iobjstr = cmd_comps[3] + except IndexError: + pass + + try: + cmdmatches = player.get_command_matches(command) + except PyooVerbNotFound: + if player.location: + cmdmatches = player.location.get_command_matches(command) + else: + raise PyooVerbNotFound + cmd = cmdmatches[0] + # glob = cmd[0] + # comps = cmd[1] + verb = cmd[2] + this = cmd[3] + + dobj = None + if verb.callspec[0] == "this": + dobj = this + elif verb.callspec[0] == "that": + # lookup object + dobj = self.lookup_object(player, dobjstr) + + iobj = None + if verb.callspec[2] == "this": + iobj = this + elif verb.callspec[2] == "that": + # lookp object + iobj = self.lookup_object(player, iobjstr) + + return verb(VerbCallFrame(self, player, verbstr, dobj, dobjstr, prepstr, iobj, iobjstr, argstr)) diff --git a/pyoo/placeloader.py b/pyoo/placeloader.py new file mode 100644 index 0000000..b2d64d3 --- /dev/null +++ b/pyoo/placeloader.py @@ -0,0 +1,87 @@ +"""PlaceLoader is a system to load simple plain text data to build a room-based space.""" + +from enum import Enum +from typing import Dict, List, TextIO, Type + +from .things import Place +from .interpret import Interpreter + + +class PlaceLoaderStates(Enum): + START = 0 + DESC = 1 + EXITS = 2 + + +class PlaceLoader: + """ + A factory which loads a simple data format containing a description of rooms, and how + they are connected, and produces room objects for each one. + + The format is a plain text file containing a series of room definitions: + + ROOMNAME + DESCRIPTION (multiple lines) + . + direction,alias ROOMNAME2 + . + ROOMNAME2 ... + + """ + + def __init__(self, in_file: TextIO, baseplace: Type[Place] = Place): + """Initialize the factory by loading a description file. + + Arguments: + in_file: A file to load the places from. + baseplace: A type to initialize the rooms from. + + """ + self.base = baseplace + self.places: Dict[str, Place] = {} # indexed by name + + if in_file: + self.process_file(in_file) + + def process_file(self, in_file: TextIO) -> None: + """Load any room information from the passed in file. + + Arguments: + in_file: The file to load the rooms from. + + """ + state = PlaceLoaderStates.START + desc: List[str] = [] + temp_exits: Dict[Place, List[str]] = {} + for line in in_file: + line = line.strip() + if state == PlaceLoaderStates.START: + rm = self.base(line) + temp_exits[rm] = [] + state = PlaceLoaderStates.DESC + desc = [] + elif state == PlaceLoaderStates.DESC: + if line == ".": + state = PlaceLoaderStates.EXITS + rm.description = desc + else: + desc.append(line) + elif state == PlaceLoaderStates.EXITS: + if line == ".": + state = PlaceLoaderStates.START + self.places[rm.name] = rm + else: + temp_exits[rm].append(line) + + # assemble the exits + for place in list(self.places.values()): + for ext in temp_exits[place]: + names, destination = ext.split(" ", 1) + for nm in names.split(","): + place.ways[nm] = self.places[destination] + place.update_go() + + +def interpreter_from_placeloader(placeloader: PlaceLoader) -> Interpreter: + """Return an interpreter intialized with the contents of a placeloader.""" + return Interpreter(list(placeloader.places.values())) diff --git a/pyoo/things.py b/pyoo/things.py new file mode 100644 index 0000000..bdd05e4 --- /dev/null +++ b/pyoo/things.py @@ -0,0 +1,241 @@ +"""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("", 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 "" % (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 "" % (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 "" % (self.name, self.__hash__()) + + +class Player(Container): + def __init__(self, names: str, description: str = ""): + super().__init__(names, description) + + def __repr__(self) -> str: + return "" % (self.name, self.__hash__()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/roomtest.txt b/roomtest.txt new file mode 100644 index 0000000..c52cb7c --- /dev/null +++ b/roomtest.txt @@ -0,0 +1,40 @@ +Porch +This is a small, fairly nondescript porch covered on two sides with a neck-high fence. It is made of old boards nailed down instead of screwed. + +To the north is the front door, slightly ajar. +. +n,north Play Room +. +Play Room +This is a medium-sized room with light wood parcade floor. There are toys of every description strewn about willy-nilly, two large baskets of stuffed animals and various tables adorned with books and drawing supplies. Large south-facing windows stream bright winter sunlight, warming the room. + +To the south is the front door. To the east the room empties into the living room, to the north it continues into the hallway. +. +s,south Porch +n,north Hallway +e,east Living Room +. +Hallway +This is a narrow wood-floored hallway, echoing with every sound. + +Several doors connect to the hallway, all closed. It also contiues to the south into the play room, and east into the kitchen. +. +s,south Play Room +e,east Kitchen +. +Kitchen +This is a small linolium-floored kitchen, with red counters, a double stainless steel sink filled with dishes and a noisy refridgerator. It is illuminated from above by recessed flourescent lights, although only one bulb seems to be working. + +The room contiues to the south into the living room, and to the west into the hallway. +. +s,south Living Room +w,west Hallway +. +Living Room +This small intimate livingroom with light wood parkade flooring is dominated by a large wampa rug on the floor and a TV opposite a three seat red couch. A window to the south has the shades draw for enhanced viewing. + +The room continues to the west past the couch into the play room, and to the north into the kitchen. +. +w,west Play Room +n,north Kitchen +. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..629752e --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +"""Module setup.""" + +import os.path +from setuptools import setup, find_packages + +PACKAGE_NAME = "pyoo" +VERSION = "0.0.3" + +srcpath = os.path.dirname(__file__) + +if __name__ == "__main__": + setup( + name=PACKAGE_NAME, + version=VERSION, + packages=find_packages(), + python_requires=">=3.6.3", + # scripts=["scripts/adventure.py"] + description="A simple LambdaMOO-like command interpreter for Python", + long_description=open(os.path.join(srcpath, "README.md"), "r").read(), + long_description_content_type="text/markdown", + ) diff --git a/testrooms.py b/testrooms.py new file mode 100644 index 0000000..f926091 --- /dev/null +++ b/testrooms.py @@ -0,0 +1,49 @@ +from pyoo.things import Place, Player +from pyoo.placeloader import interpreter_from_placeloader, PlaceLoader +from pyoo.interpret import PyooVerbNotFound +from pyoo.base import make_verb + + +class DescriptivePlace(Place): + def handle_enter(self, player): + super().handle_enter(player) + self.do_look() + + @make_verb("look,l", "none", "none", "none") + def look(self, verb_callframe): + self.do_look() + + def do_look(self): + print(self.name) + if isinstance(self.description, str): + print(self.description) + else: + for line in self.description: + print(line) + + +loader = PlaceLoader(open("roomtest.txt", "r"), DescriptivePlace) +player = Player("player") +game = interpreter_from_placeloader(loader) +porch = game.lookup_global_object("Porch")[0][1] +run = True +game.update() +game.handle_move(porch, player) + +# REPL +if __name__ == "__main__": + while run: + cmd = "" + try: + cmd = input(">") + except EOFError: + run = False + if cmd.startswith("quit"): + run = False + else: + try: + game.interpret(cmd, player) + except PyooVerbNotFound: + print("I don't understand that.") + + print("Bye!") diff --git a/testverb.py b/testverb.py new file mode 100644 index 0000000..a73ce23 --- /dev/null +++ b/testverb.py @@ -0,0 +1,91 @@ +from pyoo.interpret import Interpreter +from pyoo.things import Thing, Place, Player +from pyoo.base import make_verb, PyooVerbNotFound + + +class Hammer(Thing): + def __init__(self): + Thing.__init__(self, "hammer", "a heavy ball-peen hammer.") + + @make_verb("hit", "that", "with", "this") + def hit(self, verb_callframe): + try: + verb_callframe.dobj.handle_hit(self) + except AttributeError: + pass + + @make_verb("drop", "this", "none", "none") + def drop(self, verb_callframe): + print(verb_callframe) + + @make_verb("get", "this", "none", "none") + def get(self, verb_callframe): + print(verb_callframe) + + +class Nail(Thing): + def __init__(self): + Thing.__init__(self, "nail", "a nine inch nail.") + self.depth = 1 + + def handle_hit(self, hitter): + if self.depth > 0: + print("bang! the nail is hammered.") + self.depth -= 1 + else: + print("ping! the nail won't go deeper.") + + def contents_desc_hook(self): + if self.depth > 0: + return "You see a nail sticking out "+str(self.depth)+"cm." + else: + return "You see a nail fully hammered in." + + +class HammerTime(Place): + def __init__(self): + Place.__init__(self, "HAMMERTIME") + self.handle_enter(Hammer()) + self.handle_enter(Nail()) + + @make_verb("look,l", "none", "none", "none") + def look(self, verb_callframe): + for cont in self.contents: + try: + print(cont.contents_desc_hook()) + except AttributeError: + continue + print("You see a hammer.") + + @make_verb("look,l", "that", "none", "none") + def look_at(self, verb_callframe): + if verb_callframe.dobj: + dobj = verb_callframe.dobj + print("%s: %s" % (dobj.name, dobj.description)) + else: + print("That doesn't appear to be here.") + + +hammertime = HammerTime() +game = Interpreter([hammertime]) +player = Player("player") +game.add_player(player) +game.handle_move(hammertime, player) +game.update() +run = True +if __name__ == "__main__": + while run: + cmd = "" + try: + cmd = input(">") + except EOFError: + run = False + if cmd.startswith("quit"): + run = False + else: + try: + game.interpret(cmd, player) + except PyooVerbNotFound: + print("I don't understand that.") + + print("Bye!") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..49c7de0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py36, py37, py38, py39 + +[testenv] +deps = + flake8 + mypy +commands = + flake8 + mypy -p pyoo + +[flake8] +max-line-length = 120 +max-complexity = 15 \ No newline at end of file