Update all code to be python3 compatible. Add tox. Modernize things. Add license, setup, etc.

main
Cas Rusnov 3 years ago
parent 77563ec9e5
commit 9cc4854732
  1. 25
      license.txt
  2. 6
      pyoo/__init__.py
  3. 54
      pyoo/base.py
  4. 54
      pyoo/interpret.py
  5. 56
      pyoo/placeloader.py
  6. 52
      pyoo/things.py
  7. 0
      requirements.txt
  8. 21
      setup.py
  9. 39
      testrooms.py
  10. 60
      testverb.py
  11. 8
      tox.ini

@ -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.

@ -1,6 +0,0 @@
"""PYOO: A python MOO-like verb based text adventure engine."""
from .interpret import Interpreter, PyooError, PyooVerbNotFound
from .base import verb, prepositions, normalized_preps
from .things import Thing, Container, Place, Player
from .placeloader import PlaceLoader

@ -1,43 +1,51 @@
prepositions = ('with/using',
'at/to',
# 'in front of',
'in/inside/into',
# 'on top of/on/onto/upon',
# 'out of/from inside/from',
'over',
'through',
'under/underneath/beneath',
'behind',
'beside',
'for/about',
'is',
'as',
'off')
#'off/off of')
normalized_preps = tuple([x.split('/') for x in prepositions])
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",
)
NORMALIZED_PREPS = tuple([x.split("/") for x in PREPOSITIONS])
# this simple decorator adds verb metadata to a method or function
# verbname is a comma-separated list of verb names with possible woldcard
# dobjspec is '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)
# dobjspec is '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 is 'this' or 'that' or 'none' or 'any'
# prepspec is one of prepositions strings
# verb prototypes are: (verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr)
def verb(verbname, dobjspec, prepspec, iobjspec):
def verb_decorate(verbfunc):
names = [x.strip() for x in verbname.split(',')]
names = [x.strip() for x in verbname.split(",")]
verbfunc.name = names[0]
verbfunc.names = names
ps = prepspec
if isinstance(ps, basestring):
if ps.find('/') > 0:
ps = ps.split('/')
if isinstance(ps, str):
if ps.find("/") > 0:
ps = ps.split("/")
else:
for p in normalized_preps:
for p in NORMALIZED_PREPS:
if ps in p:
ps = p
break
verbfunc.callspec = (dobjspec, ps, iobjspec)
verbfunc.is_verb = True
return verbfunc
return verb_decorate

