gameshell/gamebase.py
2019-10-15 18:40:50 -04:00

1159 lines
53 KiB
Python

# gamebase.py
import re as _re
import heapq as _hq
import gamemap as _gm
import gameevents as _ge
import random as _ra
import sys as _sys
import pickle as _pi
import ruamel.yaml as _yaml
import textwrap as _tw
import gamethings as _gt
import gamelocus as _gl
import gamesequence as _gs
import gameutil as _gu
class GameError(RuntimeError):
pass
#class _ScriptBreak(object):
#
# def __init__(self, value):
# self.value = value
class GameBase(object):
#coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))')
#coordRegex1 = _re.compile(r'(-?[a-zA-Z]+) ?(-?[0-9]+)') # "B12" or "B 12"
#coordRegex2 = _re.compile(r'\(([0-9]+), ([0-9]+)\)') # "(2, 12)"
#coordRegex3 = _re.compile(r'(-?[0-9]+),? (-?[0-9]+)') # "2 12" or "2, 12"
def __init__(self):
self.outstream = _sys.stdout
self.__useFuncs = {}
self.__behaviors = {}
self.__gameEvents = {}
self.__IOCalls = {} # {str : function}
self.__scripts = {} # functions with the same signature, but not callable by the user
self.customValues = {} # for setting flags and such
self.level = None
self.persist = {} # {level : {thingName : thing}}
self.singletons = {} # {thingID : thing}
self.ps2 = '? '
self.eventQueue = []
self.gameTime = 0.0
self.skipLoop = True
self.nextThing = 1
# player info
self.playerName = 'You'
self.playerDescription = 'The main character.'
self.player = None # reference to the player's 'thing'
self.autoMove = True
# function deligates
self.onLevelLoad = None # str : level name? -> None
# default events and useFuncs
self.registerEvent('noop', self.handleNoOp)
self.registerEvent('go', self.handleGo)
self.registerEvent('arrive', self.handleArrive)
self.registerEvent('use', self.handleUse)
self.registerEvent('useon', self.handleUseOn)
self.registerEvent('take', self.handleTake)
self.registerEvent('drop', self.handleDrop)
self.registerEvent('behave', self.handleBehave)
self.registerUseFunc('examine', self.examine)
self.registerUseFunc('key', self.key)
self.registerUseFunc('container', self.container)
self.registerUseFunc('info', self.info)
self.registerUseFunc('wear', self.wear)
self.registerBehavior('stand', self.stand)
self.registerBehavior('wander', self.wander)
self.registerBehavior('follow', self.follow)
#self.registerScript('if', self.ifScript)
self.registerScript('printf', self.printfScript)
#self.registerScript('get', self.getCustomValue)
#self.registerScript('set', self.setCustomValue)
#self.registerScript('del', self.delCustomValue)
self.registerScript('spawn', self.spawnThing)
self.registerScript('give', self.giveToPlayer)
self.registerScript('move', self.moveThingScript)
self.registerScript('cvget', self.cvget)
self.registerScript('cvmod', self.cvmod)
self.registerScript('cvdel', self.cvdel)
self.registerScript('playercmd', self.playerCmdScript)
# Helper functions
def requestInput(self, prompt = ''):
"""Like input by default, but should be overridden if you don't want
to get input from stdin."""
return input(prompt)
def justifyText(self, text, width = 80):
ret = []
text = text.lstrip().rstrip()
# start by making it all one long line.
text = _re.sub(r'\s{2,}', r' ', text)
# then add newlines as needed.
#i = 80
#i = get_terminal_size()[0]
i = width
while i < len(text):
while text[i] != ' ':
i -= 1
ret.append(text[:i])
text = text[i+1:]
ret.append(text)
return '\n'.join(ret)
def smoothMove(self, actor, loc, speed, action = None, immediate = False):
"""Move a thing smoothly at a constant speed."""
if (actor.x, actor.y) in loc:
self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed))
return
dist, path, endPoint = self.level.path(actor.x, actor.y, loc)
#print(path)
if dist == -1:
print('{0} cannot reach there.'.format(actor.name), file = self.outstream)
return
elif dist == 1:
self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed))
return speed
elif immediate:
self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc))
return speed * (dist - 1)
else:
self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, timeTaken = speed))
return speed * dist
# commands
def wait(self, args):
"""wait seconds"""
if len(args) < 1:
return
self.setEvent(float(args[0]), _ge.NoOpEvent())
def go(self, args):
"""go [-r] [to [the]] destination [additional arguments...]
Go to a location. "walk" and "move" are aliases of go.
-r: run to the location. "r" and "run" are aliases of -r.
The "run" command is an alias of "go -r".
"to" and "the" do nothing, and are intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "go to the balcony" rather than just "go balcony").
Destination can be a coordinate pair or object name. For instance, if one
wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6".
The letter is not case-sensitive."""
if self.level == None:
raise GameError("Cannot move: No level has been loaded.")
if self.player == None:
raise GameError("Cannot move: Player character doesn't exist.")
speed = 0.6666667
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333
args.pop(0)
if args[0] == 'to':
args.pop(0)
if args[0] == 'the':
args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args)
# Now we have a heading! Let's see if we can get there...
self.smoothMove(self.player, _gl.PointLocus(x, y), speed)
return
def look(self, args):
"""look [at [the]] object
Describe an object.
"at" and "the" do nothing, and are intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "look at the balcony" rather than just "look balcony").
Object can be the name of the object, or its coordinates."""
if self.level == None:
raise GameError("Cannot look: No level has been loaded.")
if self.player == None:
raise GameError("Cannot look: Player character doesn't exist.")
if len(args) == 0:
print(self.justifyText(self.level.description), file = self.outstream)
else:
if args[0] == 'at':
args.pop(0)
if args[0] == 'the':
args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False, player = self.player)
if not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream))
else:
print("{} cannot see that.".format(self.player.name), file = self.outstream)
return
elif thing == None:
print("There is nothing to see here.\n", file = self.outstream)
return
else:
print(self.justifyText(str(thing)), file = self.outstream)
def talk(self, args):
"""talk [to [the]] character
Talk to a character.
"to" and "the" do nothing, and are intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "talk to the guard" rather than just "talk guard").
Character can be the name of the character, or their coordinates."""
if self.level == None:
raise GameError("Cannot talk: No level has been loaded.")
if self.player == None:
raise GameError("Cannot talk: Player character doesn't exist.")
if len(args) == 0:
print(self.justifyText(self.level.description), file = self.outstream)
else:
if args[0] == 'to':
args.pop(0)
if args[0] == 'the':
args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False)
if not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog))
else:
print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream)
return
elif thing == None:
print("There is nobody here.\n", file = self.outstream)
return
else:
_gu.startDialog(thing, self.outstream, self.dialog)
def use(self, args):
"""use [-r] [the] object [on [the] object2]
Use an object. If the player is not already close to it, they will go to it.
use [-r] [the] item on [the] object
Use an item from the player's inventory on an object.
-r: run to the location. "r" and "run" are aliases of -r.
"the" does nothing, and is intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "use the lever" rather than just "use lever").
Object can be the name of the object, or its coordinates. It can also be
the name of an item in the player's inventory."""
if self.level == None:
raise GameError("Cannot use: No level has been loaded.")
if self.player == None:
raise GameError("Cannot use: Player character doesn't exist.")
speed = 0.6666667
useArgs = []
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333
args.pop(0)
if args[0] == 'the':
args.pop(0)
if 'on' in args:
self.useOn(args, speed)
return
if 'with' in args:
useArgs = args[args.index('with')+1:]
thing, x, y = _gu.parseCoords(self.level, args, player = self.player)
if thing == None:
print("There is nothing to use.", file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
if thing.thingType != 'u' and thing not in self.player.inventory:
print("The {0} cannot be used.".format(thing.name), file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
# Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs)))
return
def useOn(self, args, speed):
"""Called by use when there is an 'on' clause"""
onIndex = args.index('on')
item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player)
if args[onIndex+1] == 'the':
onIndex += 1
thing, x, y = _gu.parseCoords(self.level, args[onIndex+1:], usePlayerCoords = True, player = self.player)
useArgs = []
if 'with' in args:
useArgs = args[args.index('with')+1:]
if item == None or item not in self.player.inventory:
print("There is no such item in the inventory.", file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
if thing == None and x < 0 and y < 0:
print("Argument contains 'to' but with no real predicate.", file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
if not item.ranged:
# Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)))
else:
if not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)))
else:
print("{} cannot see that.".format(self.player.name), file = self.outstream)
return
def take(self, args):
"""take [the] item
Take an item. 'get' is an alias of take.
If the player is not already close to it, they will go to it.
"the" does nothing, and is intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "take the flask" rather than just "take flask").
Object can be the name of the object, or its coordinates."""
if self.level == None:
raise GameError("Cannot take: No level has been loaded.")
if self.player == None:
raise GameError("Cannot take: Player character doesn't exist.")
speed = 0.6666667
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333
args.pop(0)
if args[0] == 'the':
args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args)
if thing == None:
print("There is nothing to take.", file = self.outstream)
return
if thing.thingType != 'i':
print("The {0} cannot be taken.".format(thing.name), file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
# Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing)))
def drop(self, args):
"""drop [the] item"""
if self.level == None:
raise GameError("Cannot drop: No level has been loaded.")
if self.player == None:
raise GameError("Cannot drop: Player character doesn't exist.")
if args[0] == 'the':
args.pop(0)
thingName = ' '.join(args)
if thingName in self.player.thingNames:
self.setEvent(0.0, _ge.DropEvent(self.player.getThingByName(thingName)))
else:
print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream)
def loadMap(self, args):
# loadMap (fileName)
# loadMap (fileName, entrance)
# loadMap (fileName, x, y)
# save persistent things in previous level
if self.level != None:
if self.level.name not in self.persist:
self.persist[self.level.name] = {}
for i in self.level.persistent:
self.persist[self.level.name][i] = self.level.getThingByID(i)
preLoaded = False
if args[0] in self.persist:
preLoaded = True
# load the new level
if len(args) == 2:
self.level, self.nextThing = _gm.GameMap.read(args[0], int(args[1]), self.singletons, preLoaded, self.nextThing)
else:
self.level, self.nextThing = _gm.GameMap.read(args[0], None, self.singletons, preLoaded, self.nextThing)
if self.level == None:
raise GameError("Map could not be loaded.")
# get persistent things from it
if args[0] in self.persist:
persistedThings = tuple(self.persist[args[0]].keys())
for i in persistedThings:
self.nextThing = self.level.addThing(self.persist[args[0]][i], self.nextThing, True) #nextThing shouldn't change
del self.persist[args[0]][i] # delete them from the persist dict to prevent item duplication
print(self.level.openingText, file = self.outstream)
#print(self.outstream.getvalue())
mx, my = self.level.playerStart
if len(args) == 3:
mx, my = int(args[1]), int(args[2])
if self.player == None:
# create a player to put in the level.
self.player = _gt.PlayerCharacter(x = mx, y = my, description = self.playerDescription,
inventory = {}, customValues = {}, name = self.playerName)
#print("Player created.")
else:
self.player.x, self.player.y = mx, my
#print("Player moved.")
self.player.prevx, self.player.prevy = self.player.x, self.player.y
self.nextThing = self.level.addThing(self.player, self.nextThing) # The player needs to be added to the new level.
if self.onLevelLoad != None:
self.onLevelLoad()
self.parseScript(self.level.enterScript)
def unloadMap(self, args):
"""Args is only there just in case."""
self.level = None
self.player = None
def saveGame(self, args):
if len(args) < 1:
print("Save file must have a name!", file = self.outstream)
return
# choose pickle protocol depending on python version:
# 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x
prot = _pi.HIGHEST_PROTOCOL
# save persistent things so that the current level can be recalled as-is
if self.level != None:
if self.level.name not in self.persist:
self.persist[self.level.name] = {}
for i in self.level.persistent:
self.persist[self.level.name][i] = self.level.getThingByID(i)
# build data object to be saved
data = (self.player, self.level.name, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing)
# save it!
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
if args[0].endswith('.dat'): # This is really for absolute paths, but doesn't really check for that.
fileName = args[0]
with open(fileName, 'wb') as f:
_pi.dump(data, f, protocol=prot)
# delete things in the current map from the persist dict to prevent item duplication
persistedThings = tuple(self.persist[self.level.name].keys())
for i in persistedThings:
del self.persist[self.level.name][i]
# push a no-op event so that saving doesn't cost player characters time
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
def loadGame(self, args):
if len(args) < 1:
print("Save file must have a name!", file = self.outstream)
return
# choose pickle protocol depending on python version:
# 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x
prot = _pi.HIGHEST_PROTOCOL
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
if args[0].endswith('.dat'):
fileName = args[0]
x, y, levelname = 1, 1, 'testing/test1.txt'
with open(fileName, 'rb') as f:
self.player, levelname, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing = _pi.load(f)
#print(levelname, x, y, file = self.outstream)
self.loadMap((levelname, self.player.x, self.player.y))
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
def dialog(self, dialogName, conversant = None):
yaml = _yaml.YAML()
dialog = None
with open(dialogName, 'r') as f:
dialog = yaml.load(f)
self.getIO('startDialog')()
self.runDialog(dialog)
self.getIO('endDialog')()
def runDialog(self, dialogObj):
#print(dialogObj)
if 'script' in dialogObj:
self.parseScript(dialogObj['script'])
if 'cond' in dialogObj:
cases = dialogObj['cond']
ret = 0
for case in cases:
cond = case['case'].split() # should be list like ["value", "==", 1]
if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], {'scriptLocal': {}, 'global': self.customValues})):
ret = self.runDialog(case)
break
elif len(cond) == 3 and _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}):
ret = self.runDialog(case)
break
else:
raise GameError("All routes are false: {}".format(testValue))
if ret > 1:
return ret - 1
else:
return ret
# if ret == 1 or ret == 0, go back again.
elif 'opener' in dialogObj:
self.getIO('openDialog')(dialogObj['opener'].split('\n')) # split by lines so that fill doesn't erase newlines
while isinstance(dialogObj, dict):
if 'action' in dialogObj:
action = dialogObj['action']
if action == 'answer':
answer = 0
skips = []
j = 0 # follower to i
options = []
if 'answers' in dialogObj and isinstance(dialogObj['answers'], list):
for i in range(len(dialogObj['answers'])):
ans = dialogObj['answers'][i]
if ans[0] == '?':
condEnd = ans.index(':')
cond = ans[1:condEnd].strip().split()
if _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}):
options.append(ans[condEnd+1:].strip())
#print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE))
j += 1
else:
skips.append(i)
else:
options.append(ans)
#print(_tw.fill('{}: {}'.format(j+1, ans), width = TERM_SIZE))
j += 1
answer = self.getIO('respondDialog')(options)
#print(answer)
#answer = int(input(self.ps2)) - 1
# account for offset if there were answer options that didn't meet conditions
for i in skips:
if i <= answer:
answer += 1
else:
break
if 'replies' in dialogObj and isinstance(dialogObj['replies'], list):
ret = self.runDialog(dialogObj['replies'][answer])
if ret > 1:
return ret - 1
elif ret == 0:
return 0
# if ret == 1, then do this dialog again
elif len(action) >= 4 and action[:4] == 'back':
if len(action) == 4:
return 1
return int(action[4:])
elif action == 'exit':
return 0
else:
raise RuntimeError('Malformed action: {}'.format(action))
else:
raise RuntimeError("Dialog branch with neither switch nor openner.")
def parseScript(self, instr: str):
"""A thin wrapper for _gs.parseScript."""
return _gs.parseScript(instr, self.customValues, self.__scripts)
def loadGameData(self, dataFile):
yaml = _yaml.YAML()
yaml.register_class(_gt.Item)
yaml.register_class(_gt.Useable)
yaml.register_class(_gt.NPC)
yaml.register_class(_gt.Door)
yaml.register_class(_gt.MapExit)
yaml.register_class(_gt.MapEntrance)
data = None
with open(dataFile, 'r') as f:
data = yaml.load(f)
# In this context, 'singleton' means a 'thing' that can be accessed by
# any map. This is useful for when you have major characters who will
# show up in many scenes, and their inventory must stay the same, for
# example.
self.singletons = {}
if 'singletons' in data:
for thing in data['singletons']:
#thing = data['singletons'][datum]
if not isinstance(thing, _gt.Thing):
print("Non-thing in singletons, ignoring.\n", _sys.stderr)
continue
thing.thingID = self.nextThing
self.nextThing += 1
if thing.thingType in 'iun':
self.nextThing = _gm.GameMap.addThingRecursive(thing.customValues, self.nextThing)
if thing.thingType == 'n':
for i in thing.tempInventory:
if i.thingID == -1:
i.thingID = self.nextThing
self.nextThing = _gm.GameMap.addThingRecursive(i.customValues, self.nextThing + 1)
thing.addThing(i)
del thing.tempInventory
self.singletons[thing.name] = thing
return data
def gameEventLoop(self):
#print(self.skipLoop)
if self.skipLoop:
return
#print(self.skipLoop)
while len(self.eventQueue) > 0:
ev = _hq.heappop(self.eventQueue)
self.gameTime = ev[0]
e = ev[1]
if e.eventType not in self.__gameEvents:
raise GameError("Unhandled event.")
ret = False
for i in self.__gameEvents[e.eventType]:
ret = ret or i(e)
self.observe(e)
if ret:
break
if len(self.eventQueue) == 0:
self.gameTime = 0.0
_ge.resetEventNum()
self.skipLoop = True
def setEvent(self, t, e, skip = False):
_hq.heappush(self.eventQueue, (self.gameTime + t, e))
self.skipLoop = skip
def clearEvents(self, actor = None):
if isinstance(actor, _gt.Thing):
i = 0
while i < len(self.eventQueue):
if actor is self.eventQueue[i].actor:
del self.eventQueue[i]
else:
i += 1
_hq.heapify(self.eventQueue)
else:
self.eventQueue.clear()
# default event handlers
def handleNoOp(self, e):
return True
def handleGo(self, e):
if e.actor == None:
raise GameError("'Go' event raised for no object.")
if self.level.isPassable(e.path[0]):
#print(e.actor.name, e.actor.x, e.actor.y)
self.level.moveThing(e.actor, e.path[0])
#print(e.actor.x, e.actor.y)
#print(e.action)
if len(e.path) > 1:
self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.timeTaken + e.speed))
else:
self.setEvent(e.speed, _ge.ArriveEvent(e.actor, e.path[0], e.speed, e.action, e.locus, e.timeTaken + e.speed))
elif isinstance(e.locus, _gl.Locus):
self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True)
else:
print('{0} cannot !reach there.'.format(e.actor.name), file = self.outstream)
return False
def handleArrive(self, e):
if e.actor == None:
raise GameError("'Go' event raised for no object.")
#print(e.actor, "arriving at", e.pos, self.level.isPassable(e.pos))
if self.level.isPassable(e.pos) or (e.actor.x, e.actor.y) == self.level.intToCoords(e.pos):
thing = self.level.getThingAtPos(e.pos)
self.level.moveThing(e.actor, e.pos)
if e.action == None:
#print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, _gu.numberToLetter(e.x), e.y, e.t), file = self.outstream)
if thing:
if thing.thingType == 'x':
if e.actor == self.player:
self.parseScript(thing.onUse)
if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)):
a = self.requestInput('Do you want to go {0}? (Y/n)'.format(str(thing)))
if a != 'n' and a != 'N':
self.loadMap((thing.destination, thing.exitid))
else:
print('{0} went {1}.'.format(actor.name, str(thing)))
self.level.removeThing(actor.name)
else:
e.action()
#elif isinstance(e.locus, _gl.Locus):
# self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True)
else:
# checking the locus again would inevitably fail, so we just give up.
print('{0} cannot @reach there.'.format(e.actor.name), file = self.outstream)
return e.actor == self.player
def handleUse(self, e):
if e.thing.useFunc == '':
print('The {0} cannot be used by itself.'.format(e.thing.name), file = self.outstream)
return True
self.setEvent(self.__useFuncs[e.thing.useFunc](e.thing, e.args), _ge.NoOpEvent())
return False
def handleUseOn(self, e):
if e.item.useOnFunc == '':
print('The {0} cannot be used on other objects.'.format(e.item.name), file = self.outstream)
return True
self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.thing, e.args), _ge.NoOpEvent())
return False
def handleTake(self, e):
if e.actor == None or e.actor.thingType not in 'np' or e.item == None:
raise GameError("'Take' event cannot be handled.")
self.level.removeThingByID(e.item.thingID)
e.actor.addThing(e.item)
return True
def handleDrop(self, e):
if e.actor == None or e.actor.thingType not in 'np':
raise GameError("'Drop' event cannot be handled.")
e.item.x = e.actor.x
e.item.y = e.actor.y
self.nextThing = self.level.addThing(e.item, self.nextThing, True) # nextThing shouldn't change
self.actor.removeThing(e.item)
return True
def handleBehave(self, e):
"""Allow an NPC to make another move."""
if not isinstance(e.actor, _gt.Observer):
raise GameError("Non-observer is trying to behave.")
if len(e.actor.behaviorQueue) > 0:
nextBehavior = _hq.heappop(e.actor.behaviorQueue)
timeTaken = self.__behaviors[e.actor.behaviors[nextBehavior[1].eventType][1]](e.actor, nextBehavior[1])
self.setEvent(timeTaken, _ge.BehaveEvent(e.actor))
else:
e.actor.busy = False
return False
# default useFuncs: take a list of arguments, return the time the use took
def examine(self, thing, args):
"""Just prints the given text."""
if 'pattern' not in thing.customValues or 'text' not in thing.customValues:
raise ValueError("Non-examinable thing {0} examined.".format(thing.name))
if thing.customValues['pattern'] == 'single':
print(self.justifyText(str(thing.customValues['text'])), file = self.outstream)
elif thing.customValues['pattern'] == 'loop':
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
cursor = thing.customValues['cursor']
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text'])
elif thing.customValues['pattern'] == 'once':
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
cursor = thing.customValues['cursor']
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
if cursor < len(thing.customValues['text']) - 1:
thing.customValues['cursor'] += 1
elif thing.customValues['pattern'] == 'random':
cursor = _ra.randrange(len(thing.customValues['text']))
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
thing.customValues['cursor'] = cursor
if 'set' in thing.customValues:
if 'cursor' in thing.customValues:
self.customValues[thing.customValues['set']] = thing.customValues[cursor]
else:
self.customValues[thing.customValues['set']] = True
return 0.0
def container(self, thing, args):
"""Acts as a container. Items can be traded between the container and the player's inventory."""
items = list(thing.customValues['items'])
thing.customValues['items'], timeOpen = self.getIO('container')(self.player, items)
return timeOpen
def info(self, thing, args):
"""Acts as a bookshelf, filing cabinet, bulletin board, or anything that contains raw info."""
items = dict(thing.customValues['items'])
return self.getIO('info')(items)
def wear(self, item, args):
"""Wear clotherg or otherwise equip a passive item."""
# An item must be in the player's inventory in order to wear it.
inInv = item in self.player.inventory
if not inInv:
print("You cannot wear what you are not carrying.", file = self.outstream)
return 0.0
if 'wearing' not in self.customValues:
self.customValues['wearing'] = {}
if item.customValues['slot'] not in self.customValues['wearing']:
self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(item)
# This is so a player can't put on a garment, then drop it while
# still also wearing it.
print("{} put on the {}.".format(self.player.name, item.name), file = self.outstream)
else: # the player must be wearing something that will get in the way
# put the old clothes back in the inventory
self.player.addThing(self.customValues['wearing'][item.customValues['slot']])
self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(item)
# This is so a player can't put on a garment, then drop it while
# still also wearing it.
print("{} took off the {} and put on the {}.".format(self.player.name, oldItem.name, item.name), file = self.outstream)
return 1.0
# item use-on functions
def key(self, item, thing, args):
"""Item is a key, which unlocks a door. This may be implemented for containers later."""
if isinstance(thing, tuple) or thing.thingType != 'd':
print("That is not a door.", file = self.outstream)
return 0.0
if thing.lock(item.name):
print("The key fits the lock.", file = self.outstream)
if not thing.passable and self.player.x == thing.x and self.player.y == thing.y:
self.player.x, self.player.y = self.player.prevx, self.player.prevy
else:
print("The key doesn't fit that lock.", file = self.outstream)
return 0.125
# default scripts
def printfScript(self, args):
"""Assume the first argument is a stringable object, and format all others."""
ret = args[0].format(*[self.getValueFromString(i) for i in args[1:]])
print(ret, file = self.outstream)
return ret
def spawnThing(self, args):
"""Spawns a thing. [type name=... location=... etc]"""
if args[0].casefold() not in ('item', 'useable', 'npc', 'door', 'mapexit', 'mapentrance'):
raise GameError("{} not a spawnable thing type.".format(args[0]))
# Get the standard fields that most Things have in common: name, coordinates, graphic.
name = None
x, y = -1, -1
fgc, bgc, shape = None, None, None
persist = False
for i in args[1:]:
if i[0:5].casefold() == 'name=':
name = i[5:]
elif i[0:9].casefold() == 'location=':
_, x, y = _gu.parseCoords(self.level, i[9:].split(), usePlayerCoords = False)
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[4] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[5:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
fgc = i[4:]
elif i[0:4].casefold() == 'bgc=':
if len(i[4:]) != 7 or i[4] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[5:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
bgc = i[4:]
elif i[0:6].casefold() == 'shape=':
if i[6] not in 'ox^ #|-':
raise GameError("Invalid shape: {}".format(i[6]))
shape = i[6]
elif i == 'persist':
persist = True
if x < 0 or y < 0:
raise GameError("Cannot spawn thing: bad location")
# Unfortunately, there needs to be a case for each type.
thing = None
if args[0].casefold() == 'item':
# spawn an item
description = 'A nondescript item.'
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
if name == None:
name = 'item {}'.format(self.nextThing)
if bgc == None:
bgc = _gt.Item.defaultGraphic[0]
if fgc == None:
fgc = _gt.Item.defaultGraphic[1]
if shape == None:
shape = _gt.Item.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
elif i[0:8].casefold() == 'usefunc=':
useFunc = i[8:]
elif i[0:10].casefold() == 'useonfunc=':
useOnFunc = i[10:]
elif i.casefold() == 'ranged':
ranged = True
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape))
elif args[0].casefold() == 'useable':
# spawn a useable thing
description = 'A nondescript useable thing.'
useFunc = ''
customValues = {}
playerx, playery = x, y
if name == None:
name = 'useable {}'.format(self.nextThing)
if bgc == None:
bgc = _gt.Useable.defaultGraphic[0]
if fgc == None:
fgc = _gt.Useable.defaultGraphic[1]
if shape == None:
shape = _gt.Useable.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
elif i[0:8].casefold() == 'usefunc=':
useFunc = i[8:]
elif i[0:10].casefold() == 'playerpos=':
_, playerx, playery = _gu.parseCoords(self.level, i[10:].split())
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape))
elif args[0].casefold() == 'npc':
# spawn an NPC
description = 'A nondescript character.'
behavior = 'stand'
inv = []
customValues = {}
playerx, playery = x, y
if name == None:
name = 'character {}'.format(self.nextThing)
if bgc == None:
bgc = _gt.NPC.defaultGraphic[0]
if fgc == None:
fgc = _gt.NPC.defaultGraphic[1]
if shape == None:
shape = _gt.NPC.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
elif i[0:9].casefold() == 'behavior=':
useFunc = i[9:]
elif i[0:10].casefold() == 'playerpos=':
_, playerx, playery = _gu.parseCoords(self.level, i[10:].split())
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, (bgc, fgc, shape))
elif args[0].casefold() == 'door':
# spawn a door
description = 'a nondescript door.'
locked = False
key = None
if name == None:
name = 'door {}'.format(self.nextThing)
if bgc == None:
bgc = _gt.Door.defaultGraphic[0]
if fgc == None:
fgc = _gt.Door.defaultGraphic[1]
if shape == None:
shape = _gt.Door.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
elif i.casefold() == 'locked':
locked = True
thing = _gt.Door(name, x, y, locked, description, key, (bgc, fgc, shape))
elif args[0].casefold() == 'mapexit':
# spawn an exit to another map (use with EXTREME caution!)
destination = ''
prefix = None
exitid = 0
onUse = ''
key = True
if name == None:
name = 'somewhere'
if bgc == None:
bgc = _gt.MapExit.defaultGraphic[0]
if fgc == None:
fgc = _gt.MapExit.defaultGraphic[1]
if shape == None:
shape = _gt.MapExit.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'destination=':
destination = i[12:]
elif i[0:7].casefold() == 'prefix=':
prefix = i[7:]
elif i[0:7].casefold() == 'exitid=':
exitid = int(i[7:])
elif i[0:6].casefold() == 'onuse=':
onUse = i[6:]
elif i[0:4].casefold() == 'key=':
key = i[4:]
thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, (bgc, fgc, shape))
elif args[0].casefold() == 'mapentrance':
# spawn a map entrance
exitid = 0
for i in args[1:]:
if i[0:7].casefold() == 'exitid=':
exitid = int(i[7:])
thing = _gt.MapEntrance(x, y, exitid, name)
else:
raise GameError("{} not a valid thing type.".format(args[0]))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
return thing
def giveToPlayer(self, args):
"""We get to assume it's an item."""
name = 'item {}'.format(self.nextThing)
x, y = self.playerx, self.playery
fgc, bgc, shape = _gt.Item.defaultGraphic['fgc'], _gt.Item.defaultGraphic['bgc'], _gt.Item.defaultGraphic['shape']
description = 'A nondescript item.'
persist = True
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
for i in args[1:]:
if i[0:5].casefold() == 'name=':
name = i[5:]
elif i[0:10].casefold() == 'location=':
_, x, y = _gu.parseCoords(self.level, i[10:].split(), usePlayerCoords = False)
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
fgc = i[4:]
elif i[0:4].casefold() == 'bgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
bgc = i[4:]
elif i[0:6].casefold() == 'shape=':
if len(i[6:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[6:]))
for j in i[6:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[6:]))
shape = i[6:]
elif i == 'persist':
persist = True
elif i[0:12].casefold() == 'description=':
description = i[12:]
elif i[0:8].casefold() == 'usefunc=':
useFunc = i[8:]
elif i[0:10].casefold() == 'useonfunc=':
useOnFunc = i[10:]
elif i.casefold() == 'ranged':
ranged = True
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape))
thing.thingID = self.nextThing
self.player.addThing(thing)
self.nextThing += 1
return thing
def moveThingScript(self, args):
colon = args.index(':')
thing, x, y = _gu.parseCoords(self.level, args[0:colon], usePlayerCoords = False)
if thing == None:
return False
_, newX, newY = self.parseCoords(args[colon+1:], usePlayerCoords = True, allowInventory = False)
self.level.moveThing(thing, newX, newY)
def cvget(self, args):
thing, x, y = _gu.parseCoords(self.level, args[0:-1], usePlayerCoords = False, player = self.player)
if thing != None and thing.thingType in 'ciu':
return _gs.getCustomValue(args[-1:], {'scriptLocal': thing.customValues, 'global': {}})
else:
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
def cvmod(self, args):
"""Modify a custom value in a thing.
args = thing identifier operator value"""
thing, x, y = _gu.parseCoords(args[0:-3], usePlayerCoords = False, player = self.player)
if thing != None and thing.thingType in 'ciu':
return _gs.setCustomValue(args[-3:], {'scriptLocal': thing.customValues, 'global': {}})
else:
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
def cvdel(self, args):
"""Delete custom values."""
colon = args.index(':')
thing, x, y = _gu.parseCoords(args[0:colon], usePlayerCoords = False, player = self.player)
if thing != None and thing.thingType in 'ciu':
return _gs.delCustomValue(args[colon+1:], {'scriptLocal': {}, 'global': thing.customValues})
else:
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
def playerCmdScript(self, args):
"""Run a player command from a script."""
self.getIO('playercmd')(line[1:])
return False
# Observer
def observe(self, e):
if self.level == None:
# Maybe not an error? Not sure yet.
return
for thingID in self.level.things:
thing = self.level.things[thingID]
if isinstance(thing, _gt.Observer):
if e.eventType in thing.behaviors:
if not thing.busy:
thing.busy = True
timeTaken = self.__behaviors[thing.behaviors[e.eventType][1]](thing, e)
if timeTaken >= 0:
self.setEvent(timeTaken, _ge.BehaveEvent(thing))
# We assume that otherwise, the behavior itself decides when it's done.
else:
# negative priority means just forget it;
# otherwise, lower number means higher priority
if thing.behaviors[e.eventType][0] >= 0:
#print("'{}' event added to {}'s queue".format(e.eventType, thing.name))
_hq.heappush(thing.behaviorQueue, (thing.behaviors[e.eventType][0], e))
# behaviors
def stand(self, actor):
pass
def wander(self, actor):
pass
def follow(self, actor, event):
# make sure we only follow who we want to
if event.actor.name != actor.customValues["follow"]["target"]:
return 0
else:
dest = 0
#print(event.eventType)
if event.eventType == 'go':
dest = event.path[-1]
elif event.eventType == 'arrive':
dest = event.pos
x, y = self.level.intToCoords(dest)
self.smoothMove(actor, _gl.CircleLocus(x, y, 2), event.speed, action = lambda: self.setEvent(0, _ge.BehaveEvent(actor)))
return -1
# stuff for extended classes to use
def registerUseFunc(self, name, func):
"""Registers a function for use by things in the map, but not directly
callable by the player."""
self.__useFuncs[name] = func
def registerBehavior(self, name, func):
"""Registers a function for use as an NPC's behavior."""
self.__behaviors[name] = func
def registerEvent(self, name, func):
"""Registers a function to handle an event in the event loop.
They should take the event as an argument, and return True if it should
always give the player a turn, False otherwise."""
if name in self.__gameEvents:
self.__gameEvents[name].add(func)
else:
self.__gameEvents[name] = set((func,))
#self.__gameEvents[name] = func
def registerIO(self, name, func):
"""Registers a function for useFuncs and such to use."""
self.__IOCalls[name] = func
def registerScript(self, name, func):
"""Registers a function as a script callable from yaml files."""
self.__scripts[name] = func
def getIO(self, name):
"""This is so derived classes can access their IOCalls."""
if name not in self.__IOCalls:
raise GameError("No IO call for {}.".format(name))
return self.__IOCalls[name]