# 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 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.__gameEvents = {} self.__IOCalls = {} # {str : function} self.customVals = {} # for setting flags and such self.level = None self.persist = {} # {level : {thingName : thing}} self.ps2 = '? ' self.eventQueue = [] self.gameTime = 0.0 self.skipLoop = True # player info self.playerx = -1 self.playery = -1 self.prevx = -1 # prevx and prevy are for moving the player out of the way self.prevy = -1 # when the tile they're standing on becomes blocked. self.playerName = 'You' self.playerInv = {} # 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.registerUseFunc('examine', self.examine) self.registerUseFunc('key', self.key) self.registerUseFunc('container', self.container) # 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.""" coordStr = args[0] if len(args) > 1: coordStr += ' ' + args[1] #print(coordStr) x = -1 y = -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 name = ' '.join(args) thing = self.level.getThingByName(name) if thing != None and usePlayerCoords: return thing, thing.playerx, thing.playery elif thing != None: return thing, thing.x, thing.y elif allowInventory: if name in self.playerInv: thing = self.playerInv[name] if thing != None: return thing, -1, -1 else: return None, -1, -1 #raise RuntimeError("'None' item named '{0}' in player inventory.".format(name)) else: return None, -1, -1 #raise ValueError("{0} cannot be reached.".format(name)) else: # nothing return None, -1, -1 #raise ValueError("{0} cannot be reached.".format(name)) return None, x, y 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 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 RuntimeError("Cannot move: No level has been loaded.") 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.playerx, self.playery): #_hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, x, y, 0.0))) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: print('{0} cannot reach {1}{2}.'.format(self.playerName, self.numberToLetter(x), y), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.playerx, self.playery) space = path[pos] if space == -1: self.setEvent(0.0, _ge.ArriveEvent(self.playerName, self.playerx, self.playery, 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.playerName, newx, newy)) else: self.setEvent(t * speed, _ge.ArriveEvent(self.playerName, newx, newy, t * speed)) break t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.ArriveEvent(self.playerName, 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 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 thing: print(self.justifyText(str(thing)), file = self.outstream) else: print("There is nothing to see here.\n", file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) 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.""" 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) if 'on' in args: self.useOn(args, speed) return 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.name not in self.playerInv: 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.playerx, self.playery) or thing.name in self.playerInv: self.setEvent(0.125, _ge.UseEvent(thing)) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.playerx, self.playery) 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.playerName, 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)) 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]) if args[onIndex+1] == 'the': onIndex += 1 thing, x, y = self.parseCoords(args[onIndex+1:]) if item == None or item.name not in self.playerInv: print("There is no {0} in the inventory.".format(item.name), 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 # Similar to go, but not quite the same. if (x, y) == (self.playerx, self.playery): self.setEvent(0.125, _ge.UseOnEvent(item, thing)) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: if thing != None: print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) else: print('{0} cannot reach {1}{2}.'.format(self.playerName, self.numberToLetter(x), y), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.playerx, self.playery) 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.playerName, 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)) 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.""" 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) 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.playerx, self.playery): self.setEvent(0.125, _ge.TakeEvent(thing)) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: pos = self.level.coordsToInt(self.playerx, self.playery) 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.playerName, 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.TakeEvent(thing)) return def drop(self, args): """drop [the] item""" if args[0] == 'the': args.pop(0) if args[0] in self.playerInv: self.setEvent(0.0, _ge.DropEvent(self.playerInv[args[0]])) else: print('{0} do not have a {1}.'.format(self.playerName, 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.getThingByName(i) preLoaded = False if args[0] in self.persist: preLoaded = True # load the new level if len(args) == 2: self.level = _gm.GameMap.read(args[0], int(args[1]), preLoaded) else: self.level = _gm.GameMap.read(args[0], preLoaded) # get persistent things from it if args[0] in self.persist: persistedThings = tuple(self.persist[args[0]].keys()) for i in persistedThings: self.level.addThing(self.persist[args[0]][i], True) 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()) if len(args) <= 2: self.playerx, self.playery = self.level.playerStart else: self.playerx, self.playery = int(args[1]), int(args[2]) self.prevx, self.prevy = self.playerx, self.playery if self.onLevelLoad != None: self.onLevelLoad() 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.getThingByName(i) # build data object to be saved data = (self.playerName, self.playerx, self.playery, self.playerInv, self.level.name, self.persist, self.eventQueue, self.gameTime) # 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.playerName, x, y, self.playerInv, levelname, self.persist, self.eventQueue, self.gameTime = _pi.load(f) #print(levelname, x, y, file = self.outstream) self.loadMap((levelname, x, y)) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return 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: 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': 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 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), _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), _ge.NoOpEvent()) return False def handleTake(self, e): self.level.removeThing(e.item.name) self.playerInv[e.item.name] = e.item return True def handleDrop(self, e): e.item.x = self.playerx e.item.y = self.playery self.level.addThing(e.item, True) del self.playerInv[e.item.name] return True # default useFuncs: take a list of arguments, return the time the use took def examine(self, thing): """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(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(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(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(thing.customValues['text'][cursor]), file = self.outstream) thing.customValues['cursor'] = cursor if 'set' in thing.customValues: if 'cursor' in thing.customValues: self.customVals[thing.customValues['set']] = thing.customValues[cursor] else: self.customVals[thing.customValues['set']] = 0 return 0.0 def container(self, thing): """Acts as a container. Items can be traded between the container and the player's inventory.""" items = list(thing.customValues['items']) self.playerInv, thing.customValues['items'], timeOpen = self.getIO('container')(self.playerInv, items) return timeOpen def key(self, item, thing): """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.playerx == thing.x and self.playery == thing.y: self.playerx, self.playery = self.prevx, self.prevy else: print("The key doesn't fit that lock.", file = self.outstream) return 0.125 # 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 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 getIO(self, name): """This is so derived classes can access their IOCalls.""" return self.__IOCalls[name]