| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  | import copy | 
					
						
							|  |  |  | import random | 
					
						
							| 
									
										
										
										
											2023-04-08 16:52:34 -04:00
										 |  |  | 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 | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | # 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: | 
					
						
							| 
									
										
										
										
											2023-03-25 14:30:38 -04:00
										 |  |  |     log = log.get('GraphUtils') | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |     # crateria can be forced in corner cases | 
					
						
							|  |  |  |     def createMinimizerTransitions(startApName, locLimit, forcedAreas=None): | 
					
						
							|  |  |  |         if forcedAreas is None: | 
					
						
							|  |  |  |             forcedAreas = [] | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |         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] | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |         if startAp.GraphArea in forcedAreas: | 
					
						
							|  |  |  |             forcedAreas.remove(startAp.GraphArea) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |         GraphUtils.log.debug("availAreas: {}".format(availAreas)) | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |         GraphUtils.log.debug("forcedAreas: {}".format(forcedAreas)) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |         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) | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |         while nLocs < locLimit or len(openTransitions()) < trLimit or len(forcedAreas) > 0: | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |             GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()])) | 
					
						
							|  |  |  |             fromAreas = availAreas | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |             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 | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |                 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 | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |                     minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas]) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |                 fromAreas = [area for area in fromAreas if getNLocs(lambda loc: loc.GraphArea == area) == minLocs] | 
					
						
							|  |  |  |             nextArea = random.choice(fromAreas) | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |             if nextArea in forcedAreas: | 
					
						
							|  |  |  |                 forcedAreas.remove(nextArea) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |             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) | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |         GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape)) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |         if n >= 1: | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |             # 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 | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |             if firstEscape is not None: | 
					
						
							|  |  |  |                 possibleTargets.append(firstEscape) | 
					
						
							|  |  |  |             poss = possibleTargets[:] | 
					
						
							|  |  |  |             while len(possibleTargets) < 4: | 
					
						
							|  |  |  |                 possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) | 
					
						
							| 
									
										
										
										
											2023-04-09 18:35:46 -04:00
										 |  |  |         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]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |         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 |