@ -1,24 +1,22 @@
import string
import functools
import itertools
import fnmatch
from .base import prepositions, normalized_preps
class PyooError(Exception):
pass
class PyooVerbNotFound(PyooError):
pass
class Interpreter(object):
def __init__(self, defaultcontents=[], player=None, rooms=[], room=None):
# the "working" contents
self.contents = list()
self.contents = []
# name lookup caches
self.namecache = list()
self.commandcache = list()
self.namecache = []
self.commandcache = []
# the "player" object
self.player = player
@ -44,13 +42,13 @@ class Interpreter(object):
self.contents.append(self.room)
try:
self.contents.extend(self.room.contents)
except:
except Exception:
pass
if self.player:
self.contents.append(self.player)
try:
self.contents.extend(self.player.inventory)
except:
except Exception:
pass
self.update_namecache()
@ -58,30 +56,30 @@ class Interpreter(object):
self.update_placecache()
def update_namecache(self):
self.namecache = list()
self.namecache = []
for obj in self.contents:
for name in obj.names:
self.namecache.append((name, obj))
def update_placecache(self):
self.placecache = list()
self.placecache = []
for obj in self.rooms:
for name in obj.names:
self.placecache.append((name, obj))
def update_commandcache(self):
self.commandcache = list()
self.commandcache = []
for obj in self.contents:
for verbglob in obj.verb_globs():
# special case for internal verbs to opt out of matching by their default name, they start with #
if verbglob[0][0] == '#':
if verbglob[0][0] == "#":
continue
self.commandcache.append(verbglob)
def get_matches(self, command):
res = [x for x in self.commandcache if fnmatch.fnmatch(command, x[0])]
res = [x for x in self.commandcache if fnmatch.fnmatch(command, x[0])]
# sort by ambiguity (percentage of *)
res.sort(lambda a,b: cmp(a[0].count('*') / float(len(a[0])), b[0].count('*') / float(len(b[0]))))
res.sort(key=lambda a: a[0].count("*") / float(len(a[0])))
return res
def get_namematches(self, name):
@ -107,22 +105,22 @@ class Interpreter(object):
if not cmdmatches:
raise PyooVerbNotFound()
cmd = cmdmatches[0]
glob = cmd[0]
comps = cmd[1]
# glob = cmd[0]
# comps = cmd[1]
verb = cmd[2]
this = cmd[3]
# FIXME make this better to supprot mulitple word objstrs and prepstr
# FIXME make this better to support mulitple word objstrs and prepstr
cmd_comps = command.split(' ')
cmd_comps = command.split(" ")
verbstr = cmd_comps[0]
dobjstr = ''
prepstr = ''
iobjstr = ''
argstr = ''
dobjstr = ""
prepstr = ""
iobjstr = ""
argstr = ""
try:
argstr = ' '.join(cmd_comps[1:])
argstr = " ".join(cmd_comps[1:])
except IndexError:
pass
@ -134,9 +132,9 @@ class Interpreter(object):
pass
dobj = None
if verb.callspec[0] == 'this':
if verb.callspec[0] == "this":
dobj = this
elif verb.callspec[0] == 'that':
elif verb.callspec[0] == "that":
# lookup object
m = self.get_namematches(dobjstr)
if m:
@ -146,9 +144,9 @@ class Interpreter(object):
dobj = None
iobj = None
if verb.callspec[2] == 'this':
if verb.callspec[2] == "this":
iobj = this
elif verb.callspec[2] == 'that':
elif verb.callspec[2] == "that":
# lookp object
m = self.get_namematches(iobjstr)
if m:

@ -1,46 +1,58 @@
from enum import Enum
from typing import TextIO
from .things import Place
from .things import Place, Thing
class PlaceLoaderStates(Enum):
START = 0
DESC = 1
EXITS = 2
(ST_START, ST_DESC, ST_EXITS) = range(3)
class PlaceLoader(object):
def __init__(self, infile, baseplace=Place):
"""Create the factory by reading the contents of infile (an iteratable object [file-like or otherwise]). Specify the base class for place with baseplace."""
"""
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.
"""
def __init__(self, in_file: TextIO, baseplace: Thing = Place):
"""Initialize the factory by loading a description file."""
self.base = baseplace
self.places = dict() #indexed by name
self.places = {} # indexed by name
if infile:
self.process_file(infile)
if in_file:
self.process_file(in_file)
def process_file(self, inf):
state = ST_START
def process_file(self, in_file: TextIO):
"""Load any room information from the passed in file."""
state = PlaceLoaderStates.START
rm = None
desc = list()
for line in inf:
desc = []
for line in in_file:
line = line.strip()
if state == ST_START:
if state == PlaceLoaderStates.START:
rm = self.base(line)
rm.temp_exits = list()
state = ST_DESC
state = PlaceLoaderStates.DESC
desc = list()
elif state == ST_DESC:
if line == '.':
state = ST_EXITS
elif state == PlaceLoaderStates.DESC:
if line == ".":
state = PlaceLoaderStates.EXITS
rm.description = desc
else:
desc.append(line)
elif state == ST_EXITS:
if line == '.':
state = ST_START
elif state == PlaceLoaderStates.EXITS:
if line == ".":
state = PlaceLoaderStates.START
self.places[rm.name] = rm
rm = None
else:
rm.temp_exits.append(line)
# assemble the exits
for place in self.places.values():
for place in list(self.places.values()):
for ext in place.temp_exits:
names, destination = ext.split(' ',1)
for nm in names.split(','):
names, destination = ext.split(" ", 1)
for nm in names.split(","):
place.ways[nm] = self.places[destination]
place.update_go()

