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:
lordlou
2023-04-09 18:35:46 -04:00
committed by GitHub
parent 0bc5a3bc8d
commit 6059b5ef66
78 changed files with 3214 additions and 24689 deletions

View File

@@ -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

View File

@@ -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()}

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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