# gamebase.py import re as _re import heapq as _hq import gamemap as _gm import gameevents as _ge import random as _ra import os as _os 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 : {thingID : thing}} self.singletons = {} # {thingName : thing} self.prefabs = {} # {thingName : 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: _gt.Thing, loc: _gl.Locus, speed: float, action = None, immediate = False, closeEnough = True): """Move a thing smoothly at a constant speed. actor should be a Thing. loc should be a locus. speed should be a number. action should be a callable with 0 arguments. A lambda would be good here. If immediate is True, the go event would happen immediately. closeEnough should be True only if the path is to a non-passable point locus.""" if not isinstance(actor, _gt.Thing): raise TypeError("The actor passed to smoothMove must be a Thing.") if not isinstance(loc, _gl.Locus): raise TypeError("The locus passed to smoothMove must be a Locus.") 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, closeEnough) #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 dist == 0: # We should try to avoid this branch, because it means an error happened. self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed)) return elif immediate: self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc, closeEnough)) return speed * (dist - 1) else: self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, closeEnough, 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.") if len(args) == 0: print(f"{self.player.name} goes nowhere.", file = self.outstream) return 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: # no arguments: print the level description 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 thing == None: print("There is nothing to see here.", file = self.outstream) return elif thing in self.player.inventory: print(self.justifyText(str(thing)), file = self.outstream) return elif 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), closeEnough = False) else: print("{} cannot see that.".format(self.player.name), 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(f'{self.player.name} says, "Hello!" Nobody responds.', file = self.outstream) return 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 thing == None: if len(args) > 0: print(f"There is nobody named {' '.join(args)}.", file = self.outstream) else: print(f"There is nobody there.", file = self.outstream) return elif 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), closeEnough = False) else: print("{} cannot talk to {}.".format(self.player.name, thing.name), 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.") if len(args) == 0: print(f"{self.player.name} pokes the air as if pressing a button.", file = self.outstream) return 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. if thing.thingType == 'i': # it must be in inventory to pass the last check. self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs)) else: self.smoothMove(self.player, _gl.PointLocus(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') if len(args[:onIndex]) == 0: # just starts with 'use on ...' print(f"{self.player.name} tries to use nothing, but nothing happens.", file = self.outstream) return if len(args[onIndex + 1:]) == 0: # just ends with '... on' print(f"{self.player.name} tries to use the {' '.join(args[:onIndex])} on nothing, but nothing happens.", file = self.outstream) return 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, _gl.PointLocus(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)), closeEnough = False) 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 len(args) == 0: print(f"{self.player.name} reaches out and grasps at nothing.", file = self.outstream) return 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, _gl.PointLocus(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 len(args) == 0: print(f"{self.player.name} falls over.", file = self.outstream) return if args[0] == 'the': args.pop(0) thingName = ' '.join(args) if thingName in self.player.thingNames: self.setEvent(0.0, _ge.DropEvent(self.player, 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 self.clearEvents() # load the new level if len(args) == 2: self.level, self.nextThing = _gm.GameMap.read(args[0], int(args[1]), self.singletons, self.prefabs, preLoaded, self.nextThing) else: self.level, self.nextThing = _gm.GameMap.read(args[0], None, self.singletons, self.prefabs, 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.prefabs, 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.prefabs, 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] try: f = open(fileName, 'wb') _pi.dump(data, f, protocol=prot) except FileNotFoundException as err: _os.mkdir('saves') try: f = open(fileName, 'wb') _pi.dump(data, f, protocol=prot) except OSError as newErr: print("Save failed: {}".format(newErr), file = self.outstream) # 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) print('\n'.join(_os.listdir('./saves')), file = self.outstream) return # pickle protocol 4 for Python 3.4.0 to 3.7.x prot = 4 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], _gs.ScriptEnvironment(self.customValues, {}))): ret = self.runDialog(case) break elif len(cond) == 3 and _gs.compareValues(cond, _gs.ScriptEnvironment(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, _gs.ScriptEnvironment(self.customValues, {})): options.append(ans[condEnd+1:].strip()) j += 1 else: skips.append(i) else: options.append(ans) j += 1 answer = self.getIO('respondDialog')(options) # 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[5:]) 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.Prefab) 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) self.prefabs = {} if 'prefabs' in data: for thing in data['prefabs']: if not isinstance(thing, _gt.Thing): print("Non-thing in prefabs, 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.prefabs, 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.prefabs, self.nextThing + 1) # for prefabs, we don't add their items to their "real" inventory and don't delete the temporary one. self.prefabs[thing.name] = thing # 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. #print(data) 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.prefabs, 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.prefabs, self.nextThing + 1) thing.addThing(i) del thing.tempInventory self.singletons[thing.name] = thing #print(self.singletons) 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] ret = False if e.eventType in self.__gameEvents: for i in self.__gameEvents[e.eventType]: ret = ret or i(e) self.observe(e) # An event can still be observed even if it has no callback. if ret: break if len(self.eventQueue) == 0: self.gameTime = 0.0 _ge.resetEventNum() self.skipLoop = True def setEvent(self, timeFromNow: float, event: _ge.GameEvent, skip = False): _hq.heappush(self.eventQueue, (self.gameTime + timeFromNow, event)) 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.closeEnough, 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, True, e.closeEnough) 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) #print ("action = None") if thing: #print("thing != None") if thing.thingType == 'x': #print("thing is exit") if e.actor == self.player: #print("actor = player") self.parseScript(thing.onUse) if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)): #print("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)), file = self.outstream) self.level.removeThing(actor.name) else: e.action() return False # so that use works. #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: _ge.UseEvent): """Called when a UseEvent needs processed.""" 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: _ge.UseOnEvent): """Called when a UseOnEvent needs processed. It calls the item's useOnFunc in the useFuncs map with the item, target, and arguments.""" 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.target, 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.prefabs, self.nextThing, True) # nextThing shouldn't change e.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)) # Start out with a cursor at 0. if not 'cursor' in thing.customValues: thing.customValues['cursor'] = 0 # With 'single', the cursor will not be modified. if thing.customValues['pattern'] == 'single': if isinstance(thing.customValues['text'], str): print(self.justifyText(str(thing.customValues['text'])), file = self.outstream) elif isinstance(thing.customValues['text'], list): print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) # With 'loop', the given strings will keep looping. elif thing.customValues['pattern'] == 'loop': cursor = thing.customValues['cursor'] print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text']) # With 'once', the strings will be used one by one, until it gets to the end. The last one is then repeated. 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, key : _gt.Item, door : _gt.Door, args): """Item is a key, which unlocks a door. This may be implemented for containers later.""" if isinstance(door, tuple) or door.thingType != 'd': print("That is not a door.", file = self.outstream) return 0.0 if door.lock(key.name): print("The key fits the lock.", file = self.outstream) if not door.passable and self.player.x == door.x and self.player.y == door.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.background if fgc == None: fgc = _gt.Item.defaultGraphic.foreground if shape == None: shape = _gt.Item.defaultGraphic.shape 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:': cvEnv = customValues equalsign = i.index('=') cv = i[3:equalsign] ns = cv.find(':') while ns != -1: cvEnv[cv[1:ns]] = {} cvEnv = cvEnv[cv[1:ns]] cvEnv[cv] = self.getValueFromString(i[equalsign+1:]) thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(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.background if fgc == None: fgc = _gt.Useable.defaultGraphic.foreground if shape == None: shape = _gt.Useable.defaultGraphic.shape 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, _gt.ThingGraphic(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.background if fgc == None: fgc = _gt.NPC.defaultGraphic.foreground if shape == None: shape = _gt.NPC.defaultGraphic.shape 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, _gt.ThingGraphic(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.background if fgc == None: fgc = _gt.Door.defaultGraphic.foreground if shape == None: shape = _gt.Door.defaultGraphic.shape 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, _gt.ThingGraphic(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.background if fgc == None: fgc = _gt.MapExit.defaultGraphic.foreground if shape == None: shape = _gt.MapExit.defaultGraphic.shape 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, _gt.ThingGraphic(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) elif args[0].casefold() == 'singleton': if name in self.singletons: single = self.singletons[name] single.x, single.y = x, y single.prevx, single.prevy = x, y else: raise GameError("{} not a valid thing type.".format(args[0])) self.nextThing = self.level.addThing(thing, self.prefabs, 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.player.x, self.player.y fgc, bgc, shape = _gt.Item.defaultGraphic.foreground, _gt.Item.defaultGraphic.background, _gt.Item.defaultGraphic.shape description = 'A nondescript item.' persist = True useFunc = '' useOnFunc = '' customValues = {} ranged = False for i in args[0:]: 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, _gt.ThingGraphic(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): return 0 def wander(self, actor): return 0 def follow(self, actor, event): # make sure we only follow who we want to if "follow" not in actor.customValues: return 0 if "isFollowing" not in actor.customValues["follow"]: return 0 if actor.customValues["follow"]["isFollowing"]: if event.actor.name != actor.customValues["follow"]["target"]: if actor.customValues["follow"]["target"] == '': # This is a quick-and-dirty fix for an issue I was having with followers. actor.customValues["follow"]["target"] = actor 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)), closeEnough = False) return -1 else: return 0 # 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] # |\_/| # /0 0\ # \o/