Archipelago/worlds/sm/variaRandomizer/graph/graph.py

414 lines
18 KiB
Python

import copy, logging
from operator import attrgetter
from ..utils import log
from ..logic.smbool import SMBool, smboolFalse
from ..utils.parameters import infinity
from ..logic.helpers import Bosses
class Path(object):
__slots__ = ( 'path', 'pdiff', 'distance' )
def __init__(self, path, pdiff, distance):
self.path = path
self.pdiff = pdiff
self.distance = distance
class AccessPoint(object):
# name : AccessPoint name
# graphArea : graph area the node is located in
# transitions : intra-area transitions
# traverse: traverse function, will be wand to the added transitions
# exitInfo : dict carrying vanilla door information : 'DoorPtr': door address, 'direction', 'cap', 'screen', 'bitFlag', 'distanceToSpawn', 'doorAsmPtr' : door properties
# entryInfo : dict carrying forced samus X/Y position with keys 'SamusX' and 'SamusY'.
# (to be updated after reading vanillaTransitions and gather entry info from matching exit door)
# roomInfo : dict with 'RoomPtr' : room address, 'area'
# shortName : short name for the credits
# internal : if true, shall not be used for connecting areas
def __init__(self, name, graphArea, transitions,
traverse=lambda sm: SMBool(True),
exitInfo=None, entryInfo=None, roomInfo=None,
internal=False, boss=False, escape=False,
start=None,
dotOrientation='w'):
self.Name = name
self.GraphArea = graphArea
self.ExitInfo = exitInfo
self.EntryInfo = entryInfo
self.RoomInfo = roomInfo
self.Internal = internal
self.Boss = boss
self.Escape = escape
self.Start = start
self.DotOrientation = dotOrientation
self.intraTransitions = self.sortTransitions(transitions)
self.transitions = copy.copy(self.intraTransitions)
self.traverse = traverse
self.distance = 0
# inter-area connection
self.ConnectedTo = None
def __copy__(self):
exitInfo = copy.deepcopy(self.ExitInfo) if self.ExitInfo is not None else None
entryInfo = copy.deepcopy(self.EntryInfo) if self.EntryInfo is not None else None
roomInfo = copy.deepcopy(self.RoomInfo) if self.RoomInfo is not None else None
start = copy.deepcopy(self.Start) if self.Start is not None else None
# in any case, do not copy connections
return AccessPoint(self.Name, self.GraphArea, self.intraTransitions, self.traverse,
exitInfo, entryInfo, roomInfo,
self.Internal, self.Boss, self.Escape,
start, self.DotOrientation)
def __str__(self):
return "[" + self.GraphArea + "] " + self.Name
def __repr__(self):
return self.Name
def sortTransitions(self, transitions=None):
# sort transitions before the loop in getNewAvailNodes.
# as of python3.7 insertion order is guaranteed in dictionaires.
if transitions is None:
transitions = self.transitions
return { key: transitions[key] for key in sorted(transitions.keys()) }
# connect to inter-area access point
def connect(self, destName):
self.disconnect()
if self.Internal is False:
self.transitions[destName] = self.traverse
self.ConnectedTo = destName
else:
raise RuntimeError("Cannot add an internal access point as inter-are transition")
self.transitions = self.sortTransitions()
def disconnect(self):
if self.ConnectedTo is not None:
if self.ConnectedTo not in self.intraTransitions:
del self.transitions[self.ConnectedTo]
else:
self.transitions[self.ConnectedTo] = self.intraTransitions[self.ConnectedTo]
self.ConnectedTo = None
# tells if this node is to connect areas together
def isArea(self):
return not self.Internal and not self.Boss and not self.Escape
# used by the solver to get area and boss APs
def isInternal(self):
return self.Internal or self.Escape
def isLoop(self):
return self.ConnectedTo == self.Name
class AccessGraph(object):
__slots__ = ( 'log', 'accessPoints', 'InterAreaTransitions',
'EscapeAttributes', 'apCache', '_useCache',
'availAccessPoints' )
def __init__(self, accessPointList, transitions, dotFile=None):
self.log = log.get('Graph')
self.accessPoints = {}
self.InterAreaTransitions = []
self.EscapeAttributes = {
'Timer': None,
'Animals': None
}
for ap in accessPointList:
self.addAccessPoint(ap)
for srcName, dstName in transitions:
self.addTransition(srcName, dstName)
if dotFile is not None:
self.toDot(dotFile)
self.apCache = {}
self._useCache = False
# store the avail access points to display in vcr
self.availAccessPoints = {}
def useCache(self, use):
self._useCache = use
if self._useCache:
self.resetCache()
def resetCache(self):
self.apCache = {}
def printGraph(self):
if self.log.getEffectiveLevel() == logging.DEBUG:
self.log.debug("Area graph:")
for s, d in self.InterAreaTransitions:
self.log.debug("{} -> {}".format(s.Name, d.Name))
def addAccessPoint(self, ap):
ap.distance = 0
self.accessPoints[ap.Name] = ap
def toDot(self, dotFile):
colors = ['red', 'blue', 'green', 'yellow', 'skyblue', 'violet', 'orange',
'lawngreen', 'crimson', 'chocolate', 'turquoise', 'tomato',
'navyblue', 'darkturquoise', 'green', 'blue', 'maroon', 'magenta',
'bisque', 'coral', 'chartreuse', 'chocolate', 'cyan']
with open(dotFile, "w") as f:
f.write("digraph {\n")
f.write('size="30,30!";\n')
f.write('rankdir=LR;\n')
f.write('ranksep=2.2;\n')
f.write('overlap=scale;\n')
f.write('edge [dir="both",arrowhead="box",arrowtail="box",arrowsize=0.5,fontsize=7,style=dotted];\n')
f.write('node [shape="box",fontsize=10];\n')
for area in set([ap.GraphArea for ap in self.accessPoints.values()]):
f.write(area + ";\n") # TODO area long name and color
drawn = []
i = 0
for src, dst in self.InterAreaTransitions:
if src.Name in drawn:
continue
f.write('%s:%s -> %s:%s [taillabel="%s",headlabel="%s",color=%s];\n' % (src.GraphArea, src.DotOrientation, dst.GraphArea, dst.DotOrientation, src.Name, dst.Name, colors[i]))
drawn += [src.Name,dst.Name]
i += 1
f.write("}\n")
def addTransition(self, srcName, dstName, both=True):
src = self.accessPoints[srcName]
dst = self.accessPoints[dstName]
src.connect(dstName)
self.InterAreaTransitions.append((src, dst))
if both is True:
self.addTransition(dstName, srcName, False)
# availNodes: all already available nodes
# nodesToCheck: nodes we have to check transitions for
# smbm: smbm to test logic on. if None, discard logic check, assume we can reach everything
# maxDiff: difficulty limit
# return newly opened access points
def getNewAvailNodes(self, availNodes, nodesToCheck, smbm, maxDiff, item=None):
newAvailNodes = {}
# with python >= 3.6 the insertion order in a dict is keeps when looping on the keys,
# so we no longer have to sort them.
for src in nodesToCheck:
for dstName in src.transitions:
dst = self.accessPoints[dstName]
if dst in availNodes or dst in newAvailNodes:
continue
if smbm is not None:
if self._useCache == True and (src, dst, item) in self.apCache:
diff = self.apCache[(src, dst, item)]
else:
tFunc = src.transitions[dstName]
diff = tFunc(smbm)
if self._useCache == True:
self.apCache[(src, dst, item)] = diff
else:
diff = SMBool(True)
if diff.bool and diff.difficulty <= maxDiff:
if src.GraphArea == dst.GraphArea:
dst.distance = src.distance + 0.01
else:
dst.distance = src.distance + 1
newAvailNodes[dst] = { 'difficulty': diff, 'from': src }
#self.log.debug("{} -> {}: {}".format(src.Name, dstName, diff))
return newAvailNodes
# rootNode: starting AccessPoint instance
# smbm: smbm to test logic on. if None, discard logic check, assume we can reach everything
# maxDiff: difficulty limit.
# smbm: if None, discard logic check, assume we can reach everything
# return available AccessPoint list
def getAvailableAccessPoints(self, rootNode, smbm, maxDiff, item=None):
availNodes = { rootNode : { 'difficulty' : SMBool(True, 0), 'from' : None } }
newAvailNodes = availNodes
rootNode.distance = 0
while len(newAvailNodes) > 0:
newAvailNodes = self.getNewAvailNodes(availNodes, newAvailNodes, smbm, maxDiff, item)
availNodes.update(newAvailNodes)
return availNodes
# gets path from the root AP used to compute availAps
def getPath(self, dstAp, availAps):
path = []
root = dstAp
while root != None:
path = [root] + path
root = availAps[root]['from']
return path
def getAvailAPPaths(self, availAccessPoints, locsAPs):
paths = {}
for ap in availAccessPoints:
if ap.Name in locsAPs:
path = self.getPath(ap, availAccessPoints)
pdiff = SMBool.wandmax(*(availAccessPoints[ap]['difficulty'] for ap in path))
paths[ap.Name] = Path(path, pdiff, len(path))
return paths
def getSortedAPs(self, paths, locAccessFrom):
ret = []
for apName in locAccessFrom:
path = paths.get(apName, None)
if path is None:
continue
difficulty = paths[apName].pdiff.difficulty
ret.append((difficulty if difficulty != -1 else infinity, path.distance, apName))
ret.sort()
return [apName for diff, dist, apName in ret]
# locations: locations to check
# items: collected items
# maxDiff: difficulty limit
# rootNode: starting AccessPoint
# return available locations list, also stores difficulty in locations
def getAvailableLocations(self, locations, smbm, maxDiff, rootNode='Landing Site'):
rootAp = self.accessPoints[rootNode]
self.availAccessPoints = self.getAvailableAccessPoints(rootAp, smbm, maxDiff)
availAreas = set([ap.GraphArea for ap in self.availAccessPoints.keys()])
availLocs = []
# get all the current locations APs first to only compute these paths
locsAPs = set()
for loc in locations:
for ap in loc.AccessFrom:
locsAPs.add(ap)
# sort availAccessPoints based on difficulty to take easier paths first
availAPPaths = self.getAvailAPPaths(self.availAccessPoints, locsAPs)
for loc in locations:
if loc.GraphArea not in availAreas:
loc.distance = 30000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} locDiff is area nok".format(loc.Name))
continue
locAPs = self.getSortedAPs(availAPPaths, loc.AccessFrom)
if len(locAPs) == 0:
loc.distance = 40000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} no aps".format(loc.Name))
continue
for apName in locAPs:
if apName == None:
loc.distance = 20000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} ap is none".format(loc.Name))
break
tFunc = loc.AccessFrom[apName]
ap = self.accessPoints[apName]
tdiff = tFunc(smbm)
#if loc.Name == "Kraid":
# print("{} root: {} ap: {}".format(loc.Name, rootNode, apName))
if tdiff.bool == True and tdiff.difficulty <= maxDiff:
diff = loc.Available(smbm)
if diff.bool == True:
path = availAPPaths[apName].path
#if loc.Name == "Kraid":
# print("{} path: {}".format(loc.Name, [a.Name for a in path]))
pdiff = availAPPaths[apName].pdiff
(allDiff, locDiff) = self.computeLocDiff(tdiff, diff, pdiff)
if allDiff.bool == True and allDiff.difficulty <= maxDiff:
loc.distance = ap.distance + 1
loc.accessPoint = apName
loc.difficulty = allDiff
loc.path = path
# used only by solver
loc.pathDifficulty = pdiff
loc.locDifficulty = locDiff
availLocs.append(loc)
#if loc.Name == "Kraid":
# print("{} diff: {} tdiff: {} pdiff: {}".format(loc.Name, diff, tdiff, pdiff))
break
else:
loc.distance = 1000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} allDiff is false".format(loc.Name))
else:
loc.distance = 1000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} allDiff is false".format(loc.Name))
else:
loc.distance = 10000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} tdiff is false".format(loc.Name))
if loc.difficulty is None:
#if loc.Name == "Kraid":
# print("loc: {} no difficulty in loc".format(loc.Name))
loc.distance = 100000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {}: {}".format(loc.Name, loc))
#print("availableLocs: {}".format([loc.Name for loc in availLocs]))
return availLocs
# test access from an access point to another, given an optional item
def canAccess(self, smbm, srcAccessPointName, destAccessPointName, maxDiff, item=None):
if item is not None:
smbm.addItem(item)
#print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName))
destAccessPoint = self.accessPoints[destAccessPointName]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item)
can = destAccessPoint in availAccessPoints
# if not can:
# self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()]))
if item is not None:
smbm.removeItem(item)
#print("canAccess: {}".format(can))
return can
# returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName
# (not including source ap)
# or None if no possible path
def accessPath(self, smbm, srcAccessPointName, destAccessPointName, maxDiff):
destAccessPoint = self.accessPoints[destAccessPointName]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff)
if destAccessPoint not in availAccessPoints:
return None
return self.getPath(destAccessPoint, availAccessPoints)
# gives theoretically accessible APs in the graph (no logic check)
def getAccessibleAccessPoints(self, rootNode='Landing Site'):
rootAp = self.accessPoints[rootNode]
inBossChk = lambda ap: ap.Boss and ap.Name.endswith("In")
allAreas = {dst.GraphArea for (src, dst) in self.InterAreaTransitions if not inBossChk(dst) and not dst.isLoop()}
self.log.debug("allAreas="+str(allAreas))
nonBossAPs = [ap for ap in self.getAvailableAccessPoints(rootAp, None, 0) if ap.GraphArea in allAreas]
bossesAPs = [self.accessPoints[boss+'RoomIn'] for boss in Bosses.Golden4()] + [self.accessPoints['Draygon Room Bottom']]
return nonBossAPs + bossesAPs
# gives theoretically accessible locations within a base list
# returns locations with accessible GraphArea in this graph (no logic considered)
def getAccessibleLocations(self, locations, rootNode='Landing Site'):
availAccessPoints = self.getAccessibleAccessPoints(rootNode)
self.log.debug("availAccessPoints="+str([ap.Name for ap in availAccessPoints]))
return [loc for loc in locations if any(ap.Name in loc.AccessFrom for ap in availAccessPoints)]
class AccessGraphSolver(AccessGraph):
def computeLocDiff(self, tdiff, diff, pdiff):
# tdiff: difficulty from the location's access point to the location's room
# diff: difficulty to reach the item in the location's room
# pdiff: difficulty of the path from the current access point to the location's access point
# in output we need the global difficulty but we also need to separate pdiff and (tdiff + diff)
locDiff = SMBool.wandmax(tdiff, diff)
allDiff = SMBool.wandmax(locDiff, pdiff)
return (allDiff, locDiff)
class AccessGraphRando(AccessGraph):
def computeLocDiff(self, tdiff, diff, pdiff):
allDiff = SMBool.wandmax(tdiff, diff, pdiff)
return (allDiff, None)