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)) def createMinimizerTransitions(startApName, locLimit): 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] GraphUtils.log.debug("availAreas: {}".format(availAreas)) 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: GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()])) fromAreas = availAreas if nLocs >= locLimit: GraphUtils.log.debug("not enough open transitions") # we just need transitions, avoid adding a huge area 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] elif len(openTransitions()) <= 1: # dont' get stuck by adding dead ends fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1] nextArea = random.choice(fromAreas) 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) # first get our list of 4 entries for escape patch if n >= 1: # get actual animals: pick one of the remaining targets animalsAccess = possibleTargets.pop() graph.EscapeAttributes['Animals'] = animalsAccess # we now have at most 3 targets left, fill up to fill cycling 4 targets for animals suprise possibleTargets.append('Climb Bottom Left') if firstEscape is not None: possibleTargets.append(firstEscape) poss = possibleTargets[:] while len(possibleTargets) < 4: possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) 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