# 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 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.ps2 = '? ' self.eventQueue = [] self.gameTime = 0.0 self.skipLoop = True self.nextThing = 0 # player info self.playerName = 'You' self.playerDescription = 'The main character.' self.player = None # reference to the player's 'thing' # function deligates self.onLevelLoad = None # str : level name? -> None #self.onContainer = None # list : contents -> list : newContents, float : timeOpen # 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.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) # 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 letterToNumber(self, letter): if letter.isdecimal(): return int(letter) ret = 0 sign = 1 start = 0 for i in range(len(letter)): if letter[i] == '-': sign = -sign start -= 1 elif letter[i].isalpha(): if letter[i].isupper(): ret += (ord(letter[i]) - ord('A')) * (26**(i+start)) else: ret += (ord(letter[i]) - ord('a')) * (26**(i+start)) else: return ret * sign return ret * sign def numberToLetter(self, number): if isinstance(number, str): return number ret = '' sign = '' if number == 0: return 'A' elif number < 0: sign = '-' number = -number while number > 0: ret += chr(ord('A') + number % 26) number = int(number / 26) return sign + ret def parseCoords(self, args, usePlayerCoords = True, allowInventory = True): """Takes an argument list, and figures out what it's talking about. Returns (thing, x, y). "Thing" can be None.""" if self.level == None: raise GameError('Coordinates cannot be parsed because there is no map.') #print(coordStr) x = -1 y = -1 # first, try by name for objects in the map name = ' '.join(args) thing = self.level.getThingByName(name) if thing != None: if usePlayerCoords: return thing, thing.playerx, thing.playery else: return thing, thing.x, thing.y # Second, try by name for objects in the inventory if allowInventory: if name in self.player.thingNames: return self.player.getThingByName(name), -1, -1 # Third, try by location coordStr = args[0] if len(args) > 1: coordStr += ' ' + args[1] match = GameBase.coordRegex.match(coordStr) if match != None: # if coordinates were given #print(match.group()) groups = match.groups() ind = 0 for i in range(len(groups)): if groups[i] == None or groups[i] == match.group(): continue else: ind = i break #print(groups[ind], groups[ind+1]) x = self.letterToNumber(groups[ind]) y = self.letterToNumber(groups[ind+1]) #print(x, y) thing = self.level.getThingAtCoords(x, y) if thing != None and usePlayerCoords: return thing, thing.playerx, thing.playery else: return thing, x, y else: # if a name was given return None, -1, -1 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 getValueFromString(self, arg: str, env = None): if env == None: env = self.customValues val = None validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg) if arg[0] in '"\'' and arg[-1] == arg[0]: # assume it's a string val = arg[1:-1] elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None: if '.' in arg: val = float(arg) else: val = int(arg) elif arg.casefold() == 'true': val = True elif arg.casefold() == 'false': val = False elif arg.casefold() == 'playerx' and self.player != None: val = self.player.x elif arg.casefold() == 'playery' and self.player != None: val = self.player.y elif arg.casefold() == 'playername' and self.player != None: val = self.player.name elif arg.casefold() == 'playerinv' and self.player != None: val = self.player.inventory elif arg.casefold() == 'playerdesc' and self.player != None: val = self.player.description elif validIdent != None: group = validIdent.group() if 'scriptLocal' in self.customValues and group in self.customValues['scriptLocal']: val = self.customValues['scriptLocal'][group] elif group in env: val = env[group] else: return False # for if statements; if a variable doesn't exist, should evaluate to False # evaluate all values of all indecies openBracket = validIdent.end() if openBracket < len(arg): if arg[openBracket] == '[': ptr = openBracket depth = 0 while ptr < len(arg): if depth == 0 and arg[ptr] != '[': raise GameError('Invalid value syntax: {}'.format(arg)) if arg[ptr] == '[': depth += 1 elif arg[ptr] == ']': depth -= 1 if depth == 0: index = self.getValueFromString(arg[openBracket+1:ptr]) if index in val: val = val[index] else: return False openBracket = ptr + 1 ptr += 1 else: raise GameError('Invalid value syntax: {}'.format(arg)) else: raise GameError('Invalid argument to getValueFromString: {}'.format(arg)) return val def compareValues(self, args: list): """Generalized comparisons, may eventually be extended to other operators""" if len(args) == 1: return bool(self.getValueFromString(args[0])) elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): lval = self.getValueFromString(args[0]) operator = args[1] rval = self.getValueFromString(args[2]) if operator == '==': return lval == rval elif operator == '!=': return lval != rval elif operator == '<=': return lval <= rval elif operator == '>=': return lval >= rval elif operator == '<': return lval < rval elif operator == '>': return lval > rval elif operator == 'in': if args[2].casefold() == 'playerinv': return lval in self.player.thingNames else: return lval in rval else: raise GameError("Condition cannot be evaluated: {}".format(' '.join(args))) # commands 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 = self.parseCoords(args, allowInventory = False) # Now we have a heading! Let's see if we can get there... if (x, y) == (self.player.x, self.player.y): #_hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, x, y, 0.0))) self.setEvent(0.0, _ge.ArriveEvent(self.player, self.player.x, self.player.y, 0.0)) return dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: print('{0} cannot reach {1}{2}.'.format(self.player.name, self.numberToLetter(x), y), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] if space == -1: self.setEvent(0.0, _ge.ArriveEvent(self.player, self.player.x, self.player.y, 0.0)) return #target = self.level.coordsToInt(x, y) t = 1 #while space != target: while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] if space != -1: self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) else: self.setEvent(t * speed, _ge.ArriveEvent(self.player, newx, newy, t * speed)) break t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.ArriveEvent(self.player, newx, newy, t * 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 = self.parseCoords(args, usePlayerCoords = False) if not self.level.lineOfSight(self.player.x, self.player.y, x, y): print("{} cannot see that.".format(self.player.name), file = self.outstream) elif thing == None: print("There is nothing to see here.\n", file = self.outstream) else: print(self.justifyText(str(thing)), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) 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 = self.parseCoords(args, usePlayerCoords = False, allowInventory = False) if not self.level.lineOfSight(self.player.x, self.player.y, x, y): print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream) elif thing == None: print("There is nobody here.\n", file = self.outstream) else: if 'dialogs' in thing.customValues: if isinstance(thing.customValues['dialogs'], list): if 'dialogPtr' in thing.customValues and isinstance(thiong.customValues['dialogPtr'], int): self.dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing) else: self.dialog(thing.customValues['dialogs'][0], thing) elif isinstance(thing.customValues['dialogs'], str): self.dialog(thing.customValues['dialogs'], thing) else: raise GameError("Character '{}' has dialog of an unexpected type.".format(thing.name)) else: print("{} has nothing to say to you.".format(thing.name), file = self.outstream) 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 = self.parseCoords(args) 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 (x, y) == (self.player.x, self.player.y) or thing in self.player.inventory: self.setEvent(0.125, _ge.UseEvent(thing, useArgs)) return dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] #target = self.level.coordsToInt(x, y) t = 1 #while space != target: while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) self.setEvent(t * speed + 0.125, _ge.UseEvent(thing, useArgs)) return def useOn(self, args, speed): """Called by use when there is an 'on' clause""" onIndex = args.index('on') item, x, y = self.parseCoords(args[:onIndex], usePlayerCoords = False, allowInventory = True) if args[onIndex+1] == 'the': onIndex += 1 thing, x, y = self.parseCoords(args[onIndex+1:], usePlayerCoords = True, allowInventory = True) 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. if (x, y) == (self.player.x, self.player.y): self.setEvent(0.125, _ge.UseOnEvent(item, thing, useArgs)) return dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: if thing != None: print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) else: print('{0} cannot reach {1}{2}.'.format(self.player.name, self.numberToLetter(x), y), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] #target = self.level.coordsToInt(x, y) t = 1 #while space != target: while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) self.setEvent(t * speed + 0.125, _ge.UseOnEvent(item, thing, useArgs)) else: if not self.level.lineOfSight(self.player.x, self.player.y, x, y): print("{} cannot use the {} on that from here.".format(self.player.name, item.name), file = self.outstream) elif thing == None: print("There is nothing to use the {} on here.".format(item.name), file = self.outstream) else: self.setEvent(0, _ge.UseOnEvent(item, thing, useArgs)) 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 = self.parseCoords(args, allowInventory = False) 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. if (x, y) == (self.player.x, self.player.y): self.setEvent(0.125, _ge.TakeEvent(self.player, thing)) return dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] #target = self.level.coordsToInt(x, y) t = 1 #while space != target: while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.player.name, newx, newy))) self.setEvent(t * speed + 0.125, _ge.TakeEvent(self.player, thing)) return 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) #for i in self.player.inventory: # thingName = self.player.inventory[i].name # if ' '.join(args) == thingName: # self.setEvent(0.0, _ge.DropEvent(self.player.inventory[i])) # return 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]), preLoaded, self.nextThing) else: self.level, self.nextThing = _gm.GameMap.read(args[0], None, 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()) x, y = self.level.playerStart if len(args) == 3: x, y = int(args[1]), int(args[2]) if self.player == None: # create a player to put in the level. self.player = _gm.PlayerCharacter(x, y, self.playerDescription, {}, {}, self.playerName) print("Player created.") else: self.player.x, self.player.y = x, y 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('dialog')(dialog) def parseScript(self, instr: str): """parses then runs a script.""" #print('parseScript called with {}.'.format(instr)) if instr == '': return # nothing to be done. literalStr = list(instr) inQuotes = False script = [] stack = [] ret = [] argStart = 0 c = 0 l = 0 while c < len(instr): if inQuotes: if instr[c] == '"': if instr[c-1] == '\\': del literalStr[l-1] l -= 1 else: inQuotes = False del literalStr[l] l -= 1 else: if instr[c] == '"': # quoted string if l > 0 and instr[c-1] == '\\': del literalStr[l-1] l -= 1 else: inQuotes = True del literalStr[l] l -= 1 elif instr[c] in ' \t\n;}{': if l > 0 and instr[c-1] == '\\': del literalStr[l-1] l -= 1 else: if argStart != l: ret.append(''.join(literalStr[argStart:l])) if instr[c] == ';': if len(ret) > 0: script.append(ret) ret = [] elif instr[c] == '{': # block stack.append(script) script.append(ret) script = [] ret.append(script) ret = [] del literalStr[l] l -= 1 elif instr[c] == '}': # close block if len(ret) > 0: script.append(ret) ret = [] script = stack.pop() del literalStr[l] l -= 1 argStart = l + 1 c += 1 l += 1 if argStart != l: ret.append(''.join(literalStr[argStart:l])) if len(ret) > 0: script.append(ret) #print('After parsing: {}'.format(script)) self.customValues['scriptLocal'] = {} ret = self.runScript(script) if 'scriptLocal' in self.customValues: del self.customValues['scriptLocal'] if isinstance(ret, _ScriptBreak): ret = ret.value return ret def runScript(self, script: list): """run a script""" ret = False for line in script: if len(line) == 0: # empty line continue elif line[0].casefold() == 'playercmd': # run a player command self.getIO('playercmd')(line[1:]) elif line[0].casefold() == 'break': # exit early return _ScriptBreak(ret) elif line[0] in self.__scripts: # run a script ret = self.__scripts[line[0]](line[1:]) if isinstance(ret, _ScriptBreak): return ret else: # conditional evaluation ret = self.compareValues(line) # We specifically want the return value of the last line executed. return ret 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 self.__gameEvents[e.eventType](e): #print('break loop') 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 # default event handlers def handleNoOp(self, e): return True def handleGo(self, e): #if e.actor == self.playerName: # self.prevx, self.prevy = self.playerx, self.playery # self.playerx, self.playery = e.x, e.y #else: if e.actor == None: raise GameError("'Go' event raised for no object.") self.level.moveThing(e.actor, e.x, e.y) return False def handleArrive(self, e): #if e.actor == self.playerName: # self.prevx, self.prevy = self.playerx, self.playery # self.playerx, self.playery = e.x, e.y # print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(self.playerName, self.numberToLetter(e.x), e.y, e.t), file = self.outstream) # thing = self.level.getThingAtCoords(self.playerx, self.playery) # if thing: # if thing.thingType == 'x': # 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)) # return True #else: # actor = self.level.getThingByName(e.actor) # if actor: # self.level.moveThing(e.actor, e.x, e.y) # print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(actor.name, self.numberToLetter(e.x), e.y, e.t), file = self.outstream) # thing = self.level.getThingAtCoords(actor.x, actor.y) # if thing and thing != actor: # if thing.thingType == 'x': # print('{0} went {1}.'.format(actor.name, str(thing))) # self.level.removeThing(actor.name) # else: # print('There is nothing by the name of {0}.'.format(e.actor), file = self.outstream) # return False if e.actor == None: raise GameError("'Go' event raised for no object.") thing = self.level.getThingAtCoords(e.x, e.y) self.level.moveThing(e.actor, e.x, e.y) print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, self.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) return True 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): self.__behaviors[e.actor.behavior](e.actor) # 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 ifScript(self, args): """If statement: if [not] value [op value] script""" if len(args) < 2: raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) inverse = False ret = False if args[0] == 'not': inverse = True args.pop(0) if len(args) < 2: raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) # evaluate condition if len(args) > 1 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): if len(args) < 4: raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) ret = self.compareValues(args[:3]) args = args[3:] else: ret = bool(self.getValueFromString(args[0])) args = args[1:] if inverse: ret = not ret # if condition is true, evaluate further if ret: if isinstance(args[-1], list): return self.runScript(args[-1]) else: return self.runScript([args]) 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 getCustomValue(self, args, env = None): norm = True if env == None: env = self.customValues else: norm = False if len(args) > 0 and args[0] == 'local': if 'scriptLocal' in self.customValues: env = self.customValues['scriptLocal'] else: raise GameError('Attempted to set a local variable without local scope.') args.pop(0) return self.getValueFromString(args[0], env) def setCustomValue(self, args, env = None): """takes [customValue, op, value]""" if env == None: env = self.customValues if len(args) > 0 and args[0] == 'local': if 'scriptLocal' in self.customValues: env = self.customValues['scriptLocal'] else: raise GameError('Attempted to set a local variable without local scope.') args.pop(0) if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='): raise GameError('Arguments are not fit for the setCustomValue script.') # This next line allows cvmod to use values in the thing's customValues dict, # but doing this means that accessing a game CV requires setting it to a local var. val = self.getValueFromString(args[2], env) # set the value to a default value (0, 0.0, '', etc.) if not yet assigned if args[0] not in env and args[1] != '=': if isinstance(val, int): env[args[0]] = 0 elif isinstance(val, float): env[args[0]] = 0.0 elif isinstance(val, str): env[args[0]] = '' elif isinstance(val, bool): env[args[0]] = False # Beyond this point are types that can only be found from other customValues elif isinstance(val, tuple): env[args[0]] = () elif isinstance(val, list): env[args[0]] = [] elif isinstance(val, dict): env[args[0]] = {} else: raise GameError("Operation not supported for unassigned custom values of type {}: {}" .format(type(val), args[1])) # done parsing, evaluate if args[1] == '=': env[args[0]] = val elif args[1] == '+=': env[args[0]] += val elif args[1] == '-=': env[args[0]] -= val elif args[1] == '*=': env[args[0]] *= val elif args[1] == '/=': env[args[0]] /= val elif args[1] == '%=': env[args[0]] %= val elif args[1] == '//=': env[args[0]] //= val elif args[1] == '**=': env[args[0]] **= val elif args[1] == 'b=': env[args[0]] = bool(val) elif args[1] == '!=': env[args[0]] = not bool(val) elif args[1] == '|=': env[args[0]] |= val elif args[1] == '&=': env[args[0]] &= val elif args[1] == '^=': env[args[0]] ^= val return env[args[0]] def delCustomValue(self, args, env = None): """To clean up after a map.""" if env == None: env = self.customValues for i in args: if i in env: del(env[i]) 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 = self.parseCoords(i[9:].split(), usePlayerCoords = False, allowInventory = 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 = _gm.Item.defaultGraphic[0] if fgc == None: fgc = _gm.Item.defaultGraphic[1] if shape == None: shape = _gm.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 = _gm.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 = _gm.Useable.defaultGraphic[0] if fgc == None: fgc = _gm.Useable.defaultGraphic[1] if shape == None: shape = _gm.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 = self.parseCoords(i[10:].split()) elif i[0:3].casefold() == 'cv:': equalsign = i.index('=') cv = i[3:equalsign] customValues[cv] = self.getValueFromString(i[equalsign+1:]) thing = _gm.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 = _gm.NPC.defaultGraphic[0] if fgc == None: fgc = _gm.NPC.defaultGraphic[1] if shape == None: shape = _gm.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 = self.parseCoords(i[10:].split()) elif i[0:3].casefold() == 'cv:': equalsign = i.index('=') cv = i[3:equalsign] customValues[cv] = self.getValueFromString(i[equalsign+1:]) thing = _gm.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 = _gm.Door.defaultGraphic[0] if fgc == None: fgc = _gm.Door.defaultGraphic[1] if shape == None: shape = _gm.Door.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'description=': description = i[12:] elif i.casefold() == 'locked': locked = True thing = _gm.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 = _gm.MapExit.defaultGraphic[0] if fgc == None: fgc = _gm.MapExit.defaultGraphic[1] if shape == None: shape = _gm.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 = _gm.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 = _gm.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 = _gm.Item.defaultGraphic['fgc'], _gm.Item.defaultGraphic['bgc'], _gm.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 = self.parseCoords(i[10:].split(), usePlayerCoords = False, allowInventory = 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 = _gm.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 = self.parseCoords(args[0:colon], usePlayerCoords = False, allowInventory = 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 = self.parseCoords(args[0:-1], usePlayerCoords = False, allowInventory = True) if thing != None and thing.thingType in 'ciu': return self.getCustomValue(args[-1:], env = thing.customValues) 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 = self.parseCoords(args[0:-3], usePlayerCoords = False, allowInventory = True) if thing != None and thing.thingType in 'ciu': return self.setCustomValue(args[-3:], env = thing.customValues) 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 = self.parseCoords(args[0:colon], usePlayerCoords = False, allowInventory = True) if thing != None and thing.thingType in 'ciu': return self.setCustomValue(args[colon+1:], env = thing.customValues) else: raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.") # behaviors def stand(self, actor): pass def wander(self, actor): pass # 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.""" 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]