#gamemap.py import re import heapq import ruamel.yaml import math as _mt import gamethings as _gt import gamelocus as _gl from ruamel.yaml.comments import CommentedMap class Singleton(object): """This is a super basic class (would be a struct in other languages) that represents where a singleton Thing should be in a map.""" yaml_flag = u'!Singleton' def __init__(self, name: str, x: int, y: int): self.name = name self.x = x self.y = y @classmethod def to_yaml(cls, representer, node): representer.represent_mapping({ 'name': self.name, 'location': (self.x, self.y) }) @classmethod def from_yaml(cls, constructor, node): parts = CommentedMap() constructor.construct_mapping(node, parts, True) # since all parts are necessary, I won't bother with if statements # and let it crash. if not isinstance(parts['name'], str): raise RuntimeError("Name must be a string.") if not isinstance(parts['location'], list): raise RuntimeError("Location must be a list.") if not (isinstance(parts['location'][0], int) and isinstance(parts['location'][1], int)): raise RuntimeError("Coordinates must be integers.") return cls(parts['name'], parts['location'][0], parts['location'][1]) class MapError(RuntimeError): pass class GameMap(object): # Matrix tile codes: # e: empty (0) # w: wall (0) # regular expressions tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])') matrixRegex = re.compile(r'(?:[ \t]*(?:[a-z ](?:[0-9]+|[ ]))+(\n))+') yaml = ruamel.yaml.YAML() yaml.register_class(_gt.Item) yaml.register_class(_gt.Useable) yaml.register_class(_gt.NPC) yaml.register_class(_gt.Door) yaml.register_class(_gt.MapExit) yaml.register_class(_gt.MapEntrance) yaml.register_class(Singleton) def __init__(self, name, graph, matrix, dimensions): self.name = name self.openingText = "" self.mapGraph = graph self.mapMatrix = matrix self.dimensions = dimensions self.things = {} # int thingID : thing self.thingPos = {} # int location : list of int thingIDs self.thingNames = {} # str name: list of int thingIDs self.playerStart = (1, 1) self.description = "The area is completely blank." self.floorColors = [] self.wallColors = [] self.persistent = [] self.enterScript = '' self.version = 'square 1' @staticmethod def __cleanStr(text: str, end = '\n'): if text == None: return text text = text.strip() i = 0 while True: i = text.find('\n', i) if i == -1: break j = i+1 if len(text) > j: while text[j] in ' \t': j += 1 # just find the first non-space chartacter text = text[:i+1] + text[j:] i += 1 else: break return text.replace('\n', end) @staticmethod def read(infile = None, prevMap = None, singletons = None, preLoaded = False, nextThing = 0): """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.""" info = None tryToRead = True if infile != None: try: with open(infile, 'r') as f: info = GameMap.yaml.load(f) except OSError as e: print("The file could not be read.") return None, nextThing else: raise MapError("No file was specified for loading.") # Future feature: different map standards, with cool special features # like hex tiles, or walls in-between tiles. # For now just 'square 1': square tiles, version 1. version = 'square 1' if 'version' in info: if info['version'] in ('square 1'): version = info['version'] # Now what we do with the data mat = None if 'layout' not in info: raise MapError('No layout in {0}.'.format(infile)) layout = info['layout'] if layout != None: #print(layout.text) match = GameMap.matrixRegex.match(layout.lstrip()) if match == None: raise MapError('Map read a file without a map matrix.') mat = match.group() else: raise MapError('Map read a file without a map matrix.') # generate matrix and graph first mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat) level = GameMap(infile, mapGraph, mapMatrix, dimensions) # Now, load other info nextThing = GameMap.loadThings(level, info, prevMap, singletons, preLoaded, nextThing) return level, nextThing @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 l = 0 while len(matrixStr) > 0: tile = GameMap.tileRegex.match(matrixStr) if tile != None: tileType = tile.group(1) tileNum = tile.group(2) if tileType == ' ': tileType = 'e' if tileNum == ' ': tileNum = '0' mat[l].append((tileType, int(tileNum))) #x += 1 matrixStr = matrixStr[len(tile.group()):] elif matrixStr[0] == '\n': if x == 0: x = len(mat[l]) elif x != len(mat[l]): raise MapError("Map matrix has jagged edges.") l += 1 #x = 0 mat.append([]) i = 1 while i < len(matrixStr) and matrixStr[i] in ' \t\n': i += 1 if i == len(matrixStr): matrixStr = '' else: matrixStr = matrixStr[i:] else: # This should happen when it finishes? raise MapError("Unexpected token in map matrix: '{0}'".format(matrixStr)) 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, singletons = None, preLoaded = False, nextThing = 0): """load the things from the xml part of the map file.""" if 'openingText' in info: level.openingText = info['openingText'] if 'playerStart' in info: level.playerStart = info['playerStart'] if 'description' in info: level.description = info['description'] if 'enterScript' in info: level.enterScript = info['enterScript'] # get map colors if 'floorColors' in info: level.floorColors = info['floorColors'] if 'wallColors' in info: level.wallColors = info['wallColors'] if len(level.floorColors) == 0: level.floorColors.append('#9F7F5F') if len(level.wallColors) == 0: level.wallColors.append('#7F3F0F') # get things hasKnownEntrance = False if 'loadOnce' in info and not preLoaded: for thing in info['loadOnce']: #print(type(thing)) if isinstance(thing, _gt.Thing): nextThing = level.addThing(thing, nextThing, True) else: raise MapError("Non-thing loaded as a thing:\n{}".format(thing)) if 'loadAlways' in info: for thing in info['loadAlways']: #print(type(thing)) if isinstance(thing, _gt.Thing): nextThing = level.addThing(thing, nextThing) if ((thing.thingType == 'x' and not hasKnownEntrance) or thing.thingType == 'a') and prevMap == thing.exitid: level.playerStart = (thing.x, thing.y) hasKnownEntrance = True elif isinstance(thing, Singleton): if singletons != None: single = singletons[thing.name] single.x, single.y = thing.x, thing.y single.prevx, single.prevy = thing.x, thing.y nextThing = level.addThing(single, nextThing) else: raise MapError("Non-thing loaded as a thing:\n{}".format(thing)) return nextThing # stuff the gameshell itself might use def addThing(self, thing, nextThing = 0, persist = False): if thing == None: # it must be a singleton return nextThing #if thing.name in self.thingNames: # resolved # raise ValueError("Cannot have two objects named {0}.".format(thing.name)) if thing.thingID == -1: # This is to ensure that we don't double up IDs. thing.thingID = nextThing nextThing += 1 # Some things, like containers, have other things as custom values, # so they need IDs as well. # Let's only add them if they weren't already loaded. if thing.thingType in 'iun': nextThing = GameMap.addThingRecursive(thing.customValues, nextThing) if thing.thingType == 'n': for i in thing.tempInventory: if i.thingID == -1: i.thingID = nextThing nextThing = GameMap.addThingRecursive(i.customValues, nextThing + 1) thing.addThing(i) del thing.tempInventory pos = self.coordsToInt(thing.x, thing.y) if pos not in self.thingPos: self.thingPos[pos] = [thing.thingID] else: self.thingPos[pos].append(thing.thingID) if thing.name not in self.thingNames: self.thingNames[thing.name] = [thing.thingID] else: self.thingNames[thing.name].append(thing.thingID) self.things[thing.thingID] = thing if persist: self.persistent.append(thing.thingID) return nextThing @staticmethod def addThingRecursive(container, nextThing = 0): if isinstance(container, _gt.Thing): if container.thingID == -1: container.thingID = nextThing nextThing = GameMap.addThingRecursive(container.customValues, nextThing) return nextThing + 1 else: return nextThing elif isinstance(container, dict): for i in container: nextThing = GameMap.addThingRecursive(container[i], nextThing) return nextThing elif isinstance(container, list): for i in container: nextThing = GameMap.addThingRecursive(i, nextThing) return nextThing else: return nextThing def getThing(self, **kwargs): if 'name' in kwargs: return self.getThingByName(kwargs['name']) elif 'thingID' in kwargs: return self.getThingByID(kwargs['thingID']) elif 'pos' in kwargs: return self.getThingAtPos(kwargs['pos']) elif 'coords' in kwargs: return self.getThingAtCoords(kwargs['coords'][0], kwargs['coords'][1]) else: raise ValueError('Thing cannot be found by {}.'.format(str(kwargs))) def removeThing(self, **kwargs): if 'name' in kwargs: return self.removeThingByName(kwargs['name']) elif 'thingID' in kwargs: return self.removeThingByID(kwargs['thingID']) elif 'pos' in kwargs: return self.removeThingAtPos(kwargs['pos']) elif 'coords' in kwargs: return self.removeThingAtCoords(kwargs['coords'][0], kwargs['coords'][1]) else: raise ValueError('Thing cannot be found by {}.'.format(str(kwargs))) def path(self, x1, y1, loc, closeEnough = True): startThing = self.getThingAtCoords(x1, y1) #if not closeEnough: # if startThing and not startThing.passable: # return -1, [], -1 # meaning you can't get there dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough) #endPoint = self.coordsToInt(x2, y2) numVertex = self.dimensions[0] * self.dimensions[1] if endPoint > -1 and dist[endPoint] < numVertex + 1: pathList = [endPoint] nextPoint = prev[endPoint] while nextPoint != -1: pathList.append(nextPoint) nextPoint = prev[nextPoint] pathList.reverse() return dist[endPoint], pathList[1:], endPoint else: return -1, [], -1 # meaning you can't get there def dijkstra(self, x1, y1, loc = None, 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. The loc parameter is an optional locus which will cause the function to return once it finds a point that's in the locus.""" # first test to see that the start point is passable startThing = self.getThingAtCoords(x1, y1) startPoint = self.coordsToInt(x1, y1) endPoint = -1 # until one matches the locus, which it might not. 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 # This is so it doesn't path into a non-passable end point. queue = [] heapq.heappush(queue, (dist[startPoint], startPoint)) while len(queue) > 0: u = heapq.heappop(queue)[1] if loc != None and self.intToCoords(u) in loc: return dist, prev, u for v in self.mapGraph[u]: thing = self.getThingAtPos(v) if thing and not thing.passable: if closeEnough and self.intToCoords(v) in loc: return dist, prev, u else: 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, endPoint #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): """Test for line of signt from one tile to another.""" # Trivial case first: if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1: return True # Common case second: lst = list(_gl.LineLocus(x1, y1, x2, y2, False))[1:-1] # Here is where we actually check: for space in lst: if not self.isPassable(self.coordsToInt(*space)): return False return True def isPassable(self, x, y = -1): pos = x if y == -1: x, y = self.intToCoords(x) else: pos = self.coordsToInt(x, y) if self.mapMatrix[y][x][0] == 'w': return False thingsInSpace = self.getThingsAtPos(pos) for thing in thingsInSpace: if not thing.passable: return False return True @staticmethod def __coordsToInt(x, y, width): return x + y * width def coordsToInt(self, x: int, y: int, width = -1): if width < 0: return x + y * self.dimensions[0] else: return x + y * width def intToCoords(self, pos: int): 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.things[self.thingPos[pos][0]] else: return None def getThingsAtPos(self, pos): if pos in self.thingPos: return [self.things[i] for i in self.thingPos[pos]] else: return [] def getThingByName(self, name): if name in self.thingNames: return self.things[self.thingNames[name][0]] else: return None def getThingsByName(self, name): if name in self.thingNames: ret = [] for i in self.thingNames[name]: ret.append(self.things[i]) return ret else: return [] def getThingByID(self, thingID): if thingID in self.things: return self.things[thingID] else: return None def removeThingByThing(self, thing): if thing != None: oldPos = self.coordsToInt(thing.x, thing.y) if oldPos in self.thingPos: self.thingPos[oldPos].remove(thing.thingID) if len(self.thingPos[oldPos]) == 0: del self.thingPos[oldPos] oldName = thing.name if oldName in self.thingNames: self.thingNames[oldName].remove(thing.thingID) if len(self.thingNames[oldName]) == 0: del self.thingNames[oldName] if thing.thingID in self.persistent: self.persistent.remove(thing.thingID) del self.things[thing.thingID] return thing else: return None def removeThingAtCoords(self, x, y): return self.removeThingAtPos(self.coordsToInt(x, y)) def removeThingsAtCoords(self, x, y): return self.removeThingsAtPos(self.coordsToInt(x, y)) def removeThingAtPos(self, pos): if pos in self.thingPos: return self.removeThingByThing(self.getThingAtPos(pos)) else: return None def removeThingsAtPos(self, pos): if pos in self.thingPos: ret = [] for i in self.thingPos[pos]: ret.append(self.removeThingByThing(self.things[i])) return ret else: return [] def removeThingByName(self, name): if name in self.thingNames: return self.removeThingByThing(self.getThingByName(name)) else: return None def removeThingsByName(self, name): if name in self.thingNames: ret = [] for i in self.thingNames[name]: ret.append(self.removeThingByThing(self.things[i])) return ret else: return [] def removeThingByID(self, thingID): if thingID in self.things: return self.removeThingByThing(self.things[thingID]) else: return None def moveThing(self, thing, x, y = -1): newPos = x if y != -1: newPos = self.coordsToInt(x, y) else: x, y = self.intToCoords(x) if thing != None: if thing.x == x and thing.y == y: return # it's already there, so don't do anything. oldPos = self.coordsToInt(thing.x, thing.y) if oldPos in self.thingPos: self.thingPos[oldPos].remove(thing.thingID) if len(self.thingPos[oldPos]) == 0: del self.thingPos[oldPos] if newPos not in self.thingPos: self.thingPos[newPos] = [thing.thingID] else: self.thingPos[newPos].append(thing.thingID) relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y thing.prevx, thing.prevy = thing.x, thing.y thing.x, thing.y = x, y thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery else: raise MapError("There is nothing to move.") class LoSLocus(_gl.Locus): """A locus that defines all points within line-of-sight of a given points.""" def __init__(self, x, y, level): self.x = x self.y = y self.level = level def __contains__(self, item): if not isinstance(item, tuple) and not isinstance(item, list): raise ValueError("Item must be a tuple or a list.") x, y = item return self.level.lineOfSight(x, y, self.x, self.y) def __iter__(self): ret = [(self.x, self.y)] mat = [[False for i in range(self.level.y)] for j in range(self.level.x)] mat[self.x][self.y] = True fringe = [(self.x, self.y + 1), (self.x + 1, self.y), (self.x, self.y - 1), (self.x - 1, self.y), (self.x + 1, self.y + 1), (self.x + 1, self.y - 1), (self.x - 1, self.y - 1), (self.x - 1, self.y + 1)] while len(fringe) > 0: point = fringe.pop(0) check = [] if abs(point[0] - self.x) > abs(point[1] - self.y): if point[0] > self.x: check.append((point[0] - 1, point[1])) if point[1] > self.y: check.append((point[0] - 1, point[1] - 1)) elif point[1] < self.y: check.append((point[0] - 1, point[1] + 1)) else: check.append((point[0] + 1, point[1])) if point[1] > self.y: check.append((point[0] + 1, point[1] - 1)) elif point[1] < self.y: check.append((point[0] + 1, point[1] + 1)) elif abs(point[0] - self.x) < abs(point[1] - self.y): if point[1] > self.y: check.append((point[0], point[1] - 1)) if point[0] > self.x: check.append((point[0] - 1, point[1] - 1)) elif point[0] < self.x: check.append((point[0] + 1, point[1] - 1)) else: check.append((point[0], point[1] + 1)) if point[0] > self.x: check.append((point[0] - 1, point[1] + 1)) elif point[0] < self.x: check.append((point[0] + 1, point[1] + 1)) else: if point[0] > self.x: if point[1] > self.y: check.append((point[0] - 1, point[1] - 1)) else: check.append((point[0] - 1, point[1] - 1)) else: if point[1] > self.y: check.append((point[0] + 1, point[1] - 1)) else: check.append((point[0] + 1, point[1] - 1)) status = [mat[i[0]][i[1]] for i in check] addIf = False if True in status: if False in status: addIf = self.level.lineOfSight(point[0], point[1], self.x, self.y) else: addIf = True if addIf: mat[point[0]][point[1]] = self.level.isPassable(*point) ret.append(point) return iter(ret)