gameshell/gamemap.py
2019-05-30 15:44:29 -04:00

1013 lines
39 KiB
Python

#gamemap.py
import re
import heapq
import xml.etree.ElementTree as ET
import ruamel.yaml
import math as _mt
from ruamel.yaml.comments import CommentedMap # for loading classes
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', ' ')
self.thingID = -1 # ID not assigned
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):
yaml_flag = u'!Item'
defaultGraphic = ('clear', '#00BF00', '^')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic):
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
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Item.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Item.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Item.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if node.useOnFunc != '':
ret['useOnFunc'] = node.useOnFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.ranged:
ret['ranged'] = node.ranged
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
bgc = Item.defaultGraphic[0]
fgc = Item.defaultGraphic[1]
shape = Item.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'useOnFunc' in parts:
useOnFunc = parts['useOnFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'ranged' in parts:
useOnFunc = parts['ranged']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, useOnFunc, customValues, ranged, graphic)
class Useable(Thing):
yaml_flag = u'!Useable'
defaultGraphic = ('clear', '#0000FF', '#')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic):
super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery)
self.useFunc = useFunc
self.customValues = customValues
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Useable.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Useable.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Useable.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
customValues = {}
playerx, playery = parts['location']
bgc = Useable.defaultGraphic[0]
fgc = Useable.defaultGraphic[1]
shape = Useable.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, customValues, playerx, playery, graphic)
class NPC(Thing):
yaml_flag = u'!NPC'
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, name, x: int, y: int, description: str, behavior: str, inventory: list, customValues: dict, playerx = None, playery = None, following = False, graphic = defaultGraphic):
super(NPC, self).__init__('c', name, x, y, description, 6, playerx, playery)
self.behavior = behavior
self.following = following
self.inventory = inventory
self.customValues = customValues
self.graphic = graphic
self.behaveEvent = None
def give(self, item):
self.inventory.append(item)
def drop(self, index):
return self.inventory.pop(index)
def dialog(self):
pass
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y),
'description': node.description, 'behavior': node.behavior}
# save graphic
graphic = {}
if node.graphic[0] != NPC.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != NPC.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != NPC.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.following:
ret['following'] = node.following
if len(node.inventory) > 0:
ret['inventory'] = node.inventory
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
following = False
inventory = []
customValues = {}
playerx, playery = parts['location']
bgc = NPC.defaultGraphic[0]
fgc = NPC.defaultGraphic[1]
shape = NPC.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'following' in parts:
useFunc = parts['following']
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], parts['behavior'], inventory, customValues,
playerx, playery, following, graphic)
class Door(Thing):
yaml_flag = u'!Door'
defaultGraphic = ('clear', '#7F3F00', '#')
def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic):
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
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y)}
# save graphic
graphic = {}
if node.graphic[0] != Door.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Door.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Door.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save door state
if node.passable:
ret['locked'] = not node.passable
if node.descBase != None:
ret['description'] = node.descBase
if node.key != None:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
description = None
locked = False
key = None
bgc = Door.defaultGraphic[0]
fgc = Door.defaultGraphic[1]
shape = Door.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load door state
if 'description' in parts:
description = parts['description']
if 'locked' in parts:
locked = parts['locked']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
locked, description, key, graphic)
class MapExit(Thing):
yaml_flag = u'!MapExit'
defaultGraphic = ('clear', '#FF0000', 'x')
def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix: None, graphic = defaultGraphic):
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
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination}
# save graphic
graphic = {}
if node.graphic[0] != MapExit.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != MapExit.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != MapExit.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
if node.prefix != None:
ret['prefix'] = node.prefix
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
prefix = None
bgc = MapExit.defaultGraphic[0]
fgc = MapExit.defaultGraphic[1]
shape = MapExit.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
if 'prefix' in parts:
prefix = parts['prefix']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['id'], parts['destination'], prefix, graphic)
class MapEntrance(Thing):
yaml_flag = u'!MapEntrance'
defaultGraphic = ('clear', 'clear', 'x')
# no graphic - should not be drawn
def __init__(self, x: int, y: int, exitid: int, name = None):
if name == None:
name = 'entrance {}'.format(exitid)
super(MapEntrance, self).__init__('a', name, x, y, description, 1)
self.exitid = exitid
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'location': (node.x, node.y), 'id': node.exitid}
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
return cls(parts['location'][0], parts['location'][1], parts['id'])
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))+')
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 = ''
@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 convert(infile: str):
"""Convert an XML map to a YAML one."""
data = None
with open(infile, 'r') as f:
data = f.read()
info = ET.fromstring(data)
layout = info.find('layout')
ret = {}
if layout == None:
raise MapError('No layout in {0}.'.format(infile))
# "Layout" needs some work before we convert it.
ret['layout'] = GameMap.__cleanStr(layout.text.strip())
# now the rest of the things
if 'openingText' in info.attrib:
ret['openingText'] = info.attrib['openingText']
else:
raise MapError('No opening text in {0}.'.format(infile))
if 'playerStart' in info.attrib:
ps = info.attrib['playerStart'].split(',')
ret['playerStart'] = (int(ps[0]), int(ps[1]))
else:
raise MapError('No player start position in {0}.'.format(infile))
if info.text != None:
ret['description'] = GameMap.__cleanStr(info.text, ' ')
else:
raise MapError('No description in {0}.'.format(infile))
# get map colors
floorColors = ['#9F7F5F']
floorColorsStr = info.find('floorColors')
if floorColorsStr != None:
floorColors = floorColorsStr.text.lstrip().split()
if len(floorColors) == 0:
floorColors.append('#9F7F5F')
ret['floorColors'] = floorColors
wallColors = ['#7F3F0F']
wallColorsStr = info.find('wallColors')
if wallColorsStr != None:
wallColors = wallColorsStr.text.lstrip().split()
if len(wallColors) == 0:
wallColors.append('#7F3F0F')
ret['wallColors'] = wallColors
# get things
ret['loadAlways'] = []
ret['loadOnce'] = []
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.
for node1 in node:
if node1.tag == 'door':
ret['loadOnce'].append(GameMap.__loadDoor(node1))
elif node1.tag == 'useable':
ret['loadOnce'].append(GameMap.__loadUseable(node1))
elif node1.tag == 'item':
ret['loadOnce'].append(GameMap.__loadItem(node1))
elif node.tag == 'exit':
ret['loadAlways'].append(GameMap.__loadExit(None, node, -1)) # weird arguments: there is no actual level
elif node.tag == 'door':
ret['loadAlways'].append(GameMap.__loadDoor(node))
elif node.tag == 'useable':
ret['loadAlways'].append(GameMap.__loadUseable(node))
#start saving
outfile = infile[:-3] + 'yml'
yaml = ruamel.yaml.YAML()
yaml.indent(mapping=4, sequence=4, offset=2)
yaml.register_class(Item)
yaml.register_class(Useable)
yaml.register_class(Door)
yaml.register_class(MapExit)
with open(outfile, 'w') as f:
f.write('%YAML 1.2\n---\n')
yaml.dump(ret, f)
@staticmethod
def read(infile = None, prevMap = 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
yaml = ruamel.yaml.YAML()
yaml.register_class(Item)
yaml.register_class(Useable)
yaml.register_class(NPC)
yaml.register_class(Door)
yaml.register_class(MapExit)
if infile != None:
try:
with open(infile, 'r') as f:
info = yaml.load(f)
except OSError as e:
print("The file could not be read.")
return None, nextThing
else:
raise RuntimeError("No file was specified for loading.")
# 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 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)
level = GameMap(infile, mapGraph, mapMatrix, dimensions)
# Now, load other info
nextThing = GameMap.loadThings(level, info, prevMap, 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 RuntimeError("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 RuntimeError("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, 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))
nextThing = level.addThing(thing, nextThing, True)
if 'loadAlways' in info:
for thing in info['loadAlways']:
#print(type(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
return nextThing
# stuff the gameshell itself might use
def addThing(self, thing, nextThing = 0, persist = False):
if thing == None:
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.
if thing.thingType in 'iuc':
nextThing = self.addThingRecursive(thing.customValues, nextThing)
if thing.thingType == 'c':
for i in thing.inventory:
i.thingID = nextThing
nextThing += 1
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
def addThingRecursive(self, container, nextThing = 0):
if isinstance(container, Thing):
container.thingID = nextThing
return nextThing + 1
elif isinstance(container, dict):
for i in container:
nextThing = self.addThingRecursive(container[i], nextThing)
return nextThing
elif isinstance(container, list):
for i in container:
nextThing = self.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, 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):
Dx = x2 - x1
Dy = y2 - y1
y = y1 + 0.5
x = x1 + 0.5
lst = []
if abs(Dx) >= abs(Dy):
if Dx < 0:
x = x2 + 0.5
y = y2 + 0.5
dy = Dy / Dx
while(x < max(x2, x1) - 0.5):
x += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(y) != _mt.floor(y + dy):
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y + dy)))
y += dy
elif abs(Dx) < abs(Dy):
if Dy < 0:
x = x2 + 0.5
y = y2 + 0.5
dx = Dx / Dy
while(y < max(y2, y1) - 0.5):
y += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(x) != _mt.floor(x + dx):
lst.append(self.coordsToInt(_mt.floor(x + dx), _mt.floor(y)))
x += dx
# Here is where we actually check:
for space in lst:
if not self.isPassable(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:
ret = []
for i in self.thingPos[pos]:
ret.append(self.things[i])
return ret
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:
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)
relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y
thing.x, thing.y = x, y
thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery
else:
raise RuntimeError("There is nothing to move.".format(name))