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
|
||||
|
||||
@@ -6,12 +6,13 @@ from ..logic.smboolmanager import SMBoolManager
|
||||
from collections import Counter
|
||||
|
||||
class ItemLocation(object):
|
||||
__slots__ = ( 'Item', 'Location', 'Accessible' )
|
||||
__slots__ = ( 'Item', 'Location', 'Accessible', 'player' )
|
||||
|
||||
def __init__(self, Item=None, Location=None, accessible=True):
|
||||
def __init__(self, Item=None, Location=None, player=0, accessible=True):
|
||||
self.Item = Item
|
||||
self.Location = Location
|
||||
self.Accessible = accessible
|
||||
self.player = player
|
||||
|
||||
def json(self):
|
||||
return {'Item': self.Item.json(), 'Location': self.Location.json()}
|
||||
|
||||
@@ -40,7 +40,7 @@ class ItemManager:
|
||||
'ETank': Item(
|
||||
Category='Energy',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Energy Tank",
|
||||
Type='ETank',
|
||||
Id=0
|
||||
@@ -48,7 +48,7 @@ class ItemManager:
|
||||
'Missile': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Missile",
|
||||
Type='Missile',
|
||||
Id=1
|
||||
@@ -56,7 +56,7 @@ class ItemManager:
|
||||
'Super': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Super Missile",
|
||||
Type='Super',
|
||||
Id=2
|
||||
@@ -64,7 +64,7 @@ class ItemManager:
|
||||
'PowerBomb': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Power Bomb",
|
||||
Type='PowerBomb',
|
||||
Id=3
|
||||
@@ -72,7 +72,7 @@ class ItemManager:
|
||||
'Bomb': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Bomb",
|
||||
Type='Bomb',
|
||||
ItemBits=0x1000,
|
||||
@@ -81,7 +81,7 @@ class ItemManager:
|
||||
'Charge': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Charge Beam",
|
||||
Type='Charge',
|
||||
BeamBits=0x1000,
|
||||
@@ -90,7 +90,7 @@ class ItemManager:
|
||||
'Ice': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Ice Beam",
|
||||
Type='Ice',
|
||||
BeamBits=0x2,
|
||||
@@ -99,7 +99,7 @@ class ItemManager:
|
||||
'HiJump': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Hi-Jump Boots",
|
||||
Type='HiJump',
|
||||
ItemBits=0x100,
|
||||
@@ -108,7 +108,7 @@ class ItemManager:
|
||||
'SpeedBooster': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Speed Booster",
|
||||
Type='SpeedBooster',
|
||||
ItemBits=0x2000,
|
||||
@@ -117,7 +117,7 @@ class ItemManager:
|
||||
'Wave': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Wave Beam",
|
||||
Type='Wave',
|
||||
BeamBits=0x1,
|
||||
@@ -126,7 +126,7 @@ class ItemManager:
|
||||
'Spazer': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Spazer",
|
||||
Type='Spazer',
|
||||
BeamBits=0x4,
|
||||
@@ -135,7 +135,7 @@ class ItemManager:
|
||||
'SpringBall': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Spring Ball",
|
||||
Type='SpringBall',
|
||||
ItemBits=0x2,
|
||||
@@ -144,7 +144,7 @@ class ItemManager:
|
||||
'Varia': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Varia Suit",
|
||||
Type='Varia',
|
||||
ItemBits=0x1,
|
||||
@@ -153,7 +153,7 @@ class ItemManager:
|
||||
'Plasma': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Plasma Beam",
|
||||
Type='Plasma',
|
||||
BeamBits=0x8,
|
||||
@@ -162,7 +162,7 @@ class ItemManager:
|
||||
'Grapple': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Grappling Beam",
|
||||
Type='Grapple',
|
||||
ItemBits=0x4000,
|
||||
@@ -171,7 +171,7 @@ class ItemManager:
|
||||
'Morph': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Morph Ball",
|
||||
Type='Morph',
|
||||
ItemBits=0x4,
|
||||
@@ -180,7 +180,7 @@ class ItemManager:
|
||||
'Reserve': Item(
|
||||
Category='Energy',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Reserve Tank",
|
||||
Type='Reserve',
|
||||
Id=20
|
||||
@@ -188,7 +188,7 @@ class ItemManager:
|
||||
'Gravity': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Gravity Suit",
|
||||
Type='Gravity',
|
||||
ItemBits=0x20,
|
||||
@@ -197,7 +197,7 @@ class ItemManager:
|
||||
'XRayScope': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="X-Ray Scope",
|
||||
Type='XRayScope',
|
||||
ItemBits=0x8000,
|
||||
@@ -206,7 +206,7 @@ class ItemManager:
|
||||
'SpaceJump': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Space Jump",
|
||||
Type='SpaceJump',
|
||||
ItemBits=0x200,
|
||||
@@ -215,7 +215,7 @@ class ItemManager:
|
||||
'ScrewAttack': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Screw Attack",
|
||||
Type='ScrewAttack',
|
||||
ItemBits= 0x8,
|
||||
@@ -247,7 +247,7 @@ class ItemManager:
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Phantoon",
|
||||
Type='Phantoon'
|
||||
Type='Phantoon',
|
||||
),
|
||||
'Draygon': Item(
|
||||
Category='Boss',
|
||||
@@ -267,6 +267,30 @@ class ItemManager:
|
||||
Name="Mother Brain",
|
||||
Type='MotherBrain',
|
||||
),
|
||||
'SporeSpawn': Item(
|
||||
Category='MiniBoss',
|
||||
Class='Boss',
|
||||
Name="Spore Spawn",
|
||||
Type='SporeSpawn',
|
||||
),
|
||||
'Crocomire': Item(
|
||||
Category='MiniBoss',
|
||||
Class='Boss',
|
||||
Name="Crocomire",
|
||||
Type='Crocomire',
|
||||
),
|
||||
'Botwoon': Item(
|
||||
Category='MiniBoss',
|
||||
Class='Boss',
|
||||
Name="Botwoon",
|
||||
Type='Botwoon',
|
||||
),
|
||||
'GoldenTorizo': Item(
|
||||
Category='MiniBoss',
|
||||
Class='Boss',
|
||||
Name="Golden Torizo",
|
||||
Type='GoldenTorizo',
|
||||
),
|
||||
# used only during escape path check
|
||||
'Hyper': Item(
|
||||
Category='Beam',
|
||||
@@ -278,7 +302,7 @@ class ItemManager:
|
||||
'ArchipelagoItem': Item(
|
||||
Category='ArchipelagoItem',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Code=0xfc20,
|
||||
Name="Generic",
|
||||
Type='ArchipelagoItem',
|
||||
Id=21
|
||||
@@ -311,11 +335,12 @@ class ItemManager:
|
||||
itemCode = item.Code + modifier
|
||||
return itemCode
|
||||
|
||||
def __init__(self, majorsSplit, qty, sm, nLocs, maxDiff):
|
||||
def __init__(self, majorsSplit, qty, sm, nLocs, bossesItems, maxDiff):
|
||||
self.qty = qty
|
||||
self.sm = sm
|
||||
self.majorsSplit = majorsSplit
|
||||
self.nLocs = nLocs
|
||||
self.bossesItems = bossesItems
|
||||
self.maxDiff = maxDiff
|
||||
self.majorClass = 'Chozo' if majorsSplit == 'Chozo' else 'Major'
|
||||
self.itemPool = []
|
||||
@@ -324,7 +349,7 @@ class ItemManager:
|
||||
self.itemPool = []
|
||||
if addBosses == True:
|
||||
# for the bosses
|
||||
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
for boss in self.bossesItems:
|
||||
self.addMinor(boss)
|
||||
|
||||
def getItemPool(self):
|
||||
@@ -372,13 +397,17 @@ class ItemManager:
|
||||
return len([item for item in self.itemPool if item.Type == itemName]) >= count
|
||||
|
||||
class ItemPoolGenerator(object):
|
||||
# 100 item locs, 5 bosses, 4 mini bosses
|
||||
maxLocs = 109
|
||||
nbBosses = 9
|
||||
|
||||
@staticmethod
|
||||
def factory(majorsSplit, itemManager, qty, sm, exclude, nLocs, maxDiff):
|
||||
if majorsSplit == 'Chozo':
|
||||
return ItemPoolGeneratorChozo(itemManager, qty, sm, maxDiff)
|
||||
elif majorsSplit == 'Plando':
|
||||
return ItemPoolGeneratorPlando(itemManager, qty, sm, exclude, nLocs, maxDiff)
|
||||
elif nLocs == 105:
|
||||
elif nLocs == ItemPoolGenerator.maxLocs:
|
||||
if majorsSplit == "Scavenger":
|
||||
return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff)
|
||||
else:
|
||||
@@ -390,7 +419,7 @@ class ItemPoolGenerator(object):
|
||||
self.itemManager = itemManager
|
||||
self.qty = qty
|
||||
self.sm = sm
|
||||
self.maxItems = 105 # 100 item locs and 5 bosses
|
||||
self.maxItems = ItemPoolGenerator.maxLocs
|
||||
self.maxEnergy = 18 # 14E, 4R
|
||||
self.maxDiff = maxDiff
|
||||
self.log = log.get('ItemPool')
|
||||
@@ -405,7 +434,7 @@ class ItemPoolGenerator(object):
|
||||
pool = self.itemManager.getItemPool()
|
||||
energy = [item for item in pool if item.Category == 'Energy']
|
||||
if len(energy) == 0:
|
||||
self.maxMinors = 0.66*(self.maxItems - 5) # 5 for bosses
|
||||
self.maxMinors = 0.66*(self.maxItems - ItemPoolGenerator.nbBosses)
|
||||
else:
|
||||
# if energy has been placed, we can be as accurate as possible
|
||||
self.maxMinors = self.maxItems - len(pool) + self.nbMinorsAlready
|
||||
@@ -675,7 +704,8 @@ class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors):
|
||||
else:
|
||||
self.maxEnergy = 8 + int(float(nLocs - 55)/50.0 * 8)
|
||||
self.log.debug("maxEnergy: "+str(self.maxEnergy))
|
||||
maxItems = self.maxItems - 10 # remove bosses and minimal minore
|
||||
# remove bosses and minimal minors
|
||||
maxItems = self.maxItems - (self.nbMinorsAlready + len(self.itemManager.bossesItems))
|
||||
self.maxEnergy = int(max(self.maxEnergy, maxItems - nMajors - self.minorLocations))
|
||||
if self.maxEnergy > 18:
|
||||
self.maxEnergy = 18
|
||||
@@ -707,7 +737,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator):
|
||||
if item == 'total':
|
||||
continue
|
||||
itemClass = 'Major'
|
||||
if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain', 'SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo']:
|
||||
itemClass = 'Minor'
|
||||
for i in range(count):
|
||||
self.itemManager.addItem(item, itemClass)
|
||||
@@ -716,7 +746,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator):
|
||||
self.log.debug("Plando: remain start: {}".format(remain))
|
||||
if remain > 0:
|
||||
# add missing bosses
|
||||
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
for boss in self.itemManager.bossesItems:
|
||||
if self.exclude['alreadyPlacedItems'][boss] == 0:
|
||||
self.itemManager.addItem(boss, 'Minor')
|
||||
self.exclude['alreadyPlacedItems'][boss] = 1
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
import utils.log, random
|
||||
|
||||
from logic.smboolmanager import SMBoolManager
|
||||
from utils.parameters import infinity
|
||||
|
||||
class MiniSolver(object):
|
||||
def __init__(self, startAP, areaGraph, restrictions):
|
||||
self.startAP = startAP
|
||||
self.areaGraph = areaGraph
|
||||
self.restrictions = restrictions
|
||||
self.settings = restrictions.settings
|
||||
self.smbm = SMBoolManager()
|
||||
self.log = utils.log.get('MiniSolver')
|
||||
|
||||
# if True, does not mean it is actually beatable, unless you're sure of it from another source of information
|
||||
# if False, it is certain it is not beatable
|
||||
def isBeatable(self, itemLocations, maxDiff=None):
|
||||
if maxDiff is None:
|
||||
maxDiff = self.settings.maxDiff
|
||||
minDiff = self.settings.minDiff
|
||||
locations = []
|
||||
for il in itemLocations:
|
||||
loc = il.Location
|
||||
if loc.restricted:
|
||||
continue
|
||||
loc.itemName = il.Item.Type
|
||||
loc.difficulty = None
|
||||
locations.append(loc)
|
||||
self.smbm.resetItems()
|
||||
ap = self.startAP
|
||||
onlyBossesLeft = -1
|
||||
hasOneLocAboveMinDiff = False
|
||||
while True:
|
||||
if not locations:
|
||||
return hasOneLocAboveMinDiff
|
||||
# only two loops to collect all remaining locations in only bosses left mode
|
||||
if onlyBossesLeft >= 0:
|
||||
onlyBossesLeft += 1
|
||||
if onlyBossesLeft > 2:
|
||||
return False
|
||||
self.areaGraph.getAvailableLocations(locations, self.smbm, maxDiff, ap)
|
||||
post = [loc for loc in locations if loc.PostAvailable and loc.difficulty.bool == True]
|
||||
for loc in post:
|
||||
self.smbm.addItem(loc.itemName)
|
||||
postAvailable = loc.PostAvailable(self.smbm)
|
||||
self.smbm.removeItem(loc.itemName)
|
||||
loc.difficulty = self.smbm.wand(loc.difficulty, postAvailable)
|
||||
toCollect = [loc for loc in locations if loc.difficulty.bool == True and loc.difficulty.difficulty <= maxDiff]
|
||||
if not toCollect:
|
||||
# mini onlyBossesLeft
|
||||
if maxDiff < infinity:
|
||||
maxDiff = infinity
|
||||
onlyBossesLeft = 0
|
||||
continue
|
||||
return False
|
||||
if not hasOneLocAboveMinDiff:
|
||||
hasOneLocAboveMinDiff = any(loc.difficulty.difficulty >= minDiff for loc in locations)
|
||||
self.smbm.addItems([loc.itemName for loc in toCollect])
|
||||
for loc in toCollect:
|
||||
locations.remove(loc)
|
||||
# if len(locations) > 0:
|
||||
# ap = random.choice([loc.accessPoint for loc in locations])
|
||||
@@ -31,7 +31,7 @@ class RandoExec(object):
|
||||
vcr = VCR(self.seedName, 'rando') if self.vcr == True else None
|
||||
self.errorMsg = ""
|
||||
split = self.randoSettings.restrictions['MajorMinor']
|
||||
graphBuilder = GraphBuilder(self.graphSettings)
|
||||
self.graphBuilder = GraphBuilder(self.graphSettings)
|
||||
container = None
|
||||
i = 0
|
||||
attempts = 500 if self.graphSettings.areaRando or self.graphSettings.doorsColorsRando or split == 'Scavenger' else 1
|
||||
@@ -44,23 +44,28 @@ class RandoExec(object):
|
||||
self.restrictions = Restrictions(self.randoSettings)
|
||||
if self.graphSettings.doorsColorsRando == True:
|
||||
DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player)
|
||||
self.areaGraph = graphBuilder.createGraph()
|
||||
self.areaGraph = self.graphBuilder.createGraph(self.randoSettings.maxDiff)
|
||||
services = RandoServices(self.areaGraph, self.restrictions)
|
||||
setup = RandoSetup(self.graphSettings, Logic.locations, services, self.player)
|
||||
self.setup = setup
|
||||
container = setup.createItemLocContainer(endDate, vcr)
|
||||
if container is None:
|
||||
sys.stdout.write('*')
|
||||
sys.stdout.flush()
|
||||
i += 1
|
||||
else:
|
||||
self.errorMsg += '\n'.join(setup.errorMsgs)
|
||||
self.errorMsg += '; '.join(setup.errorMsgs)
|
||||
now = time.process_time()
|
||||
if container is None:
|
||||
if self.graphSettings.areaRando:
|
||||
self.errorMsg += "Could not find an area layout with these settings"
|
||||
else:
|
||||
self.errorMsg += "Unable to process settings"
|
||||
self.errorMsg += "Could not find an area layout with these settings; "
|
||||
if self.graphSettings.doorsColorsRando:
|
||||
self.errorMsg += "Could not find a door color combination with these settings; "
|
||||
if split == "Scavenger":
|
||||
self.errorMsg += "Scavenger seed generation timed out; "
|
||||
if setup.errorMsgs:
|
||||
self.errorMsg += '; '.join(setup.errorMsgs)
|
||||
if self.errorMsg == "":
|
||||
self.errorMsg += "Unable to process settings; "
|
||||
self.areaGraph.printGraph()
|
||||
return container
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@ class RandoServices(object):
|
||||
self.cache = cache
|
||||
self.log = log.get('RandoServices')
|
||||
|
||||
@staticmethod
|
||||
def printProgress(s):
|
||||
sys.stdout.write(s)
|
||||
# avoid flushing I/O on pythonanywhere, as they are very slow
|
||||
if os.getenv("PYTHONANYWHERE_DOMAIN") is None:
|
||||
sys.stdout.flush()
|
||||
|
||||
# collect an item/loc with logic in a container from a given AP
|
||||
# return new AP
|
||||
def collect(self, ap, container, itemLoc, pickup=True):
|
||||
@@ -36,8 +43,7 @@ class RandoServices(object):
|
||||
self.currentLocations(ap, container)
|
||||
container.collect(itemLoc, pickup=pickup)
|
||||
self.log.debug("COLLECT "+itemLoc.Item.Type+" at "+itemLoc.Location.Name)
|
||||
sys.stdout.write('.')
|
||||
sys.stdout.flush()
|
||||
RandoServices.printProgress('.')
|
||||
return itemLoc.Location.accessPoint if pickup == True else ap
|
||||
|
||||
# gives all the possible theoretical locations for a given item
|
||||
|
||||
@@ -32,11 +32,11 @@ class RandoSettings(object):
|
||||
def isPlandoRando(self):
|
||||
return self.PlandoOptions is not None
|
||||
|
||||
def getItemManager(self, smbm, nLocs):
|
||||
def getItemManager(self, smbm, nLocs, bossesItems):
|
||||
if not self.isPlandoRando():
|
||||
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, self.maxDiff)
|
||||
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, bossesItems, self.maxDiff)
|
||||
else:
|
||||
return ItemManager('Plando', self.qty, smbm, nLocs, self.maxDiff)
|
||||
return ItemManager('Plando', self.qty, smbm, nLocs, bossesItems, self.maxDiff)
|
||||
|
||||
def getExcludeItems(self, locations):
|
||||
if not self.isPlandoRando():
|
||||
@@ -67,7 +67,11 @@ class RandoSettings(object):
|
||||
|
||||
# Holds settings and utiliy functions related to graph layout
|
||||
class GraphSettings(object):
|
||||
def __init__(self, startAP, areaRando, lightAreaRando, bossRando, escapeRando, minimizerN, dotFile, doorsColorsRando, allowGreyDoors, plandoRandoTransitions):
|
||||
def __init__(self, player, startAP, areaRando, lightAreaRando,
|
||||
bossRando, escapeRando, minimizerN, dotFile,
|
||||
doorsColorsRando, allowGreyDoors, tourian,
|
||||
plandoRandoTransitions):
|
||||
self.player = player
|
||||
self.startAP = startAP
|
||||
self.areaRando = areaRando
|
||||
self.lightAreaRando = lightAreaRando
|
||||
@@ -77,6 +81,7 @@ class GraphSettings(object):
|
||||
self.dotFile = dotFile
|
||||
self.doorsColorsRando = doorsColorsRando
|
||||
self.allowGreyDoors = allowGreyDoors
|
||||
self.tourian = tourian
|
||||
self.plandoRandoTransitions = plandoRandoTransitions
|
||||
|
||||
def isMinimizer(self):
|
||||
@@ -122,10 +127,16 @@ class ProgSpeedParameters(object):
|
||||
elif progSpeed == 'fastest':
|
||||
return 0.33
|
||||
return 0
|
||||
|
||||
# chozo/slowest can make seed generation fail often, not much
|
||||
# of a gameplay difference between slow/slowest in Chozo anyway,
|
||||
# so we merge slow and slowest for some params
|
||||
def isSlow(self, progSpeed):
|
||||
return progSpeed == "slow" or (progSpeed == "slowest" and self.restrictions.split == "Chozo")
|
||||
|
||||
def getItemLimit(self, progSpeed):
|
||||
itemLimit = self.nLocs
|
||||
if progSpeed == 'slow':
|
||||
if self.isSlow(progSpeed):
|
||||
itemLimit = int(self.nLocs*0.209) # 21 for 105
|
||||
elif progSpeed == 'medium':
|
||||
itemLimit = int(self.nLocs*0.095) # 9 for 105
|
||||
@@ -143,7 +154,7 @@ class ProgSpeedParameters(object):
|
||||
|
||||
def getLocLimit(self, progSpeed):
|
||||
locLimit = -1
|
||||
if progSpeed == 'slow':
|
||||
if self.isSlow(progSpeed):
|
||||
locLimit = 1
|
||||
elif progSpeed == 'medium':
|
||||
locLimit = 2
|
||||
@@ -158,12 +169,12 @@ class ProgSpeedParameters(object):
|
||||
if self.restrictions.isLateDoors():
|
||||
progTypes += ['Wave','Spazer','Plasma']
|
||||
progTypes.append('Charge')
|
||||
if progSpeed == 'slowest':
|
||||
if progSpeed == 'slowest' and self.restrictions.split != "Chozo":
|
||||
return progTypes
|
||||
else:
|
||||
progTypes.remove('HiJump')
|
||||
progTypes.remove('Charge')
|
||||
if progSpeed == 'slow':
|
||||
if self.isSlow(progSpeed):
|
||||
return progTypes
|
||||
else:
|
||||
progTypes.remove('Bomb')
|
||||
|
||||
@@ -9,7 +9,9 @@ from ..graph.graph_utils import getAccessPoint, GraphUtils
|
||||
from ..rando.Filler import FrontFiller
|
||||
from ..rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr
|
||||
from ..rando.Restrictions import Restrictions
|
||||
from ..utils.objectives import Objectives
|
||||
from ..utils.parameters import infinity
|
||||
from ..rom.rom_patches import RomPatches
|
||||
|
||||
# checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions
|
||||
# the entry point is createItemLocContainer
|
||||
@@ -27,7 +29,9 @@ class RandoSetup(object):
|
||||
self.allLocations = locations
|
||||
self.locations = self.areaGraph.getAccessibleLocations(locations, self.startAP)
|
||||
# print("nLocs Setup: "+str(len(self.locations)))
|
||||
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations))
|
||||
# in minimizer we can have some missing boss locs
|
||||
bossesItems = [loc.BossItemType for loc in self.locations if loc.isBoss()]
|
||||
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations), bossesItems)
|
||||
self.forbiddenItems = []
|
||||
self.restrictedLocs = []
|
||||
self.lastRestricted = []
|
||||
@@ -67,7 +71,12 @@ class RandoSetup(object):
|
||||
for loc in self.restrictedLocs:
|
||||
self.log.debug("createItemLocContainer: loc is restricted: {}".format(loc.Name))
|
||||
loc.restricted = True
|
||||
|
||||
# checkDoorBeams calls checkPool, so save error messages
|
||||
errorMsgsBck = self.errorMsgs[:]
|
||||
self.checkDoorBeams()
|
||||
self.errorMsgs = errorMsgsBck
|
||||
|
||||
self.container = ItemLocContainer(self.sm, self.getItemPool(), self.locations)
|
||||
if self.restrictions.isLateMorph():
|
||||
self.restrictions.lateMorphInit(self.startAP, self.container, self.services)
|
||||
@@ -122,7 +131,9 @@ class RandoSetup(object):
|
||||
self.log.debug("fillRestrictedLocations. locs="+getLocListStr(locs))
|
||||
for loc in locs:
|
||||
itemLocation = ItemLocation(None, loc)
|
||||
if self.container.hasItemInPool(getPred('Nothing', loc)):
|
||||
if loc.BossItemType is not None:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred(loc.BossItemType, loc))
|
||||
elif self.container.hasItemInPool(getPred('Nothing', loc)):
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Nothing', loc))
|
||||
elif self.container.hasItemInPool(getPred('NoEnergy', loc)):
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('NoEnergy', loc))
|
||||
@@ -168,10 +179,13 @@ class RandoSetup(object):
|
||||
self.log.debug("checkDoorBeams. mandatoryBeams="+str(self.restrictions.mandatoryBeams))
|
||||
|
||||
def checkPool(self, forbidden=None):
|
||||
self.errorMsgs = []
|
||||
self.log.debug("checkPool. forbidden=" + str(forbidden) + ", self.forbiddenItems=" + str(self.forbiddenItems))
|
||||
if not self.graphSettings.isMinimizer() and not self.settings.isPlandoRando() and len(self.allLocations) > len(self.locations):
|
||||
# invalid graph with looped areas
|
||||
self.log.debug("checkPool: not all areas are connected, but minimizer param is off / not a plando rando")
|
||||
msg = "not all areas are connected, but minimizer param is off / not a plando rando"
|
||||
self.log.debug("checkPool: {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
return False
|
||||
ret = True
|
||||
if forbidden is not None:
|
||||
@@ -185,7 +199,9 @@ class RandoSetup(object):
|
||||
container = ItemLocContainer(self.sm, pool, self.locations)
|
||||
except AssertionError as e:
|
||||
# invalid graph altogether
|
||||
self.log.debug("checkPool: AssertionError when creating ItemLocContainer: {}".format(e))
|
||||
msg = "AssertionError when creating ItemLocContainer: {}".format(e)
|
||||
self.log.debug("checkPool: {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
return False
|
||||
# restrict item pool in chozo: game should be finishable with chozo items only
|
||||
contPool = []
|
||||
@@ -210,25 +226,55 @@ class RandoSetup(object):
|
||||
self.lastRestricted = [loc for loc in self.locations if loc not in totalAvailLocs]
|
||||
self.log.debug("restricted=" + str([loc.Name for loc in self.lastRestricted]))
|
||||
|
||||
# check if all inter-area APs can reach each other
|
||||
interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()]
|
||||
for startAp in interAPs:
|
||||
availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff)
|
||||
for ap in interAPs:
|
||||
if not ap in availAccessPoints:
|
||||
self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name))
|
||||
# check if objectives are compatible with accessible APs
|
||||
startAP = self.areaGraph.accessPoints[self.startAP]
|
||||
availAPs = [ap.Name for ap in self.areaGraph.getAvailableAccessPoints(startAP, self.sm, self.settings.maxDiff)]
|
||||
self.log.debug("availAPs="+str(availAPs))
|
||||
for goal in Objectives.objDict[self.graphSettings.player].activeGoals:
|
||||
n, aps = goal.escapeAccessPoints
|
||||
if len(aps) == 0:
|
||||
continue
|
||||
escAPs = [ap for ap in aps if ap in availAPs]
|
||||
self.log.debug("escAPs="+str(escAPs))
|
||||
if len(escAPs) < n:
|
||||
msg = "goal '{}' impossible to complete due to area layout".format(goal.name)
|
||||
self.log.debug("checkPool. {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
ret = False
|
||||
continue
|
||||
for ap in escAPs:
|
||||
if not self.areaGraph.canAccess(self.sm, ap, "Golden Four", self.settings.maxDiff):
|
||||
msg = "goal '{}' impossible to complete due to area layout".format(goal.name)
|
||||
self.log.debug("checkPool. {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
ret = False
|
||||
if not ret:
|
||||
self.log.debug("checkPool. inter-area APs check failed")
|
||||
break
|
||||
# check if all inter-area APs can reach each other
|
||||
if ret:
|
||||
interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()]
|
||||
for startAp in interAPs:
|
||||
availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff)
|
||||
for ap in interAPs:
|
||||
if not ap in availAccessPoints:
|
||||
self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name))
|
||||
ret = False
|
||||
if not ret:
|
||||
msg = "inter-area APs check failed"
|
||||
self.log.debug("checkPool. {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
# cleanup
|
||||
self.sm.resetItems()
|
||||
self.restoreBossChecks()
|
||||
# check if we can reach/beat all bosses
|
||||
if ret:
|
||||
# always add G4 to mandatory bosses, even if not required by objectives
|
||||
mandatoryBosses = set(Objectives.objDict[self.sm.player].getMandatoryBosses() + Bosses.Golden4())
|
||||
|
||||
for loc in self.lastRestricted:
|
||||
if loc.Name in self.bossesLocs:
|
||||
ret = False
|
||||
self.log.debug("unavail Boss: " + loc.Name)
|
||||
msg = "unavail Boss: {}".format(loc.Name)
|
||||
self.log.debug("checkPool. {}".format(msg))
|
||||
if ret:
|
||||
# revive bosses
|
||||
self.sm.addItems([item.Type for item in contPool if item.Category != 'Boss'])
|
||||
@@ -238,17 +284,24 @@ class RandoSetup(object):
|
||||
and self.areaGraph.canAccess(self.sm, self.startAP, 'DraygonRoomIn', maxDiff)
|
||||
if ret:
|
||||
# see if we can beat bosses with this equipment (infinity as max diff for a "onlyBossesLeft" type check
|
||||
beatableBosses = sorted([loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()])
|
||||
beatableBosses = sorted([loc.BossItemType for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()])
|
||||
self.log.debug("checkPool. beatableBosses="+str(beatableBosses))
|
||||
ret = beatableBosses == Bosses.Golden4()
|
||||
self.log.debug("checkPool. mandatoryBosses: {}".format(mandatoryBosses))
|
||||
ret = mandatoryBosses.issubset(set(beatableBosses)) and Objectives.objDict[self.sm.player].checkLimitObjectives(beatableBosses)
|
||||
if ret:
|
||||
# check that we can then kill mother brain
|
||||
self.sm.addItems(Bosses.Golden4())
|
||||
self.sm.addItems(Bosses.Golden4() + Bosses.miniBosses())
|
||||
beatableMotherBrain = [loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.Name == 'Mother Brain']
|
||||
ret = len(beatableMotherBrain) > 0
|
||||
self.log.debug("checkPool. beatable Mother Brain={}".format(ret))
|
||||
else:
|
||||
msg = "can't kill all mandatory bosses/minibosses: {}".format(', '.join(list(mandatoryBosses - set(beatableBosses))))
|
||||
self.log.debug("checkPool. {}".format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
else:
|
||||
self.log.debug('checkPool. locked by Phantoon or Draygon')
|
||||
msg = "locked by Phantoon or Draygon"
|
||||
self.log.debug('checkPool. {}'.format(msg))
|
||||
self.errorMsgs.append(msg)
|
||||
self.log.debug('checkPool. boss access sanity check: '+str(ret))
|
||||
|
||||
if self.restrictions.isChozo() or self.restrictions.isScavenger():
|
||||
@@ -319,7 +372,6 @@ class RandoSetup(object):
|
||||
else:
|
||||
forb = []
|
||||
self.forbiddenItems += forb
|
||||
self.checkPool()
|
||||
self.addRestricted()
|
||||
return len(forb)
|
||||
|
||||
@@ -344,6 +396,9 @@ class RandoSetup(object):
|
||||
def getForbiddenMovement(self):
|
||||
self.log.debug("getForbiddenMovement BEGIN. forbidden="+str(self.forbiddenItems))
|
||||
removableMovement = [mvt for mvt in self.movementItems if self.checkPool([mvt])]
|
||||
if 'Bomb' in removableMovement and not RomPatches.has(self.sm.player, RomPatches.BombTorizoWake) and Objectives.objDict[self.sm.player].isGoalActive("activate chozo robots"):
|
||||
# in this objective, without VARIA tweaks, BT has to wake so give bombs
|
||||
removableMovement.remove('Bomb')
|
||||
self.log.debug("getForbiddenMovement removable="+str(removableMovement))
|
||||
if len(removableMovement) > 0:
|
||||
# remove at least the most important
|
||||
|
||||
@@ -14,11 +14,9 @@ class Restrictions(object):
|
||||
self.suitsRestrictions = settings.restrictions['Suits']
|
||||
self.scavLocs = None
|
||||
self.scavIsVanilla = False
|
||||
self.scavEscape = False
|
||||
self.restrictionDictChecker = None
|
||||
if self.split == 'Scavenger':
|
||||
self.scavIsVanilla = settings.restrictions['ScavengerParams']['vanillaItems']
|
||||
self.scavEscape = settings.restrictions['ScavengerParams']['escape']
|
||||
# checker function chain used by canPlaceAtLocation
|
||||
self.checkers = self.getCheckers()
|
||||
self.static = {}
|
||||
@@ -84,7 +82,7 @@ class Restrictions(object):
|
||||
self.checkers.append(self.restrictionDictChecker)
|
||||
|
||||
def isLocMajor(self, loc):
|
||||
return not loc.isBoss() and (self.split == "Full" or loc.isClass(self.split))
|
||||
return (not loc.isBoss() and self.split == "Full") or loc.isClass(self.split)
|
||||
|
||||
def isLocMinor(self, loc):
|
||||
return not loc.isBoss() and (self.split == "Full" or not loc.isClass(self.split))
|
||||
@@ -93,7 +91,7 @@ class Restrictions(object):
|
||||
if self.split == "Full":
|
||||
return True
|
||||
elif self.split == 'Scavenger':
|
||||
return not self.isItemMinor(item)
|
||||
return not self.isItemMinor(item) or item.Type == "Ridley"
|
||||
else:
|
||||
return item.Class == self.split
|
||||
|
||||
@@ -135,7 +133,7 @@ class Restrictions(object):
|
||||
def getCheckers(self):
|
||||
checkers = []
|
||||
self.log.debug("add bosses restriction")
|
||||
checkers.append(lambda item, loc, cont: (item.Category != 'Boss' and not loc.isBoss()) or (item.Category == 'Boss' and item.Name == loc.Name))
|
||||
checkers.append(lambda item, loc, cont: (item.Category not in ['Boss', 'MiniBoss'] and not loc.isBoss()) or (item.Category in ['Boss', 'MiniBoss'] and item.Type == loc.BossItemType))
|
||||
if self.split != 'Full':
|
||||
if self.split != 'Scavenger':
|
||||
self.log.debug("add majorsSplit restriction")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user