gameshell/gamemap.py

689 lines
25 KiB
Python
Raw Normal View History

2019-01-18 14:56:51 -05:00
#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):
"""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)
if not closeEnough:
if startThing and not startThing.passable:
return -1, [] # meaning you can't get there
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))
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))