import copy import random from ..logic.logic import Logic from ..utils.parameters import Knows from ..graph.location import locationsDict from ..rom.rom import snes_to_pc from ..utils import log # order expected by ROM patches graphAreas = [ "Ceres", "Crateria", "GreenPinkBrinstar", "RedBrinstar", "WreckedShip", "Kraid", "Norfair", "Crocomire", "LowerNorfair", "WestMaridia", "EastMaridia", "Tourian" ] vanillaTransitions = [ ('Lower Mushrooms Left', 'Green Brinstar Elevator'), ('Morph Ball Room Left', 'Green Hill Zone Top Right'), ('Moat Right', 'West Ocean Left'), ('Keyhunter Room Bottom', 'Red Brinstar Elevator'), ('Noob Bridge Right', 'Red Tower Top Left'), ('Crab Maze Left', 'Le Coude Right'), ('Kronic Boost Room Bottom Left', 'Lava Dive Right'), ('Crocomire Speedway Bottom', 'Crocomire Room Top'), ('Three Muskateers Room Left', 'Single Chamber Top Right'), ('Warehouse Entrance Left', 'East Tunnel Right'), ('East Tunnel Top Right', 'Crab Hole Bottom Left'), ('Caterpillar Room Top Right', 'Red Fish Room Left'), ('Glass Tunnel Top', 'Main Street Bottom'), ('Green Pirates Shaft Bottom Right', 'Golden Four'), ('Warehouse Entrance Right', 'Warehouse Zeela Room Left'), ('Crab Shaft Right', 'Aqueduct Top Left') ] vanillaBossesTransitions = [ ('KraidRoomOut', 'KraidRoomIn'), ('PhantoonRoomOut', 'PhantoonRoomIn'), ('DraygonRoomOut', 'DraygonRoomIn'), ('RidleyRoomOut', 'RidleyRoomIn') ] # vanilla escape transition in first position vanillaEscapeTransitions = [ ('Tourian Escape Room 4 Top Right', 'Climb Bottom Left'), ('Brinstar Pre-Map Room Right', 'Green Brinstar Main Shaft Top Left'), ('Wrecked Ship Map Room', 'Basement Left'), ('Norfair Map Room', 'Business Center Mid Left'), ('Maridia Map Room', 'Crab Hole Bottom Right') ] vanillaEscapeAnimalsTransitions = [ ('Flyway Right 0', 'Bomb Torizo Room Left'), ('Flyway Right 1', 'Bomb Torizo Room Left'), ('Flyway Right 2', 'Bomb Torizo Room Left'), ('Flyway Right 3', 'Bomb Torizo Room Left'), ('Bomb Torizo Room Left Animals', 'Flyway Right') ] escapeSource = 'Tourian Escape Room 4 Top Right' escapeTargets = ['Green Brinstar Main Shaft Top Left', 'Basement Left', 'Business Center Mid Left', 'Crab Hole Bottom Right'] locIdsByAreaAddresses = { "Ceres": snes_to_pc(0xA1F568), "Crateria": snes_to_pc(0xA1F569), "GreenPinkBrinstar": snes_to_pc(0xA1F57B), "RedBrinstar": snes_to_pc(0xA1F58C), "WreckedShip": snes_to_pc(0xA1F592), "Kraid": snes_to_pc(0xA1F59E), "Norfair": snes_to_pc(0xA1F5A2), "Crocomire": snes_to_pc(0xA1F5B2), "LowerNorfair": snes_to_pc(0xA1F5B8), "WestMaridia": snes_to_pc(0xA1F5C3), "EastMaridia": snes_to_pc(0xA1F5CB), "Tourian": snes_to_pc(0xA1F5D7) } def getAccessPoint(apName, apList=None): if apList is None: apList = Logic.accessPoints return next(ap for ap in apList if ap.Name == apName) class GraphUtils: log = log.get('GraphUtils') def getStartAccessPointNames(): return [ap.Name for ap in Logic.accessPoints if ap.Start is not None] def getStartAccessPointNamesCategory(): ret = {'regular': [], 'custom': [], 'area': []} for ap in Logic.accessPoints: if ap.Start == None: continue elif 'areaMode' in ap.Start and ap.Start['areaMode'] == True: ret['area'].append(ap.Name) elif GraphUtils.isStandardStart(ap.Name): ret['regular'].append(ap.Name) else: ret['custom'].append(ap.Name) return ret def isStandardStart(startApName): return startApName == 'Ceres' or startApName == 'Landing Site' def getPossibleStartAPs(areaMode, maxDiff, morphPlacement, player): ret = [] refused = {} allStartAPs = GraphUtils.getStartAccessPointNames() for apName in allStartAPs: start = getAccessPoint(apName).Start ok = True cause = "" if 'knows' in start: for k in start['knows']: if not Knows.knowsDict[player].knows(k, maxDiff): ok = False cause += Knows.desc[k]['display']+" is not known. " break if 'areaMode' in start and start['areaMode'] != areaMode: ok = False cause += "Start location available only with area randomization enabled. " if 'forcedEarlyMorph' in start and start['forcedEarlyMorph'] == True and morphPlacement == 'late': ok = False cause += "Start location unavailable with late morph placement. " if ok: ret.append(apName) else: refused[apName] = cause return ret, refused def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs): locs = locationsDict preserveMajLocs = [locs[locName] for locName in preserveMajLocs if locs[locName].isClass(split)] possLocs = [locs[locName] for locName in possibleMajLocs][:nLocs] GraphUtils.log.debug("possLocs="+str([loc.Name for loc in possLocs])) candidates = [loc for loc in locs.values() if loc.GraphArea == startGraphArea and loc.isClass(split) and loc not in preserveMajLocs] remLocs = [loc for loc in locs.values() if loc not in possLocs and loc not in candidates and loc.isClass(split)] newLocs = [] while len(newLocs) < nLocs: if len(candidates) == 0: candidates = remLocs loc = possLocs.pop(random.randint(0,len(possLocs)-1)) newLocs.append(loc) loc.setClass([split]) if not loc in preserveMajLocs: GraphUtils.log.debug("newMajor="+loc.Name) loc = candidates.pop(random.randint(0,len(candidates)-1)) loc.setClass(["Minor"]) GraphUtils.log.debug("replaced="+loc.Name) def getGraphPatches(startApName): ap = getAccessPoint(startApName) return ap.Start['patches'] if 'patches' in ap.Start else [] def createBossesTransitions(): transitions = vanillaBossesTransitions def isVanilla(): for t in vanillaBossesTransitions: if t not in transitions: return False return True while isVanilla(): transitions = [] srcs = [] dsts = [] for (src,dst) in vanillaBossesTransitions: srcs.append(src) dsts.append(dst) while len(srcs) > 0: src = srcs.pop(random.randint(0,len(srcs)-1)) dst = dsts.pop(random.randint(0,len(dsts)-1)) transitions.append((src,dst)) return transitions def createAreaTransitions(lightAreaRando=False): if lightAreaRando: return GraphUtils.createLightAreaTransitions() else: return GraphUtils.createRegularAreaTransitions() def createRegularAreaTransitions(apList=None, apPred=None): if apList is None: apList = Logic.accessPoints if apPred is None: apPred = lambda ap: ap.isArea() tFrom = [] tTo = [] apNames = [ap.Name for ap in apList if apPred(ap) == True] transitions = [] def findTo(trFrom): ap = getAccessPoint(trFrom, apList) fromArea = ap.GraphArea targets = [apName for apName in apNames if apName not in tTo and getAccessPoint(apName, apList).GraphArea != fromArea] if len(targets) == 0: # fallback if no area transition is found targets = [apName for apName in apNames if apName != ap.Name] if len(targets) == 0: # extreme fallback: loop on itself targets = [ap.Name] return random.choice(targets) def addTransition(src, dst): tFrom.append(src) tTo.append(dst) while len(apNames) > 0: sources = [apName for apName in apNames if apName not in tFrom] src = random.choice(sources) dst = findTo(src) transitions.append((src, dst)) addTransition(src, dst) addTransition(dst, src) toRemove = [apName for apName in apNames if apName in tFrom and apName in tTo] for apName in toRemove: apNames.remove(apName) return transitions def getAPs(apPredicate, apList=None): if apList is None: apList = Logic.accessPoints return [ap for ap in apList if apPredicate(ap) == True] def loopUnusedTransitions(transitions, apList=None): if apList is None: apList = Logic.accessPoints usedAPs = set() for (src,dst) in transitions: usedAPs.add(getAccessPoint(src, apList)) usedAPs.add(getAccessPoint(dst, apList)) unusedAPs = [ap for ap in apList if not ap.isInternal() and ap not in usedAPs] for ap in unusedAPs: transitions.append((ap.Name, ap.Name)) # crateria can be forced in corner cases def createMinimizerTransitions(startApName, locLimit, forcedAreas=None): if forcedAreas is None: forcedAreas = [] if startApName == 'Ceres': startApName = 'Landing Site' startAp = getAccessPoint(startApName) def getNLocs(locsPredicate, locList=None): if locList is None: locList = Logic.locations # leave out bosses and count post boss locs systematically return len([loc for loc in locList if locsPredicate(loc) == True and not loc.SolveArea.endswith(" Boss") and not loc.isBoss()]) availAreas = list(sorted({ap.GraphArea for ap in Logic.accessPoints if ap.GraphArea != startAp.GraphArea and getNLocs(lambda loc: loc.GraphArea == ap.GraphArea) > 0})) areas = [startAp.GraphArea] if startAp.GraphArea in forcedAreas: forcedAreas.remove(startAp.GraphArea) GraphUtils.log.debug("availAreas: {}".format(availAreas)) GraphUtils.log.debug("forcedAreas: {}".format(forcedAreas)) GraphUtils.log.debug("areas: {}".format(areas)) inBossCheck = lambda ap: ap.Boss and ap.Name.endswith("In") nLocs = 0 transitions = [] usedAPs = [] trLimit = 5 locLimit -= 3 # 3 "post boss" locs will always be available, and are filtered out in getNLocs def openTransitions(): nonlocal areas, inBossCheck, usedAPs return GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and not ap.isInternal() and not inBossCheck(ap) and not ap in usedAPs) while nLocs < locLimit or len(openTransitions()) < trLimit or len(forcedAreas) > 0: GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()])) fromAreas = availAreas if len(openTransitions()) <= 1: # dont' get stuck by adding dead ends GraphUtils.log.debug("avoid being stuck") fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1] elif len(forcedAreas) > 0: # no risk to get stuck, we can pick a forced area if necessary GraphUtils.log.debug("add forced area") fromAreas = forcedAreas elif nLocs >= locLimit: # we just need transitions, avoid adding a huge area GraphUtils.log.debug("not enough open transitions") fromAreas = [] n = trLimit - len(openTransitions()) while len(fromAreas) == 0: fromAreas = [area for area in availAreas if len(GraphUtils.getAPs(lambda ap: not ap.isInternal())) > n] n -= 1 minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas]) fromAreas = [area for area in fromAreas if getNLocs(lambda loc: loc.GraphArea == area) == minLocs] nextArea = random.choice(fromAreas) if nextArea in forcedAreas: forcedAreas.remove(nextArea) GraphUtils.log.debug("nextArea="+str(nextArea)) apCheck = lambda ap: not ap.isInternal() and not inBossCheck(ap) and ap not in usedAPs possibleSources = GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and apCheck(ap)) possibleTargets = GraphUtils.getAPs(lambda ap: ap.GraphArea == nextArea and apCheck(ap)) src = random.choice(possibleSources) dst = random.choice(possibleTargets) usedAPs += [src,dst] GraphUtils.log.debug("add transition: (src: {}, dst: {})".format(src.Name, dst.Name)) transitions.append((src.Name,dst.Name)) availAreas.remove(nextArea) areas.append(nextArea) GraphUtils.log.debug("areas: {}".format(areas)) nLocs = getNLocs(lambda loc:loc.GraphArea in areas) GraphUtils.log.debug("nLocs: {}".format(nLocs)) # we picked the areas, add transitions (bosses and tourian first) sourceAPs = openTransitions() random.shuffle(sourceAPs) targetAPs = GraphUtils.getAPs(lambda ap: (inBossCheck(ap) or ap.Name == "Golden Four") and not ap in usedAPs) random.shuffle(targetAPs) assert len(sourceAPs) >= len(targetAPs), "Minimizer: less source than target APs" while len(targetAPs) > 0: transitions.append((sourceAPs.pop().Name, targetAPs.pop().Name)) transitions += GraphUtils.createRegularAreaTransitions(sourceAPs, lambda ap: not ap.isInternal()) GraphUtils.log.debug("FINAL MINIMIZER transitions: {}".format(transitions)) GraphUtils.loopUnusedTransitions(transitions) GraphUtils.log.debug("FINAL MINIMIZER nLocs: "+str(nLocs+3)) GraphUtils.log.debug("FINAL MINIMIZER areas: "+str(areas)) return transitions def createLightAreaTransitions(): # group APs by area aps = {} totalCount = 0 for ap in Logic.accessPoints: if not ap.isArea(): continue if not ap.GraphArea in aps: aps[ap.GraphArea] = {'totalCount': 0, 'transCount': {}, 'apNames': []} aps[ap.GraphArea]['apNames'].append(ap.Name) # count number of vanilla transitions between each area for (srcName, destName) in vanillaTransitions: srcAP = getAccessPoint(srcName) destAP = getAccessPoint(destName) aps[srcAP.GraphArea]['transCount'][destAP.GraphArea] = aps[srcAP.GraphArea]['transCount'].get(destAP.GraphArea, 0) + 1 aps[srcAP.GraphArea]['totalCount'] += 1 aps[destAP.GraphArea]['transCount'][srcAP.GraphArea] = aps[destAP.GraphArea]['transCount'].get(srcAP.GraphArea, 0) + 1 aps[destAP.GraphArea]['totalCount'] += 1 totalCount += 1 transitions = [] while totalCount > 0: # choose transition srcArea = random.choice(list(aps.keys())) srcName = random.choice(aps[srcArea]['apNames']) src = getAccessPoint(srcName) destArea = random.choice(list(aps[src.GraphArea]['transCount'].keys())) destName = random.choice(aps[destArea]['apNames']) transitions.append((srcName, destName)) # update counts totalCount -= 1 aps[srcArea]['totalCount'] -= 1 aps[destArea]['totalCount'] -= 1 aps[srcArea]['transCount'][destArea] -= 1 if aps[srcArea]['transCount'][destArea] == 0: del aps[srcArea]['transCount'][destArea] aps[destArea]['transCount'][srcArea] -= 1 if aps[destArea]['transCount'][srcArea] == 0: del aps[destArea]['transCount'][srcArea] aps[srcArea]['apNames'].remove(srcName) aps[destArea]['apNames'].remove(destName) if aps[srcArea]['totalCount'] == 0: del aps[srcArea] if aps[destArea]['totalCount'] == 0: del aps[destArea] return transitions def getVanillaExit(apName): allVanillaTransitions = vanillaTransitions + vanillaBossesTransitions + vanillaEscapeTransitions for (src,dst) in allVanillaTransitions: if apName == src: return dst if apName == dst: return src return None def isEscapeAnimals(apName): return 'Flyway Right' in apName or 'Bomb Torizo Room Left' in apName # gets dict like # (RoomPtr, (vanilla entry screen X, vanilla entry screen Y)): AP def getRooms(): rooms = {} for ap in Logic.accessPoints: if ap.Internal == True: continue # special ap for random escape animals surprise if GraphUtils.isEscapeAnimals(ap.Name): continue roomPtr = ap.RoomInfo['RoomPtr'] vanillaExitName = GraphUtils.getVanillaExit(ap.Name) # special ap for random escape animals surprise if GraphUtils.isEscapeAnimals(vanillaExitName): continue connAP = getAccessPoint(vanillaExitName) entryInfo = connAP.ExitInfo rooms[(roomPtr, entryInfo['screen'], entryInfo['direction'])] = ap rooms[(roomPtr, entryInfo['screen'], (ap.EntryInfo['SamusX'], ap.EntryInfo['SamusY']))] = ap # for boss rando with incompatible ridley transition, also register this one if ap.Name == 'RidleyRoomIn': rooms[(roomPtr, (0x0, 0x1), 0x5)] = ap rooms[(roomPtr, (0x0, 0x1), (0xbf, 0x198))] = ap return rooms def escapeAnimalsTransitions(graph, possibleTargets, firstEscape): n = len(possibleTargets) assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets) GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape)) if n >= 1: # complete possibleTargets. we need at least 2: one to # hide the animals in, and one to connect the vanilla # animals door to if not any(t[1].Name == 'Climb Bottom Left' for t in graph.InterAreaTransitions): # add standard Climb if not already in graph: it can be in Crateria-less minimizer + Disabled Tourian case possibleTargets.append('Climb Bottom Left') # make the escape possibilities loop by adding back the first escape if firstEscape is not None: possibleTargets.append(firstEscape) poss = possibleTargets[:] while len(possibleTargets) < 4: possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) n = len(possibleTargets) # check if we can both hide the animals and connect the vanilla animals door to a cycling escape if n >= 2: # get actual animals: pick the first of the remaining targets (will contain a map room AP) animalsAccess = possibleTargets.pop(0) graph.EscapeAttributes['Animals'] = animalsAccess # poss will contain the remaining map room AP(s) + optional AP(s) added above, to # get the cycling 4 escapes from vanilla animals room poss = possibleTargets[:] GraphUtils.log.debug("escapeAnimalsTransitions. poss="+str(poss)) while len(possibleTargets) < 4: if len(poss) > 1: possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) else: # no more possible variety, spam the last possible escape possibleTargets.append(poss[0]) else: # failsafe: if not enough targets left, abort and do vanilla animals animalsAccess = 'Flyway Right' possibleTargets = ['Bomb Torizo Room Left'] * 4 GraphUtils.log.debug("escapeAnimalsTransitions. animalsAccess="+animalsAccess) assert len(possibleTargets) == 4, "Invalid possibleTargets list: " + str(possibleTargets) # actually add the 4 connections for successive escapes challenge basePtr = 0xADAC btDoor = getAccessPoint('Flyway Right') for i in range(len(possibleTargets)): ap = copy.copy(btDoor) ap.Name += " " + str(i) ap.ExitInfo['DoorPtr'] = basePtr + i*24 graph.addAccessPoint(ap) target = possibleTargets[i] graph.addTransition(ap.Name, target) # add the connection for animals access bt = getAccessPoint('Bomb Torizo Room Left') btCpy = copy.copy(bt) btCpy.Name += " Animals" btCpy.ExitInfo['DoorPtr'] = 0xAE00 graph.addAccessPoint(btCpy) graph.addTransition(animalsAccess, btCpy.Name) def isHorizontal(dir): # up: 0x3, 0x7 # down: 0x2, 0x6 # left: 0x1, 0x5 # right: 0x0, 0x4 return dir in [0x1, 0x5, 0x0, 0x4] def removeCap(dir): if dir < 4: return dir return dir - 4 def getDirection(src, dst): exitDir = src.ExitInfo['direction'] entryDir = dst.EntryInfo['direction'] # compatible transition if exitDir == entryDir: return exitDir # if incompatible but horizontal we keep entry dir (looks more natural) if GraphUtils.isHorizontal(exitDir) and GraphUtils.isHorizontal(entryDir): return entryDir # otherwise keep exit direction and remove cap return GraphUtils.removeCap(exitDir) def getBitFlag(srcArea, dstArea, origFlag): flags = origFlag if srcArea == dstArea: flags &= 0xBF else: flags |= 0x40 return flags def getDoorConnections(graph, areas=True, bosses=False, escape=True, escapeAnimals=True): transitions = [] if areas: transitions += vanillaTransitions if bosses: transitions += vanillaBossesTransitions if escape: transitions += vanillaEscapeTransitions if escapeAnimals: transitions += vanillaEscapeAnimalsTransitions for srcName, dstName in transitions: src = graph.accessPoints[srcName] dst = graph.accessPoints[dstName] dst.EntryInfo.update(src.ExitInfo) src.EntryInfo.update(dst.ExitInfo) connections = [] for src, dst in graph.InterAreaTransitions: if not (escape and src.Escape and dst.Escape): # area only if not bosses and src.Boss: continue # boss only if not areas and not src.Boss: continue # no random escape if not escape and src.Escape: continue conn = {} conn['ID'] = str(src) + ' -> ' + str(dst) # remove duplicates (loop transitions) if any(c['ID'] == conn['ID'] for c in connections): continue # print(conn['ID']) # where to write conn['DoorPtr'] = src.ExitInfo['DoorPtr'] # door properties conn['RoomPtr'] = dst.RoomInfo['RoomPtr'] conn['doorAsmPtr'] = dst.EntryInfo['doorAsmPtr'] if 'exitAsmPtr' in src.ExitInfo: conn['exitAsmPtr'] = src.ExitInfo['exitAsmPtr'] conn['direction'] = GraphUtils.getDirection(src, dst) conn['bitFlag'] = GraphUtils.getBitFlag(src.RoomInfo['area'], dst.RoomInfo['area'], dst.EntryInfo['bitFlag']) conn['cap'] = dst.EntryInfo['cap'] conn['screen'] = dst.EntryInfo['screen'] if conn['direction'] != src.ExitInfo['direction']: # incompatible transition conn['distanceToSpawn'] = 0 conn['SamusX'] = dst.EntryInfo['SamusX'] conn['SamusY'] = dst.EntryInfo['SamusY'] if dst.Name == 'RidleyRoomIn': # special case: spawn samus on ridley platform conn['screen'] = (0x0, 0x1) else: conn['distanceToSpawn'] = dst.EntryInfo['distanceToSpawn'] if 'song' in dst.EntryInfo: conn['song'] = dst.EntryInfo['song'] conn['songs'] = dst.RoomInfo['songs'] connections.append(conn) return connections def getDoorsPtrs2Aps(): ret = {} for ap in Logic.accessPoints: if ap.Internal == True: continue ret[ap.ExitInfo["DoorPtr"]] = ap.Name return ret def getAps2DoorsPtrs(): ret = {} for ap in Logic.accessPoints: if ap.Internal == True: continue ret[ap.Name] = ap.ExitInfo["DoorPtr"] return ret def getTransitions(addresses): # build address -> name dict doorsPtrs = GraphUtils.getDoorsPtrs2Aps() transitions = [] # (src.ExitInfo['DoorPtr'], dst.ExitInfo['DoorPtr']) for (srcDoorPtr, destDoorPtr) in addresses: transitions.append((doorsPtrs[srcDoorPtr], doorsPtrs[destDoorPtr])) return transitions def hasMixedTransitions(areaTransitions, bossTransitions): vanillaAPs = [] for (src, dest) in vanillaTransitions: vanillaAPs += [src, dest] vanillaBossesAPs = [] for (src, dest) in vanillaBossesTransitions: vanillaBossesAPs += [src, dest] for (src, dest) in areaTransitions: if src in vanillaBossesAPs or dest in vanillaBossesAPs: return True for (src, dest) in bossTransitions: if src in vanillaAPs or dest in vanillaAPs: return True return False