@ -1,12 +1,11 @@
import string
import functools
import itertools
from .base import verb
class Thing(object):
def __init__(self, thingname, description="A nondescript object."):
names = [x.strip() for x in thingname.split(',')]
names = [x.strip() for x in thingname.split(",")]
self.name = names[0]
self.names = tuple(names)
self.description = description
@ -26,26 +25,28 @@ class Thing(object):
return verbs
def verb_globs(self):
"""Return a list of (globstr, bound method) where commands matching globstr should call method (given that 'that' matches an object in the soup)."""
"""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()
for verb in self.verbs():
vvars = [verb.names,]
if verb.callspec[0] == 'this':
for vrb in self.verbs():
vvars = [vrb.names]
if vrb.callspec[0] == "this":
vvars.append(self.names)
elif verb.callspec[0] in ('that','any'):
vvars.append(('*',))
elif vrb.callspec[0] in ("that", "any"):
vvars.append(("*",))
if verb.callspec[1] != 'none':
vvars.append(verb.callspec[1])
if vrb.callspec[1] != "none":
vvars.append(vrb.callspec[1])
if verb.callspec[2] == 'this':
if vrb.callspec[2] == "this":
vvars.append(self.names)
elif verb.callspec[2] in ('that','any'):
vvars.append(('*',))
elif vrb.callspec[2] in ("that", "any"):
vvars.append(("*",))
for combo in itertools.product(*vvars):
globstr = ' '.join(combo)
verbglobs.append((globstr, tuple(combo), verb, self))
globstr = " ".join(combo)
verbglobs.append((globstr, tuple(combo), vrb, self))
return verbglobs
def handle_move(self, newlocation):
@ -54,8 +55,9 @@ class Thing(object):
def __repr__(self):
return "<Thing '%s' object at 0x%x>" % (self.name, self.__hash__())
class Container(Thing):
def __init__(self, names, description=''):
def __init__(self, names, description=""):
Thing.__init__(self, names, description)
self.contents = list()
@ -65,30 +67,32 @@ class Container(Thing):
def __repr__(self):
return "<Container '%s' object at 0x%x>" % (self.name, self.__hash__())
class Place(Container):
def __init__(self, names, description=''):
def __init__(self, names, description=""):
Container.__init__(self, names, description)
self.ways = dict()
# 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
@verb('#go','none','none','none')
# 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
@verb("#go", "none", "none", "none")
def go(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
self.do_go(verbname)
@verb('go,move,walk,run', 'any','none','none')
@verb("go,move,walk,run", "any", "none", "none")
def go_dir(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
self.do_go(dobjstr)
def do_go(self, direction):
if self.interpreter:
if self.ways.has_key(direction):
if direction in self.ways:
self.interpreter.handle_move(self.ways[direction])
def update_go(self):
# 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 not direction in self.go.names:
if direction not in self.go.names:
self.go.names.append(direction)
def __repr__(self):
@ -97,7 +101,7 @@ class Place(Container):
class Player(Thing):
def __init__(self):
Thing.__init__(self, 'player')
Thing.__init__(self, "player")
self.inventory = list()
def __repr__(self):

@ -0,0 +1,21 @@
"""Module setup."""
import os.path
from setuptools import setup, find_packages
PACKAGE_NAME = "pyoo"
VERSION = "0.0.2"
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",
)

@ -1,42 +1,45 @@
import pyoo
from pyoo import PlaceLoader, Thing, verb, Interpreter, Container, Place, Player, PyooVerbNotFound
from pyoo.things import Place, Player
from pyoo.placeloader import PlaceLoader
from pyoo.interpret import Interpreter, PyooVerbNotFound
from pyoo.base import verb
class DescriptivePlace(Place):
def handle_enter(self, player):
self.do_look()
@verb('look,l', 'none','none','none')
@verb("look,l", "none", "none", "none")
def look(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
self.do_look()
def do_look(self):
print self.name
if isinstance(self.description, basestring):
print self.description
print(self.name)
if isinstance(self.description, str):
print(self.description)
else:
for line in self.description:
print line
print(line)
loader = PlaceLoader(file('roomtest.txt','r'), DescriptivePlace)
game = Interpreter([], Player(), loader.places.values(), None)
porch = game.get_placematches('Porch')[0][1]
loader = PlaceLoader(open("roomtest.txt", "r"), DescriptivePlace)
game = Interpreter([], Player(), list(loader.places.values()), None)
porch = game.get_placematches("Porch")[0][1]
run = True
# REPL
if __name__ == '__main__':
if __name__ == "__main__":
game.handle_move(porch)
while (run):
cmd = ''
while run:
cmd = ""
try:
cmd = raw_input('>')
cmd = input(">")
except EOFError:
run = False
if cmd.startswith('quit'):
run = False
if cmd.startswith("quit"):
run = False
else:
try:
game.interpret(cmd)
except PyooVerbNotFound:
print "I don't understand that."
print("I don't understand that.")
print "Bye!"
print("Bye!")

@ -1,36 +1,39 @@
import pyoo
from pyoo import Thing, verb, Interpreter, Container, Place, Player, PyooVerbNotFound
from pyoo.interpret import Interpreter, PyooVerbNotFound
from pyoo.things import Thing, Place, Player
from pyoo.base import verb
class Hammer(Thing):
def __init__(self):
Thing.__init__(self, 'hammer','a heavy ball-peen hammer.')
Thing.__init__(self, "hammer", "a heavy ball-peen hammer.")
@verb('hit','that','with','this')
@verb("hit", "that", "with", "this")
def hit(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
try:
dobj.handle_hit(self)
except:
except Exception:
pass
@verb('drop','this','none','none')
@verb("drop", "this", "none", "none")
def drop(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
print verbname, dobjstr, prepstr, iobjstr, dobj, iobj
print(verbname, dobjstr, prepstr, iobjstr, dobj, iobj)
@verb('get','this','none','none')
@verb("get", "this", "none", "none")
def get(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
print verbname, dobjstr, prepstr, iobjstr, dobj, iobj
print(verbname, dobjstr, prepstr, iobjstr, dobj, iobj)
class Nail(Thing):
def __init__(self):
Thing.__init__(self, 'nail','a nine inch nail.')
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."
print("bang! the nail is hammered.")
self.depth -= 1
else:
print "ping! the nail won't go deeper."
print("ping! the nail won't go deeper.")
class HammerTime(Place):
def __init__(self):
@ -38,38 +41,39 @@ class HammerTime(Place):
self.contents.append(Hammer())
self.contents.append(Nail())
@verb('look,l','none','none','none')
@verb("look,l", "none", "none", "none")
def look(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr):
if self.contents[1].depth > 0:
print "You see a nail sticking out ", self.contents[1].depth, "cm."
print("You see a nail sticking out ", self.contents[1].depth, "cm.")
else:
print "You see a nail fully hammered in."
print "You see a hammer."
print("You see a nail fully hammered in.")
print("You see a hammer.")
@verb('look,l', 'that','none','none')
@verb("look,l", "that", "none", "none")
def look_at(self, verbname, dobjstr, prepstr, iobjstr, dobj, iobj, argstr, stateobject=None):
if dobj:
print "%s: %s" % (dobj.name, dobj.description)
print("%s: %s" % (dobj.name, dobj.description))
else:
print "That doesn't appear to be here."
print("That doesn't appear to be here.")
hammertime = HammerTime()
game = Interpreter([], Player(), [hammertime,], hammertime)
game = Interpreter([], Player(), [hammertime], hammertime)
run = True
if __name__ == '__main__':
while (run):
cmd = ''
if __name__ == "__main__":
while run:
cmd = ""
try:
cmd = raw_input('>')
cmd = input(">")
except EOFError:
run = False
if cmd.startswith('quit'):
run = False
if cmd.startswith("quit"):
run = False
else:
try:
game.interpret(cmd)
except PyooVerbNotFound:
print "I don't understand that."
print("I don't understand that.")
print "Bye!"
print("Bye!")

@ -0,0 +1,8 @@
[tox]
envlist = py36, py37
[testenv]
deps =
flake8
mypy
commands = flake8 --max-line-length=120
Loading…
Cancel
Save