#gamemap.py import re import heapq import xml.etree.ElementTree as ET class Thing(object): def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None): self.thingType = thingType self.name = name self.description = description self.x = x self.y = y self.playerx = x self.playery = y if playerx: self.playerx = playerx if playery: self.playery = playery self.passable = bool(flags & 1) self.talkable = bool(flags & 2) self.lookable = bool(flags & 4) self.takeable = bool(flags & 8) self.useable = bool(flags & 16) self.graphic = ('clear', '#7F7F7F', ' ') def __str__(self): """__str__ is used for look.""" return self.description def __eq__(self, other): if not isinstance(other, Thing): return False return self.name == other.name class Item(Thing): def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = ('clear', '#00BF00', '^')): super(Item, self).__init__('i', name, x, y, description, 13) self.useFunc = useFunc self.useOnFunc = useOnFunc self.customValues = customValues self.ranged = ranged self.graphic = graphic def use(self): pass class Useable(Thing): def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = ('clear', '#0000FF', '#')): super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery) self.useFunc = useFunc self.customValues = customValues self.graphic = graphic def use(self): pass class NPC(Thing): def __init__(self, name, x: int, y: int, description: str, friendly = True, following = False, graphic = ('clear', '#000000', 'o')): super(NPC, self).__init__('c', name, x, y, description, 6) self.following = following self.friendly = friendly self.inventory = [] self.graphic = graphic def give(self, item): self.inventory.append(item) def drop(self, index): return self.inventory.pop(index) def dialog(self): pass class Door(Thing): def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = ('clear', '#7F3F00', '#')): self.descBase = description if description == None: if locked: description = "The {0} is locked.".format(name) else: description = "The {0} is unlocked.".format(name) else: if locked: description += " It is locked.".format(name) else: description += " It is unlocked.".format(name) super(Door, self).__init__('d', name, x, y, description, 1) self.passable = not locked self.key = key self.graphic = graphic def lock(self, key = None): if key == self.key: self.passable = not self.passable if self.descBase == None: if self.passable: self.description = "The {0} is unlocked.".format(self.name) else: self.description = "The {0} is locked.".format(self.name) else: if self.passable: self.description += " It is unlocked.".format(self.name) else: self.description += " It is locked.".format(self.name) return True return False class MapExit(Thing): def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix: None, graphic = ('clear', '#FF0000', 'x')): description = name if prefix: description = "{0} {1}".format(prefix, name) super(MapExit, self).__init__('x', name, x, y, description, 5) self.exitid = exitid self.destination = destination self.prefix = prefix self.graphic = graphic class GameMap(object): # Matrix tile codes: # e: empty (0) # d: door # x: map change # w: wall (0) # c: NPC (should this be a subclass of interactable?) # i: item # u: useable # p: player start (up to one per level) # regular expressions matrixRegex = re.compile(r'([ \t]*([a-z][0-9]*)+(\n))+') def __init__(self, name, graph, matrix, dimensions): self.name = name self.openingText = "" self.mapGraph = graph self.mapMatrix = matrix self.dimensions = dimensions self.thingPos = {} # int location : list of names self.thingNames = {} # Things can be looked up by name. self.playerStart = (1, 1) self.description = "The area is completely blank." self.floorColors = [] self.wallColors = [] self.persistent = [] @staticmethod def read(infile = None, prevMap = None, preLoaded = False): """Read map data and return a Map object. If infile is not provided, then it will read from stdin. Otherwise, it should be a valid file name. Entering a map through stdin will be obsolete once testing is over.""" data = None tryToRead = True if infile != None: try: with open(infile, 'r') as f: data = f.read() except OSError as e: print("The file could not be read. Falling back to stdin...") else: while tryToRead: try: data += input() except EOFError as e: tryToRead = False # Now what we do with the data info = ET.fromstring(data) mat = None layout = info.find('layout') if layout != None: #print(layout.text) match = GameMap.matrixRegex.match(layout.text.lstrip()) if match == None: raise RuntimeError('Map read a file without a map matrix.') mat = match.group() else: raise RuntimeError('Map read a file without a map matrix.') # generate matrix and graph first mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat) playerStart = (-1, -1) level = GameMap(infile, mapGraph, mapMatrix, dimensions) # Now, load other info GameMap.loadThings(level, info, prevMap, preLoaded) return level @staticmethod def parseMatrix(matrixStr): """Returns a map graph as an adjacency list, as well as the matrix as a list of lists of tuples.""" # Make the matrix first mat = [[]] x = 0 y = 0 i = 0 l = 0 while i < len(matrixStr): if matrixStr[i].isalpha(): j = i+1 while j < len(matrixStr) and matrixStr[j].isdecimal(): j += 1 if j == i+1: # no number mat[l].append((matrixStr[i], 0)) else: mat[l].append((matrixStr[i], int(matrixStr[i+1:j]))) i = j elif matrixStr[i] == '\n': if x == 0: x = len(mat[l]) elif x != len(mat[l]): raise RuntimeError('Map matrix has jagged edges.') l += 1 i += 1 mat.append([]) else: # assume it was a whitespace character. i += 1 y = len(mat) - 1 # Now for the graph numTiles = x * y dim = (x, y) graph = [[] for j in range(numTiles)] passable = ('e', 'd', 'x', 'p', 'i') for j in range(y): for i in range(x): if mat[j][i][0] in passable: here = GameMap.__coordsToInt(i, j, x) if i > 0 and mat[j][i-1][0] in passable: there = GameMap.__coordsToInt(i-1, j, x) graph[here].append(there) graph[there].append(here) if j > 0 and mat[j-1][i][0] in passable: there = GameMap.__coordsToInt(i, j-1, x) graph[here].append(there) graph[there].append(here) return mat, graph, dim @staticmethod def loadThings(level, info, prevMap = None, preLoaded = False): """load the things from the xml part of the map file.""" if 'openingText' in info.attrib: level.openingText = info.attrib['openingText'] if 'playerStart' in info.attrib: ps = info.attrib['playerStart'].split(',') level.playerStart = (int(ps[0]), int(ps[1])) if info.text != None: level.description = info.text # get map colors floorColors = info.find('floorColors') if floorColors != None: level.floorColors = floorColors.text.lstrip().split() if len(level.floorColors) == 0: level.floorColors.append('#9F7F5F') wallColors = info.find('wallColors') if wallColors != None: level.wallColors = wallColors.text.lstrip().split() if len(level.wallColors) == 0: level.wallColors.append('#7F3F0F') # get things for node in info: if node.tag == 'loadOnce': # Things in the load-once section are only loaded the first # time that the map is loaded, and saved in the player's # save file. if preLoaded: continue for node1 in node: if node1.tag == 'door': level.addThing(GameMap.__loadDoor(node1), True) elif node1.tag == 'useable': level.addThing(GameMap.__loadUseable(node1), True) elif node1.tag == 'item': level.addThing(GameMap.__loadItem(node1), True) elif node.tag == 'exit': level.addThing(GameMap.__loadExit(level, node, prevMap)) elif node.tag == 'door': level.addThing(GameMap.__loadDoor(node)) elif node.tag == 'useable': level.addThing(GameMap.__loadUseable(node)) @staticmethod def __loadExit(level, node, prevMap): exitid = 0 x, y = 0, 0 destination = '' name = '' prefix = None if 'name' in node.attrib: name = node.attrib['name'] else: print("Nameless exit, ommitting.") return None if 'id' in node.attrib: exitid = int(node.attrib['id']) else: print("Exit '{0}' with no id, ommitting.".format(name)) return None if 'location' in node.attrib: loc = node.attrib['location'].split(',') x, y = int(loc[0]), int(loc[1]) else: print("Exit '{0}' without a location, ommitting.".format(name)) return None if 'destination' in node.attrib: destination = node.attrib['destination'] else: print("Exit '{0}' with no destination, ommitting.".format(name)) return None if 'prefix' in node.attrib: prefix = node.attrib['prefix'] graphic = GameMap.__loadGraphic(node) #level.addThing(MapExit(name, x, y, exitid, destination, prefix)) if prevMap == exitid: level.playerStart = (x, y) return MapExit(name, x, y, exitid, destination, prefix, graphic) @staticmethod def __loadDoor(node): x, y = 0, 0 name = '' locked = False doorOpen = False key = None if 'name' in node.attrib: name = node.attrib['name'] else: print("Nameless door, ommitting.") return None if 'location' in node.attrib: pos = node.attrib['location'].split(',') x, y = int(pos[0]), int(pos[1]) else: print("Door '{0}' without a location, ommitting.".format(name)) return None if 'locked' in node.attrib: if node.attrib['locked'] == 'True': locked = True if 'open' in node.attrib: if node.attrib['open'] == 'True': doorOpen = True if 'key' in node.attrib: key = node.attrib['key'] graphic = GameMap.__loadGraphic(node) description = node.text return Door(name, x, y, locked, description, key, graphic) @staticmethod def __loadUseable(node): x, y = 0, 0 playerx, playery = 0, 0 name = '' useFunc = '' description = '' if 'name' in node.attrib: name = node.attrib['name'] else: print("Nameless useable, ommitting.") return None if 'location' in node.attrib: pos = node.attrib['location'].split(',') x, y = int(pos[0]), int(pos[1]) else: print("Useable '{0}' without a location, ommitting.".format(name)) return None if 'useLocation' in node.attrib: loc = node.attrib['useLocation'].split(',') playerx, playery = int(loc[0]), int(loc[1]) else: playerx, playery = x, y if 'useFunc' in node.attrib: useFunc = node.attrib['useFunc'] else: print("Unuseable useable '{0}', ommitting.".format(name)) return None graphic = GameMap.__loadGraphic(node) description = node.text # get custom values customVals = {} for val in node: if val.tag == 'graphic': continue # skip it value = None valName = '' if 'name' in val.attrib: valName = val.attrib['name'] else: print("Nameless custom value in {0}, ommitting.".format(name)) continue if 'value' in val.attrib: if val.tag == 'int': value = int(val.attrib['value']) elif val.tag == 'float': value = float(val.attrib['value']) elif val.tag == 'str': value = str(val.attrib['value']) elif val.tag == 'item': value = GameMap.__loadItem(val) elif val.tag == 'intList': value = [int(i) for i in val.attrib['value'].split(',')] elif val.tag == 'floatList': value = [float(i) for i in val.attrib['value'].split(',')] elif val.tag == 'strList': value = [i.lstrip() for i in val.attrib['value'].split(',')] # depricated, use 'list' of strings instead so you can use commas. else: print("Value {0} in {1} is of unknown type {2}, ommitting.".format(valName, name, val.tag)) continue elif val.tag == 'list': value = [] for i in val: if 'value' in i.attrib: if i.tag == 'int': value.append(int(i.attrib['value'])) elif i.tag == 'float': value.append(float(i.attrib['value'])) elif i.tag == 'str': value.append(str(i.attrib['value'])) elif i.tag == 'item': value.append(GameMap.__loadItem(i)) else: print("Value {0} in {1} has an entry of unknown type {2}, ommitting.".format(valName, name, i.tag)) continue else: print("Value {0} in {1} has an entry with no value, ommitting.".format(valName, name)) continue else: print("Value {0} in {1} has no value, ommitting.".format(valName, name)) continue customVals[valName] = value return Useable(name, x, y, description, useFunc, customVals, playerx, playery, graphic) @staticmethod def __loadItem(node): x, y = 0, 0 playerx, playery = 0, 0 name = '' useFunc = '' useOnFunc = '' description = '' ranged = False if 'name' in node.attrib: name = node.attrib['name'] else: print("Nameless item, ommitting.") return None if 'location' in node.attrib: pos = node.attrib['location'].split(',') x, y = int(pos[0]), int(pos[1]) else: print("Item '{0}' without a location, ommitting.".format(name)) return None if 'useFunc' in node.attrib: useFunc = node.attrib['useFunc'] if 'useOnFunc' in node.attrib: useOnFunc = node.attrib['useOnFunc'] if 'ranged' in node.attrib: ranged = (node.attrib['ranged'].casefold() == 'true') graphic = GameMap.__loadGraphic(node) description = node.text # get custom values customVals = {} for val in node: if val.tag == 'graphic': continue # skip it value = None valName = '' if 'name' in val.attrib: valName = val.attrib['name'] else: print("Nameless custom value in {0}, ommitting.".format(name)) continue if 'value' in val.attrib: if val.tag == 'int': value = int(val.attrib['value']) elif val.tag == 'float': value = float(val.attrib['value']) elif val.tag == 'str': value = str(val.attrib['value']) elif val.tag == 'item': value = GameMap.__loadItem(val) elif val.tag == 'intList': value = [int(i) for i in val.attrib['value'].split(',')] elif val.tag == 'floatList': value = [float(i) for i in val.attrib['value'].split(',')] elif val.tag == 'strList': value = [i.lstrip() for i in val.attrib['value'].split(',')] # depricated, use 'list' of strings instead so you can use commas. else: print("Value {0} in {1} is of unknown type {2}, ommitting.".format(valName, name, val.tag)) continue elif val.tag == 'list': value = [] for i in val: if 'value' in i.attrib: if i.tag == 'int': value.append(int(i.attrib['value'])) elif i.tag == 'float': value.append(float(i.attrib['value'])) elif i.tag == 'str': value.append(str(i.attrib['value'])) elif i.tag == 'item': value.append(GameMap.__loadItem(i)) else: print("Value {0} in {1} has an entry of unknown type {2}, ommitting.".format(valName, name, i.tag)) continue else: print("Value {0} in {1} has an entry with no value, ommitting.".format(valName, name)) continue else: print("Value {0} in {1} has no value, ommitting.".format(valName, name)) continue customVals[valName] = value return Item(name, x, y, description, useFunc, useOnFunc, customVals, ranged, graphic) @staticmethod def __loadGraphic(node): bgc = 'clear' fgc = '#7F7F7F' shape = '#' graphicInDecl = False if 'bgc' in node.attrib: bgc = node.attrib['bgc'] graphicInDecl = True if 'fgc' in node.attrib: fgc = node.attrib['fgc'] graphicInDecl = True if 'shape' in node.attrib: shape = node.attrib['shape'] graphicInDecl = True if not graphicInDecl: #check for graphic child node graphic = node.find('graphic') if graphic != None: if 'bgc' in graphic.attrib: bgc = graphic.attrib['bgc'] if 'fgc' in graphic.attrib: fgc = graphic.attrib['fgc'] if 'shape' in graphic.attrib: shape = graphic.attrib['shape'] return (bgc, fgc, shape) # stuff the gameshell itself might use def addThing(self, thing, persist = False): if thing == None: return if thing.name in self.thingNames: raise ValueError("Cannot have two objects named {0}.".format(thing.name)) pos = self.coordsToInt(thing.x, thing.y) if pos not in self.thingPos: self.thingPos[pos] = [thing.name] else: self.thingPos[pos].append(thing.name) self.thingNames[thing.name] = thing if persist: self.persistent.append(thing.name) def removeThing(self, name): thing = self.getThingByName(name) if thing: oldPos = self.coordsToInt(thing.x, thing.y) if oldPos in self.thingPos: self.thingPos[oldPos].remove(name) if len(self.thingPos[oldPos]) == 0: del self.thingPos[oldPos] del self.thingNames[name] def path(self, x1, y1, x2, y2, closeEnough = True): startThing = self.getThingAtCoords(x1, y1) if not closeEnough: if startThing and not startThing.passable: return -1, [] # meaning you can't get there dist, prev = self.dijkstra(x1, y1, closeEnough) endPoint = self.coordsToInt(x2, y2) numVertex = self.dimensions[0] * self.dimensions[1] if dist[endPoint] < numVertex + 1: return dist[endPoint], prev else: return -1, [] # meaning you can't get there def dijkstra(self, x1, y1, closeEnough = True): """Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2) The closeEnough parameter will create a path that lands beside the source if necessary.""" # first test to see that the start point is passable startThing = self.getThingAtCoords(x1, y1) startPoint = self.coordsToInt(x1, y1) #endPoint = self.coordsToInt(x2, y2) numVertex = self.dimensions[0] * self.dimensions[1] dist = [numVertex + 1 for i in range(numVertex)] prev = [-1 for i in range(numVertex)] dist[startPoint] = 0 if closeEnough: if startThing and not startThing.passable: dist[startPoint] = -1 queue = [] heapq.heappush(queue, (dist[startPoint], startPoint)) while len(queue) > 0: u = heapq.heappop(queue)[1] for v in self.mapGraph[u]: thing = self.getThingAtPos(v) if thing and not thing.passable: continue tempDist = dist[u] + 1 if tempDist < dist[v]: dist[v] = tempDist if dist[u] != -1: prev[v] = u heapq.heappush(queue, (dist[v], v)) return dist, prev #if dist[endPoint] < numVertex + 1: # return dist[endPoint], prev #else: # return -1, [] # meaning you can't get there def lineOfSight(self, x1, y1, x2, y2): pass @staticmethod def __coordsToInt(x, y, width): return x + y * width def coordsToInt(self, x, y, width = -1): if width < 0: return x + y * self.dimensions[0] else: return x + y * width def intToCoords(self, pos): return pos % self.dimensions[0], int(pos / self.dimensions[0]) def getThingAtCoords(self, x, y): return self.getThingAtPos(self.coordsToInt(x, y)) def getThingsAtCoords(self, x, y): return self.getThingsAtPos(self.coordsToInt(x, y)) def getThingAtPos(self, pos): if pos in self.thingPos: return self.thingNames[self.thingPos[pos][0]] else: return None def getThingsAtPos(self, pos): if pos in self.thingPos: ret = [] for i in self.thingPos[pos]: ret.append(self.thingNames[i]) return ret else: return None def getThingByName(self, name): if name in self.thingNames: return self.thingNames[name] else: return None def moveThing(self, name, x, y = -1): newPos = x if y != -1: newPos = self.coordsToInt(x, y) else: x, y = self.intToCoords(x) thing = self.getThingByName(name) if thing: oldPos = self.coordsToInt(thing.x, thing.y) if oldPos in self.thingPos: self.thingPos[oldPos].remove(name) if len(self.thingPos[oldPos]) == 0: del self.thingPos[oldPos] if newPos not in self.thingPos: self.thingPos[newPos] = [name] else: self.thingPos[newPos].append(name) thing.x, thing.y = x, y else: raise RuntimeError("There is nothing by the name of {0}.".format(name))