mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
SM: 20221101 update (#1479)
This adds support to most of Varia's 20221101 update. Notably, added Options for: - Objectives - Tourian - RelaxedRoundRobinCF As well as previously unsupported Options: - EscapeRando - RemoveEscapeEnemies - HideItems
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
|
||||
import random, copy
|
||||
from ..utils import log
|
||||
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets
|
||||
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets, graphAreas, getAccessPoint
|
||||
from ..logic.logic import Logic
|
||||
from ..graph.graph import AccessGraphRando as AccessGraph
|
||||
from ..logic.smbool import SMBool
|
||||
from ..utils.objectives import Objectives
|
||||
from ..rando.ItemLocContainer import getItemLocStr
|
||||
from collections import defaultdict
|
||||
|
||||
# creates graph and handles randomized escape
|
||||
class GraphBuilder(object):
|
||||
@@ -16,12 +20,37 @@ class GraphBuilder(object):
|
||||
self.log = log.get('GraphBuilder')
|
||||
|
||||
# builds everything but escape transitions
|
||||
def createGraph(self):
|
||||
def createGraph(self, maxDiff):
|
||||
transitions = self.graphSettings.plandoRandoTransitions
|
||||
if transitions is None:
|
||||
transitions = []
|
||||
if self.minimizerN is not None:
|
||||
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN)
|
||||
forcedAreas = set()
|
||||
# if no Crateria and auto escape trigger, we connect door connected to G4 to climb instead (see below).
|
||||
# This wouldn't work here, as Tourian is isolated in the resulting seed (see below again)
|
||||
# (well we could do two different transitions on both sides of doors, but that would just be confusing)
|
||||
# so we force crateria to be in the graph
|
||||
if self.graphSettings.startAP == "Golden Four" and self.graphSettings.tourian == "Disabled":
|
||||
forcedAreas.add('Crateria')
|
||||
# force areas required by objectives
|
||||
# 1st the 'clear area' ones
|
||||
forcedAreas = forcedAreas.union({goal.area for goal in Objectives.objDict[self.graphSettings.player].activeGoals if goal.area is not None})
|
||||
# for the rest, base ourselves on escapeAccessPoints :
|
||||
# - if only "1 of n" pick an area, preferably one already forced
|
||||
# - filter out G4 AP (always there)
|
||||
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
|
||||
if goal.area is None:
|
||||
n, apNames = goal.escapeAccessPoints
|
||||
aps = [getAccessPoint(apName) for apName in apNames]
|
||||
if len(aps) >= n:
|
||||
n -= len([ap for ap in aps if ap.Boss])
|
||||
escAreas = {ap.GraphArea for ap in aps if not ap.Boss}
|
||||
objForced = forcedAreas.intersection(escAreas)
|
||||
escAreasList = sorted(list(escAreas))
|
||||
while len(objForced) < n and len(escAreasList) > 0:
|
||||
objForced.add(escAreasList.pop(random.randint(0, len(escAreasList)-1)))
|
||||
forcedAreas = forcedAreas.union(objForced)
|
||||
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)))
|
||||
else:
|
||||
if not self.bossRando:
|
||||
transitions += vanillaBossesTransitions
|
||||
@@ -31,26 +60,44 @@ class GraphBuilder(object):
|
||||
transitions += vanillaTransitions
|
||||
else:
|
||||
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando)
|
||||
return AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
|
||||
ret = AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
|
||||
Objectives.objDict[self.graphSettings.player].setGraph(ret, maxDiff)
|
||||
return ret
|
||||
|
||||
def addForeignItems(self, container, itemLocs):
|
||||
itemPoolCounts = {}
|
||||
for item in container.itemPool:
|
||||
if item.Code is not None:
|
||||
itemPoolCounts[item.Type] = itemPoolCounts.get(item.Type, 0) + 1
|
||||
itemLocsCounts = {}
|
||||
for il in itemLocs:
|
||||
if il.Item.Code is not None and il.player == container.sm.player:
|
||||
itemLocsCounts[il.Item.Type] = itemLocsCounts.get(il.Item.Type, 0) + 1
|
||||
|
||||
for item, count in itemPoolCounts.items():
|
||||
for n in range(max(0, count - itemLocsCounts.get(item, 0))):
|
||||
container.sm.addItem(item)
|
||||
|
||||
# fills in escape transitions if escape rando is enabled
|
||||
# scavEscape = None or (itemLocs, scavItemLocs) couple from filler
|
||||
def escapeGraph(self, container, graph, maxDiff, scavEscape):
|
||||
# escapeTrigger = None or (itemLocs, progItemlocs) couple from filler
|
||||
def escapeGraph(self, container, graph, maxDiff, escapeTrigger):
|
||||
if not self.escapeRando:
|
||||
return True
|
||||
emptyContainer = copy.copy(container)
|
||||
emptyContainer.resetCollected(reassignItemLocs=True)
|
||||
dst = None
|
||||
if scavEscape is None:
|
||||
if escapeTrigger is None:
|
||||
possibleTargets, dst, path = self.getPossibleEscapeTargets(emptyContainer, graph, maxDiff)
|
||||
# update graph with escape transition
|
||||
graph.addTransition(escapeSource, dst)
|
||||
paths = [path]
|
||||
else:
|
||||
possibleTargets, path = self.getScavengerEscape(emptyContainer, graph, maxDiff, scavEscape)
|
||||
if path is None:
|
||||
self.addForeignItems(emptyContainer, escapeTrigger[0])
|
||||
possibleTargets, paths = self.escapeTrigger(emptyContainer, graph, maxDiff, escapeTrigger)
|
||||
if paths is None:
|
||||
return False
|
||||
# get timer value
|
||||
self.escapeTimer(graph, path, self.areaRando or scavEscape is not None)
|
||||
self.escapeTimer(graph, paths, self.areaRando or escapeTrigger is not None)
|
||||
self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer']))
|
||||
# animals
|
||||
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst)
|
||||
@@ -68,10 +115,10 @@ class GraphBuilder(object):
|
||||
|
||||
def getPossibleEscapeTargets(self, emptyContainer, graph, maxDiff):
|
||||
sm = emptyContainer.sm
|
||||
# setup smbm with item pool
|
||||
# Ice not usable because of hyper beam
|
||||
# remove energy to avoid hell runs
|
||||
# (will add bosses as well)
|
||||
# setup smbm with item pool:
|
||||
# - Ice not usable because of hyper beam
|
||||
# - remove energy to avoid hell runs
|
||||
# - (will add bosses as well)
|
||||
sm.addItems([item.Type for item in emptyContainer.itemPool if item.Type != 'Ice' and item.Category != 'Energy'])
|
||||
sm.addItem('Hyper')
|
||||
possibleTargets = self._getTargets(sm, graph, maxDiff)
|
||||
@@ -80,55 +127,167 @@ class GraphBuilder(object):
|
||||
path = graph.accessPath(sm, dst, 'Landing Site', maxDiff)
|
||||
return (possibleTargets, dst, path)
|
||||
|
||||
def getScavengerEscape(self, emptyContainer, graph, maxDiff, scavEscape):
|
||||
sm = emptyContainer.sm
|
||||
itemLocs, lastScavItemLoc = scavEscape[0], scavEscape[1][-1]
|
||||
# collect all item/locations up until last scav
|
||||
for il in itemLocs:
|
||||
emptyContainer.collect(il)
|
||||
if il == lastScavItemLoc:
|
||||
break
|
||||
def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
|
||||
container = emptyContainer
|
||||
sm = container.sm
|
||||
allItemLocs,progItemLocs,split = escapeTrigger[0],escapeTrigger[1],escapeTrigger[2]
|
||||
# check if crateria is connected, if not replace Tourian
|
||||
# connection with Climb and add special escape patch to Climb
|
||||
if not any(il.Location.GraphArea == "Crateria" for il in allItemLocs):
|
||||
escapeAttr = graph.EscapeAttributes
|
||||
if "patches" not in escapeAttr:
|
||||
escapeAttr['patches'] = []
|
||||
escapeAttr['patches'] += ['climb_disable_bomb_blocks.ips', "Climb_Asleep"]
|
||||
src, _ = next(t for t in graph.InterAreaTransitions if t[1].Name == "Golden Four")
|
||||
graph.removeTransitions("Golden Four")
|
||||
graph.addTransition(src.Name, "Climb Bottom Left")
|
||||
# disconnect the other side of G4
|
||||
graph.addTransition("Golden Four", "Golden Four")
|
||||
# remove vanilla escape transition
|
||||
graph.addTransition('Tourian Escape Room 4 Top Right', 'Tourian Escape Room 4 Top Right')
|
||||
# filter garbage itemLocs
|
||||
ilCheck = lambda il: not il.Location.isBoss() and not il.Location.restricted and il.Item.Category != "Nothing"
|
||||
# update item% objectives
|
||||
accessibleItems = [il.Item for il in allItemLocs if ilCheck(il)]
|
||||
majorUpgrades = [item.Type for item in accessibleItems if item.BeamBits != 0 or item.ItemBits != 0]
|
||||
sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades)
|
||||
if split == "Scavenger":
|
||||
# update escape access for scav with last scav loc
|
||||
lastScavItemLoc = progItemLocs[-1]
|
||||
sm.objectives.updateScavengerEscapeAccess(lastScavItemLoc.Location.accessPoint)
|
||||
sm.objectives.setScavengerHuntFunc(lambda sm, ap: sm.haveItem(lastScavItemLoc.Item.Type))
|
||||
else:
|
||||
# update "collect all items in areas" funcs
|
||||
availLocsByArea=defaultdict(list)
|
||||
for itemLoc in allItemLocs:
|
||||
if ilCheck(itemLoc) and (split.startswith("Full") or itemLoc.Location.isClass(split)):
|
||||
availLocsByArea[itemLoc.Location.GraphArea].append(itemLoc.Location.Name)
|
||||
self.log.debug("escapeTrigger. availLocsByArea="+str(availLocsByArea))
|
||||
sm.objectives.setAreaFuncs({area:lambda sm,ap:SMBool(len(container.getLocs(lambda loc: loc.Name in availLocsByArea[area]))==0) for area in availLocsByArea})
|
||||
self.log.debug("escapeTrigger. collect locs until G4 access")
|
||||
# collect all item/locations up until we can pass G4 (the escape triggers)
|
||||
itemLocs = allItemLocs[:]
|
||||
ap = "Landing Site" # dummy value it'll be overwritten at first collection
|
||||
while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)):
|
||||
il = itemLocs.pop(0)
|
||||
if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
|
||||
continue
|
||||
self.log.debug("collecting " + getItemLocStr(il))
|
||||
container.collect(il)
|
||||
ap = il.Location.accessPoint
|
||||
# final update of item% obj
|
||||
collectedLocsAccessPoints = {il.Location.accessPoint for il in container.itemLocations}
|
||||
sm.objectives.updateItemPercentEscapeAccess(list(collectedLocsAccessPoints))
|
||||
possibleTargets = self._getTargets(sm, graph, maxDiff)
|
||||
path = graph.accessPath(sm, lastScavItemLoc.Location.accessPoint, 'Landing Site', maxDiff)
|
||||
return (possibleTargets, path)
|
||||
# try to escape from all the possible objectives APs
|
||||
possiblePaths = []
|
||||
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
|
||||
n, possibleAccessPoints = goal.escapeAccessPoints
|
||||
count = 0
|
||||
for ap in possibleAccessPoints:
|
||||
self.log.debug("escapeTrigger. testing AP " + ap)
|
||||
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
|
||||
if path is not None:
|
||||
self.log.debug("escapeTrigger. add path from "+ap)
|
||||
possiblePaths.append(path)
|
||||
count += 1
|
||||
if count < n:
|
||||
# there is a goal we cannot escape from
|
||||
self.log.debug("escapeTrigger. goal %s: found %d/%d possible escapes, abort" % (goal.name, count, n))
|
||||
return (None, None)
|
||||
# try and get a path from all possible areas
|
||||
self.log.debug("escapeTrigger. completing paths")
|
||||
allAreas = {il.Location.GraphArea for il in allItemLocs if not il.Location.restricted and not il.Location.GraphArea in ["Tourian", "Ceres"]}
|
||||
def getStartArea(path):
|
||||
return path[0].GraphArea
|
||||
def apCheck(ap):
|
||||
nonlocal graph, possiblePaths
|
||||
apObj = graph.accessPoints[ap]
|
||||
return apObj.GraphArea not in [getStartArea(path) for path in possiblePaths]
|
||||
escapeAPs = [ap for ap in collectedLocsAccessPoints if apCheck(ap)]
|
||||
for ap in escapeAPs:
|
||||
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
|
||||
if path is not None:
|
||||
self.log.debug("escapeTrigger. add path from "+ap)
|
||||
possiblePaths.append(path)
|
||||
def areaPathCheck():
|
||||
nonlocal allAreas, possiblePaths
|
||||
startAreas = {getStartArea(path) for path in possiblePaths}
|
||||
return len(allAreas - startAreas) == 0
|
||||
while not areaPathCheck() and len(itemLocs) > 0:
|
||||
il = itemLocs.pop(0)
|
||||
if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
|
||||
continue
|
||||
self.log.debug("collecting " + getItemLocStr(il))
|
||||
container.collect(il)
|
||||
ap = il.Location.accessPoint
|
||||
if apCheck(ap):
|
||||
path = graph.accessPath(sm, ap, 'Landing Site', maxDiff)
|
||||
if path is not None:
|
||||
self.log.debug("escapeTrigger. add path from "+ap)
|
||||
possiblePaths.append(path)
|
||||
|
||||
return (possibleTargets, possiblePaths)
|
||||
|
||||
def _computeTimer(self, graph, path):
|
||||
traversedAreas = list(set([ap.GraphArea for ap in path]))
|
||||
self.log.debug("escapeTimer path: " + str([ap.Name for ap in path]))
|
||||
self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas))
|
||||
# rough estimates of navigation within areas to reach "borders"
|
||||
# (can obviously be completely off wrt to actual path, but on the generous side)
|
||||
traversals = {
|
||||
'Crateria':90,
|
||||
'GreenPinkBrinstar':90,
|
||||
'WreckedShip':120,
|
||||
'LowerNorfair':135,
|
||||
'WestMaridia':75,
|
||||
'EastMaridia':100,
|
||||
'RedBrinstar':75,
|
||||
'Norfair': 120,
|
||||
'Kraid': 40,
|
||||
'Crocomire': 40,
|
||||
# can't be on the path
|
||||
'Tourian': 0,
|
||||
}
|
||||
t = 90 if self.areaRando else 0
|
||||
for area in traversedAreas:
|
||||
t += traversals[area]
|
||||
t = max(t, 180)
|
||||
return t
|
||||
|
||||
|
||||
# path: as returned by AccessGraph.accessPath
|
||||
def escapeTimer(self, graph, path, compute):
|
||||
if compute == True:
|
||||
if path[0].Name == 'Climb Bottom Left':
|
||||
graph.EscapeAttributes['Timer'] = None
|
||||
return
|
||||
traversedAreas = list(set([ap.GraphArea for ap in path]))
|
||||
self.log.debug("escapeTimer path: " + str([ap.Name for ap in path]))
|
||||
self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas))
|
||||
# rough estimates of navigation within areas to reach "borders"
|
||||
# (can obviously be completely off wrt to actual path, but on the generous side)
|
||||
traversals = {
|
||||
'Crateria':90,
|
||||
'GreenPinkBrinstar':90,
|
||||
'WreckedShip':120,
|
||||
'LowerNorfair':135,
|
||||
'WestMaridia':75,
|
||||
'EastMaridia':100,
|
||||
'RedBrinstar':75,
|
||||
'Norfair': 120,
|
||||
'Kraid': 40,
|
||||
'Crocomire': 40,
|
||||
# can't be on the path
|
||||
'Tourian': 0,
|
||||
}
|
||||
t = 90 if self.areaRando else 0
|
||||
for area in traversedAreas:
|
||||
t += traversals[area]
|
||||
t = max(t, 180)
|
||||
def escapeTimer(self, graph, paths, compute):
|
||||
if len(paths) == 1:
|
||||
path = paths.pop()
|
||||
if compute == True:
|
||||
if path[0].Name == 'Climb Bottom Left':
|
||||
graph.EscapeAttributes['Timer'] = None
|
||||
return
|
||||
t = self._computeTimer(graph, path)
|
||||
else:
|
||||
escapeTargetsTimer = {
|
||||
'Climb Bottom Left': None, # vanilla
|
||||
'Green Brinstar Main Shaft Top Left': 210, # brinstar
|
||||
'Basement Left': 210, # wrecked ship
|
||||
'Business Center Mid Left': 270, # norfair
|
||||
'Crab Hole Bottom Right': 270 # maridia
|
||||
}
|
||||
t = escapeTargetsTimer[path[0].Name]
|
||||
self.log.debug("escapeTimer. t="+str(t))
|
||||
graph.EscapeAttributes['Timer'] = t
|
||||
else:
|
||||
escapeTargetsTimer = {
|
||||
'Climb Bottom Left': None, # vanilla
|
||||
'Green Brinstar Main Shaft Top Left': 210, # brinstar
|
||||
'Basement Left': 210, # wrecked ship
|
||||
'Business Center Mid Left': 270, # norfair
|
||||
'Crab Hole Bottom Right': 270 # maridia
|
||||
}
|
||||
t = escapeTargetsTimer[path[0].Name]
|
||||
self.log.debug("escapeTimer. t="+str(t))
|
||||
graph.EscapeAttributes['Timer'] = t
|
||||
assert compute
|
||||
graph.EscapeAttributes['Timer'] = 0
|
||||
timerValues = {}
|
||||
graph.EscapeAttributes['TimerTable'] = timerValues
|
||||
for path in paths:
|
||||
area = path[0].GraphArea
|
||||
prev = timerValues.get(area, 0)
|
||||
t = max(prev, self._computeTimer(graph, path))
|
||||
timerValues[area] = t
|
||||
self.log.debug("escapeTimer. area=%s, t=%d" % (area, t))
|
||||
for area in graphAreas[1:-1]: # no Ceres or Tourian
|
||||
if area not in timerValues:
|
||||
# area not in graph most probably, still write a 10 minute "ultra failsafe" value
|
||||
timerValues[area] = 600
|
||||
|
||||
Reference in New Issue
Block a user