Added Super Metroid support (#46)
Varia Randomizer based implementation LttPClient -> SNIClient
This commit is contained in:
213
worlds/sm/variaRandomizer/rando/Choice.py
Normal file
213
worlds/sm/variaRandomizer/rando/Choice.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import utils.log, random
|
||||
from utils.utils import getRangeDict, chooseFromRange
|
||||
from rando.ItemLocContainer import ItemLocation
|
||||
|
||||
# helper object to choose item/loc
|
||||
class Choice(object):
|
||||
def __init__(self, restrictions):
|
||||
self.restrictions = restrictions
|
||||
self.settings = restrictions.settings
|
||||
self.log = utils.log.get("Choice")
|
||||
|
||||
# args are return from RandoServices.getPossiblePlacements
|
||||
# return itemLoc dict, or None if no possible choice
|
||||
def chooseItemLoc(self, itemLocDict, isProg):
|
||||
return None
|
||||
|
||||
def getItemList(self, itemLocDict):
|
||||
return sorted([item for item in itemLocDict.keys()], key=lambda item: item.Type)
|
||||
|
||||
def getLocList(self, itemLocDict, item):
|
||||
return sorted(itemLocDict[item], key=lambda loc: loc.Name)
|
||||
|
||||
# simple random choice, that chooses an item first, then a locatio to put it in
|
||||
class ItemThenLocChoice(Choice):
|
||||
def __init__(self, restrictions):
|
||||
super(ItemThenLocChoice, self).__init__(restrictions)
|
||||
|
||||
def chooseItemLoc(self, itemLocDict, isProg):
|
||||
itemList = self.getItemList(itemLocDict)
|
||||
item = self.chooseItem(itemList, isProg)
|
||||
if item is None:
|
||||
return None
|
||||
locList = self.getLocList(itemLocDict, item)
|
||||
loc = self.chooseLocation(locList, item, isProg)
|
||||
if loc is None:
|
||||
return None
|
||||
return ItemLocation(item, loc)
|
||||
|
||||
def chooseItem(self, itemList, isProg):
|
||||
if len(itemList) == 0:
|
||||
return None
|
||||
if isProg:
|
||||
return self.chooseItemProg(itemList)
|
||||
else:
|
||||
return self.chooseItemRandom(itemList)
|
||||
|
||||
def chooseItemProg(self, itemList):
|
||||
return self.chooseItemRandom(itemList)
|
||||
|
||||
def chooseItemRandom(self, itemList):
|
||||
return random.choice(itemList)
|
||||
|
||||
def chooseLocation(self, locList, item, isProg):
|
||||
if len(locList) == 0:
|
||||
return None
|
||||
if isProg:
|
||||
return self.chooseLocationProg(locList, item)
|
||||
else:
|
||||
return self.chooseLocationRandom(locList)
|
||||
|
||||
def chooseLocationProg(self, locList, item):
|
||||
return self.chooseLocationRandom(locList)
|
||||
|
||||
def chooseLocationRandom(self, locList):
|
||||
return random.choice(locList)
|
||||
|
||||
# Choice specialization for prog speed based filler
|
||||
class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
|
||||
def __init__(self, restrictions, progSpeedParams, distanceProp, services):
|
||||
super(ItemThenLocChoiceProgSpeed, self).__init__(restrictions)
|
||||
self.progSpeedParams = progSpeedParams
|
||||
self.distanceProp = distanceProp
|
||||
self.services = services
|
||||
self.chooseItemFuncs = {
|
||||
'Random' : self.chooseItemRandom,
|
||||
'MinProgression' : self.chooseItemMinProgression,
|
||||
'MaxProgression' : self.chooseItemMaxProgression
|
||||
}
|
||||
self.chooseLocFuncs = {
|
||||
'Random' : self.chooseLocationRandom,
|
||||
'MinDiff' : self.chooseLocationMinDiff,
|
||||
'MaxDiff' : self.chooseLocationMaxDiff
|
||||
}
|
||||
|
||||
def currentLocations(self, item=None):
|
||||
return self.services.currentLocations(self.ap, self.container, item=item)
|
||||
|
||||
def processLateDoors(self, itemLocDict, ap, container):
|
||||
doorBeams = self.restrictions.mandatoryBeams
|
||||
def canOpenExtendedDoors(item):
|
||||
return item.Category == 'Ammo' or item.Type in doorBeams
|
||||
# exclude door items from itemLocDict
|
||||
noDoorsLocDict = {item:locList for item,locList in itemLocDict.items() if not canOpenExtendedDoors(item) or container.sm.haveItem(item.Type)}
|
||||
if len(noDoorsLocDict) > 0:
|
||||
self.log.debug('processLateDoors. no doors')
|
||||
itemLocDict.clear()
|
||||
itemLocDict.update(noDoorsLocDict)
|
||||
|
||||
def chooseItemLoc(self, itemLocDict, isProg, progressionItemLocs, ap, container):
|
||||
# if late morph, redo the late morph check if morph is the
|
||||
# only possibility since we can rollback
|
||||
canRollback = len(container.currentItems) > 0
|
||||
if self.restrictions.isLateMorph() and canRollback and len(itemLocDict) == 1:
|
||||
item, locList = list(itemLocDict.items())[0]
|
||||
if item.Type == 'Morph':
|
||||
morphLocs = self.restrictions.lateMorphCheck(container, locList)
|
||||
if morphLocs is not None:
|
||||
itemLocDict[item] = morphLocs
|
||||
else:
|
||||
return None
|
||||
# if a boss is available, choose it right away
|
||||
for item,locs in itemLocDict.items():
|
||||
if item.Category == 'Boss':
|
||||
assert len(locs) == 1 and locs[0].Name == item.Name
|
||||
return ItemLocation(item, locs[0])
|
||||
# late doors check for random door colors
|
||||
if self.restrictions.isLateDoors() and random.random() < self.lateDoorsProb:
|
||||
self.processLateDoors(itemLocDict, ap, container)
|
||||
self.progressionItemLocs = progressionItemLocs
|
||||
self.ap = ap
|
||||
self.container = container
|
||||
return super(ItemThenLocChoiceProgSpeed, self).chooseItemLoc(itemLocDict, isProg)
|
||||
|
||||
def determineParameters(self, progSpeed=None, progDiff=None):
|
||||
self.chooseLocRanges = getRangeDict(self.getChooseLocs(progDiff))
|
||||
self.chooseItemRanges = getRangeDict(self.getChooseItems(progSpeed))
|
||||
self.spreadProb = self.progSpeedParams.getSpreadFactor(progSpeed)
|
||||
self.lateDoorsProb = self.progSpeedParams.getLateDoorsProb(progSpeed)
|
||||
|
||||
def getChooseLocs(self, progDiff=None):
|
||||
if progDiff is None:
|
||||
progDiff = self.settings.progDiff
|
||||
return self.progSpeedParams.getChooseLocDict(progDiff)
|
||||
|
||||
def getChooseItems(self, progSpeed):
|
||||
if progSpeed is None:
|
||||
progSpeed = self.settings.progSpeed
|
||||
return self.progSpeedParams.getChooseItemDict(progSpeed)
|
||||
|
||||
def chooseItemProg(self, itemList):
|
||||
ret = self.getChooseFunc(self.chooseItemRanges, self.chooseItemFuncs)(itemList)
|
||||
self.log.debug('chooseItemProg. ret='+ret.Type)
|
||||
return ret
|
||||
|
||||
def chooseLocationProg(self, locs, item):
|
||||
locs = self.getLocsSpreadProgression(locs)
|
||||
random.shuffle(locs)
|
||||
ret = self.getChooseFunc(self.chooseLocRanges, self.chooseLocFuncs)(locs)
|
||||
self.log.debug('chooseLocationProg. ret='+ret.Name)
|
||||
return ret
|
||||
|
||||
# get choose function from a weighted dict
|
||||
def getChooseFunc(self, rangeDict, funcDict):
|
||||
v = chooseFromRange(rangeDict)
|
||||
|
||||
return funcDict[v]
|
||||
|
||||
def chooseItemMinProgression(self, items):
|
||||
minNewLocs = 1000
|
||||
ret = None
|
||||
|
||||
for item in items:
|
||||
newLocs = len(self.currentLocations(item))
|
||||
if newLocs < minNewLocs:
|
||||
minNewLocs = newLocs
|
||||
ret = item
|
||||
return ret
|
||||
|
||||
def chooseItemMaxProgression(self, items):
|
||||
maxNewLocs = 0
|
||||
ret = None
|
||||
|
||||
for item in items:
|
||||
newLocs = len(self.currentLocations(item))
|
||||
if newLocs > maxNewLocs:
|
||||
maxNewLocs = newLocs
|
||||
ret = item
|
||||
return ret
|
||||
|
||||
|
||||
def chooseLocationMaxDiff(self, availableLocations):
|
||||
self.log.debug("MAX")
|
||||
self.log.debug("chooseLocationMaxDiff: {}".format([(l.Name, l.difficulty) for l in availableLocations]))
|
||||
return max(availableLocations, key=lambda loc:loc.difficulty.difficulty)
|
||||
|
||||
def chooseLocationMinDiff(self, availableLocations):
|
||||
self.log.debug("MIN")
|
||||
self.log.debug("chooseLocationMinDiff: {}".format([(l.Name, l.difficulty) for l in availableLocations]))
|
||||
return min(availableLocations, key=lambda loc:loc.difficulty.difficulty)
|
||||
|
||||
def areaDistance(self, loc, otherLocs):
|
||||
areas = [getattr(l, self.distanceProp) for l in otherLocs]
|
||||
cnt = areas.count(getattr(loc, self.distanceProp))
|
||||
d = None
|
||||
if cnt == 0:
|
||||
d = 2
|
||||
else:
|
||||
d = 1.0/cnt
|
||||
return d
|
||||
|
||||
def getLocsSpreadProgression(self, availableLocations):
|
||||
split = self.restrictions.split
|
||||
cond = lambda item: ((split == 'Full' and item.Class == 'Major') or split == item.Class) and item.Category != "Energy"
|
||||
progLocs = [il.Location for il in self.progressionItemLocs if cond(il.Item)]
|
||||
distances = [self.areaDistance(loc, progLocs) for loc in availableLocations]
|
||||
maxDist = max(distances)
|
||||
locs = []
|
||||
for i in range(len(availableLocations)):
|
||||
loc = availableLocations[i]
|
||||
d = distances[i]
|
||||
if d == maxDist or random.random() >= self.spreadProb:
|
||||
locs.append(loc)
|
||||
return locs
|
||||
135
worlds/sm/variaRandomizer/rando/Filler.py
Normal file
135
worlds/sm/variaRandomizer/rando/Filler.py
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
import utils.log, copy, time, random
|
||||
|
||||
from logic.cache import RequestCache
|
||||
from rando.RandoServices import RandoServices
|
||||
from rando.Choice import ItemThenLocChoice
|
||||
from rando.RandoServices import ComebackCheckType
|
||||
from rando.ItemLocContainer import ItemLocation, getItemLocationsStr
|
||||
from utils.parameters import infinity
|
||||
from logic.helpers import diffValue2txt
|
||||
from graph.graph_utils import GraphUtils
|
||||
|
||||
# base class for fillers. a filler responsibility is to fill a given
|
||||
# ItemLocContainer while a certain condition is fulfilled (usually
|
||||
# item pool is not empty).
|
||||
# entry point is generateItems
|
||||
class Filler(object):
|
||||
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
|
||||
self.startAP = startAP
|
||||
self.cache = RequestCache()
|
||||
self.graph = graph
|
||||
self.services = RandoServices(graph, restrictions, self.cache)
|
||||
self.restrictions = restrictions
|
||||
self.settings = restrictions.settings
|
||||
self.endDate = endDate
|
||||
self.baseContainer = emptyContainer
|
||||
self.maxDiff = self.settings.maxDiff
|
||||
self.log = utils.log.get('Filler')
|
||||
|
||||
# reinit algo state
|
||||
def initFiller(self):
|
||||
self.ap = self.startAP
|
||||
self.initContainer()
|
||||
self.nSteps = 0
|
||||
self.errorMsg = ""
|
||||
self.settings.maxDiff = self.maxDiff
|
||||
self.startDate = time.process_time()
|
||||
|
||||
# sets up container initial state
|
||||
def initContainer(self):
|
||||
self.container = copy.copy(self.baseContainer)
|
||||
|
||||
# default continuation condition: item pool is not empty
|
||||
def itemPoolCondition(self):
|
||||
return not self.container.isPoolEmpty()
|
||||
|
||||
# factory for step count condition
|
||||
def createStepCountCondition(self, n):
|
||||
return lambda: self.nSteps < n
|
||||
|
||||
# calls step while condition is fulfilled and we did not hit runtime limit
|
||||
# condition: continuation condition
|
||||
# vcr: debug VCR object
|
||||
# shall return (stuck, itemLoc dict list, progression itemLoc dict list)
|
||||
def generateItems(self, condition=None, vcr=None):
|
||||
self.vcr = vcr
|
||||
self.initFiller()
|
||||
if condition is None:
|
||||
condition = self.itemPoolCondition
|
||||
isStuck = False
|
||||
date = self.startDate
|
||||
while condition() and not isStuck and date <= self.endDate:
|
||||
isStuck = not self.step()
|
||||
if not isStuck:
|
||||
self.nSteps += 1
|
||||
date = time.process_time()
|
||||
if condition() or date > self.endDate:
|
||||
isStuck = True
|
||||
if date > self.endDate:
|
||||
self.errorMsg = "Exceeded time limit of "+str(self.settings.runtimeLimit_s) +" seconds"
|
||||
else:
|
||||
self.errorMsg = "STUCK !\n"+self.container.dump()
|
||||
else:
|
||||
# check if some locations are above max diff and add relevant message
|
||||
locs = self.container.getUsedLocs(lambda loc: loc.difficulty.difficulty > self.maxDiff)
|
||||
aboveMaxDiffStr = '[ ' + ' ; '.join([loc.Name + ': ' + diffValue2txt(loc.difficulty.difficulty) for loc in locs]) + ' ]'
|
||||
if aboveMaxDiffStr != '[ ]':
|
||||
self.errorMsg += "\nMaximum difficulty could not be applied everywhere. Affected locations: {}".format(aboveMaxDiffStr)
|
||||
isStuck = False
|
||||
print('\n%d step(s) in %dms' % (self.nSteps, int((date-self.startDate)*1000)))
|
||||
if self.vcr != None:
|
||||
self.vcr.dump()
|
||||
return (isStuck, self.container.itemLocations, self.getProgressionItemLocations())
|
||||
|
||||
# helper method to collect in item/location with logic. updates self.ap and VCR
|
||||
def collect(self, itemLoc, container=None, pickup=True):
|
||||
containerArg = container
|
||||
if container is None:
|
||||
container = self.container
|
||||
location = itemLoc.Location
|
||||
item = itemLoc.Item
|
||||
pickup &= location.restricted is None or location.restricted == False
|
||||
self.ap = self.services.collect(self.ap, container, itemLoc, pickup=pickup)
|
||||
self.log.debug("AP="+self.ap)
|
||||
if self.vcr is not None and containerArg is None:
|
||||
self.vcr.addLocation(location.Name, item.Type)
|
||||
|
||||
# called by generateItems at the end to knows which particulier
|
||||
# item/locations were progression, if the info is available
|
||||
def getProgressionItemLocations(self):
|
||||
return []
|
||||
|
||||
# performs a fill step. can be multiple item/locations placement,
|
||||
# not necessarily just one.
|
||||
# return True if ok, False if stuck
|
||||
def step(self):
|
||||
pass
|
||||
|
||||
# very simple front fill algorithm with no rollback and no "softlock checks" (== dessy algorithm)
|
||||
class FrontFiller(Filler):
|
||||
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
|
||||
super(FrontFiller, self).__init__(startAP, graph, restrictions, emptyContainer, endDate)
|
||||
self.choice = ItemThenLocChoice(restrictions)
|
||||
self.stdStart = GraphUtils.isStandardStart(self.startAP)
|
||||
|
||||
def isEarlyGame(self):
|
||||
n = 2 if self.stdStart else 3
|
||||
return len(self.container.currentItems) <= n
|
||||
|
||||
# one item/loc per step
|
||||
def step(self, onlyBossCheck=False):
|
||||
self.cache.reset()
|
||||
if not self.services.can100percent(self.ap, self.container):
|
||||
comebackCheck = ComebackCheckType.ComebackWithoutItem if not self.isEarlyGame() else ComebackCheckType.NoCheck
|
||||
(itemLocDict, isProg) = self.services.getPossiblePlacements(self.ap, self.container, comebackCheck)
|
||||
else:
|
||||
(itemLocDict, isProg) = self.services.getPossiblePlacementsNoLogic(self.container)
|
||||
itemLoc = self.choice.chooseItemLoc(itemLocDict, isProg)
|
||||
if itemLoc is None:
|
||||
if onlyBossCheck == False and self.services.onlyBossesLeft(self.ap, self.container):
|
||||
self.settings.maxDiff = infinity
|
||||
return self.step(onlyBossCheck=True)
|
||||
return False
|
||||
self.collect(itemLoc)
|
||||
return True
|
||||
134
worlds/sm/variaRandomizer/rando/GraphBuilder.py
Normal file
134
worlds/sm/variaRandomizer/rando/GraphBuilder.py
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
import utils.log, random, copy
|
||||
|
||||
from graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets
|
||||
from logic.logic import Logic
|
||||
from graph.graph import AccessGraphRando as AccessGraph
|
||||
|
||||
# creates graph and handles randomized escape
|
||||
class GraphBuilder(object):
|
||||
def __init__(self, graphSettings):
|
||||
self.graphSettings = graphSettings
|
||||
self.areaRando = graphSettings.areaRando
|
||||
self.bossRando = graphSettings.bossRando
|
||||
self.escapeRando = graphSettings.escapeRando
|
||||
self.minimizerN = graphSettings.minimizerN
|
||||
self.log = utils.log.get('GraphBuilder')
|
||||
|
||||
# builds everything but escape transitions
|
||||
def createGraph(self):
|
||||
transitions = self.graphSettings.plandoRandoTransitions
|
||||
if transitions is None:
|
||||
transitions = []
|
||||
if self.minimizerN is not None:
|
||||
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN)
|
||||
else:
|
||||
if not self.bossRando:
|
||||
transitions += vanillaBossesTransitions
|
||||
else:
|
||||
transitions += GraphUtils.createBossesTransitions()
|
||||
if not self.areaRando:
|
||||
transitions += vanillaTransitions
|
||||
else:
|
||||
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando)
|
||||
return AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
|
||||
|
||||
# fills in escape transitions if escape rando is enabled
|
||||
# scavEscape = None or (itemLocs, scavItemLocs) couple from filler
|
||||
def escapeGraph(self, container, graph, maxDiff, scavEscape):
|
||||
if not self.escapeRando:
|
||||
return True
|
||||
emptyContainer = copy.copy(container)
|
||||
emptyContainer.resetCollected(reassignItemLocs=True)
|
||||
dst = None
|
||||
if scavEscape is None:
|
||||
possibleTargets, dst, path = self.getPossibleEscapeTargets(emptyContainer, graph, maxDiff)
|
||||
# update graph with escape transition
|
||||
graph.addTransition(escapeSource, dst)
|
||||
else:
|
||||
possibleTargets, path = self.getScavengerEscape(emptyContainer, graph, maxDiff, scavEscape)
|
||||
if path is None:
|
||||
return False
|
||||
# get timer value
|
||||
self.escapeTimer(graph, path, self.areaRando or scavEscape is not None)
|
||||
self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer']))
|
||||
# animals
|
||||
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst)
|
||||
return True
|
||||
|
||||
def _getTargets(self, sm, graph, maxDiff):
|
||||
possibleTargets = [target for target in escapeTargets if graph.accessPath(sm, target, 'Landing Site', maxDiff) is not None]
|
||||
self.log.debug('_getTargets. targets='+str(possibleTargets))
|
||||
# failsafe
|
||||
if len(possibleTargets) == 0:
|
||||
self.log.debug("Can't randomize escape, fallback to vanilla")
|
||||
possibleTargets.append('Climb Bottom Left')
|
||||
random.shuffle(possibleTargets)
|
||||
return possibleTargets
|
||||
|
||||
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)
|
||||
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)
|
||||
# pick one
|
||||
dst = possibleTargets.pop()
|
||||
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
|
||||
possibleTargets = self._getTargets(sm, graph, maxDiff)
|
||||
path = graph.accessPath(sm, lastScavItemLoc.Location.accessPoint, 'Landing Site', maxDiff)
|
||||
return (possibleTargets, path)
|
||||
|
||||
# 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)
|
||||
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
|
||||
248
worlds/sm/variaRandomizer/rando/ItemLocContainer.py
Normal file
248
worlds/sm/variaRandomizer/rando/ItemLocContainer.py
Normal file
@@ -0,0 +1,248 @@
|
||||
|
||||
import copy, utils.log
|
||||
|
||||
from logic.smbool import SMBool, smboolFalse
|
||||
from logic.smboolmanager import SMBoolManager
|
||||
from collections import Counter
|
||||
|
||||
class ItemLocation(object):
|
||||
__slots__ = ( 'Item', 'Location', 'Accessible' )
|
||||
|
||||
def __init__(self, Item=None, Location=None, accessible=True):
|
||||
self.Item = Item
|
||||
self.Location = Location
|
||||
self.Accessible = accessible
|
||||
|
||||
def json(self):
|
||||
return {'Item': self.Item.json(), 'Location': self.Location.json()}
|
||||
|
||||
def getItemListStr(items):
|
||||
return str(dict(Counter(["%s/%s" % (item.Type,item.Class) for item in items])))
|
||||
|
||||
def getLocListStr(locs):
|
||||
return str([loc.Name for loc in locs])
|
||||
|
||||
def getItemLocStr(itemLoc):
|
||||
return itemLoc.Item.Type + " at " + itemLoc.Location.Name
|
||||
|
||||
def getItemLocationsStr(itemLocations):
|
||||
return str([getItemLocStr(il) for il in itemLocations])
|
||||
|
||||
class ContainerSoftBackup(object):
|
||||
def __init__(self, container):
|
||||
self.itemLocations = container.itemLocations[:]
|
||||
self.itemPool = container.itemPool[:]
|
||||
self.unusedLocations = container.unusedLocations[:]
|
||||
self.currentItems = container.currentItems[:]
|
||||
|
||||
def restore(self, container, resetSM=True):
|
||||
# avoid costly deep copies of locations
|
||||
container.itemLocations = self.itemLocations[:]
|
||||
container.itemPool = self.itemPool[:]
|
||||
container.unusedLocations = self.unusedLocations[:]
|
||||
container.currentItems = self.currentItems[:]
|
||||
if resetSM:
|
||||
container.sm.resetItems()
|
||||
container.sm.addItems([it.Type for it in container.currentItems])
|
||||
|
||||
# Holds items yet to place (itemPool), locations yet to fill (unusedLocations),
|
||||
# placed items/locations (itemLocations).
|
||||
# If logic is needed, also holds a SMBoolManager (sm) and collected items so far
|
||||
# (collectedItems)
|
||||
class ItemLocContainer(object):
|
||||
def __init__(self, sm, itemPool, locations):
|
||||
self.sm = sm
|
||||
self.itemLocations = []
|
||||
self.unusedLocations = locations
|
||||
self.currentItems = []
|
||||
self.itemPool = itemPool
|
||||
self.itemPoolBackup = None
|
||||
self.unrestrictedItems = set()
|
||||
self.log = utils.log.get('ItemLocContainer')
|
||||
self.checkConsistency()
|
||||
|
||||
def checkConsistency(self):
|
||||
assert len(self.unusedLocations) == len(self.itemPool), "Item({})/Locs({}) count mismatch".format(len(self.itemPool), len(self.unusedLocations))
|
||||
|
||||
def __eq__(self, rhs):
|
||||
eq = self.currentItems == rhs.currentItems
|
||||
eq &= getLocListStr(self.unusedLocations) == getLocListStr(rhs.unusedLocations)
|
||||
eq &= self.itemPool == rhs.itemPool
|
||||
eq &= getItemLocationsStr(self.itemLocations) == getItemLocationsStr(rhs.itemLocations)
|
||||
|
||||
return eq
|
||||
|
||||
def __copy__(self):
|
||||
locs = copy.copy(self.unusedLocations)
|
||||
# we don't copy restriction state on purpose: it depends on
|
||||
# outside context we don't want to bring to the copy
|
||||
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff),
|
||||
self.itemPoolBackup[:] if self.itemPoolBackup != None else self.itemPool[:],
|
||||
locs)
|
||||
ret.currentItems = self.currentItems[:]
|
||||
ret.unrestrictedItems = copy.copy(self.unrestrictedItems)
|
||||
ret.itemLocations = [ ItemLocation(
|
||||
il.Item,
|
||||
copy.copy(il.Location)
|
||||
) for il in self.itemLocations ]
|
||||
ret.sm.addItems([item.Type for item in ret.currentItems])
|
||||
return ret
|
||||
|
||||
# create a new container based on slice predicates on items and
|
||||
# locs. both predicates must result in a consistent container
|
||||
# (same number of unused locations and not placed items)
|
||||
def slice(self, itemPoolCond, locPoolCond):
|
||||
assert self.itemPoolBackup is None, "Cannot slice a constrained container"
|
||||
locs = self.getLocs(locPoolCond)
|
||||
items = self.getItems(itemPoolCond)
|
||||
cont = ItemLocContainer(self.sm, items, locs)
|
||||
cont.currentItems = self.currentItems
|
||||
cont.itemLocations = self.itemLocations
|
||||
return copy.copy(cont)
|
||||
|
||||
# transfer collected items/locations to another container
|
||||
def transferCollected(self, dest):
|
||||
dest.currentItems = self.currentItems[:]
|
||||
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff)
|
||||
dest.sm.addItems([item.Type for item in dest.currentItems])
|
||||
dest.itemLocations = copy.copy(self.itemLocations)
|
||||
dest.unrestrictedItems = copy.copy(self.unrestrictedItems)
|
||||
|
||||
# reset collected items/locations. if reassignItemLocs is True,
|
||||
# will re-fill itemPool and unusedLocations as they were before
|
||||
# collection
|
||||
def resetCollected(self, reassignItemLocs=False):
|
||||
self.currentItems = []
|
||||
if reassignItemLocs == False:
|
||||
self.itemLocations = []
|
||||
else:
|
||||
while len(self.itemLocations) > 0:
|
||||
il = self.itemLocations.pop()
|
||||
self.itemPool.append(il.Item)
|
||||
self.unusedLocations.append(il.Location)
|
||||
self.unrestrictedItems = set()
|
||||
self.sm.resetItems()
|
||||
|
||||
def dump(self):
|
||||
return "ItemPool(%d): %s\nLocPool(%d): %s\nCollected: %s" % (len(self.itemPool), getItemListStr(self.itemPool), len(self.unusedLocations), getLocListStr(self.unusedLocations), getItemListStr(self.currentItems))
|
||||
|
||||
# temporarily restrict item pool to items fulfilling predicate
|
||||
def restrictItemPool(self, predicate):
|
||||
assert self.itemPoolBackup is None, "Item pool already restricted"
|
||||
self.itemPoolBackup = self.itemPool
|
||||
self.itemPool = [item for item in self.itemPoolBackup if predicate(item)]
|
||||
self.log.debug("restrictItemPool: "+getItemListStr(self.itemPool))
|
||||
|
||||
# remove a placed restriction
|
||||
def unrestrictItemPool(self):
|
||||
assert self.itemPoolBackup is not None, "No pool restriction to remove"
|
||||
self.itemPool = self.itemPoolBackup
|
||||
self.itemPoolBackup = None
|
||||
self.log.debug("unrestrictItemPool: "+getItemListStr(self.itemPool))
|
||||
|
||||
def removeLocation(self, location):
|
||||
if location in self.unusedLocations:
|
||||
self.unusedLocations.remove(location)
|
||||
|
||||
def removeItem(self, item):
|
||||
self.itemPool.remove(item)
|
||||
if self.itemPoolBackup is not None:
|
||||
self.itemPoolBackup.remove(item)
|
||||
|
||||
# collect an item at a location. if pickup is True, also affects logic (sm) and collectedItems
|
||||
def collect(self, itemLocation, pickup=True):
|
||||
item = itemLocation.Item
|
||||
location = itemLocation.Location
|
||||
if not location.restricted:
|
||||
self.unrestrictedItems.add(item.Type)
|
||||
if pickup == True:
|
||||
self.currentItems.append(item)
|
||||
self.sm.addItem(item.Type)
|
||||
self.removeLocation(location)
|
||||
self.itemLocations.append(itemLocation)
|
||||
self.removeItem(item)
|
||||
|
||||
def isPoolEmpty(self):
|
||||
return len(self.itemPool) == 0
|
||||
|
||||
def getNextItemInPool(self, t):
|
||||
return next((item for item in self.itemPool if item.Type == t), None)
|
||||
|
||||
def getNextItemInPoolMatching(self, predicate):
|
||||
return next((item for item in self.itemPool if predicate(item) == True), None)
|
||||
|
||||
def hasItemTypeInPool(self, t):
|
||||
return any(item.Type == t for item in self.itemPool)
|
||||
|
||||
def hasItemInPool(self, predicate):
|
||||
return any(predicate(item) == True for item in self.itemPool)
|
||||
|
||||
def hasItemCategoryInPool(self, cat):
|
||||
return any(item.Category == cat for item in self.itemPool)
|
||||
|
||||
def getNextItemInPoolFromCategory(self, cat):
|
||||
return next((item for item in self.itemPool if item.Category == cat), None)
|
||||
|
||||
def getAllItemsInPoolFromCategory(self, cat):
|
||||
return [item for item in self.itemPool if item.Category == cat]
|
||||
|
||||
def countItemTypeInPool(self, t):
|
||||
return sum(1 for item in self.itemPool if item.Type == t)
|
||||
|
||||
def countItems(self, predicate):
|
||||
return sum(1 for item in self.itemPool if predicate(item) == True)
|
||||
|
||||
# gets the items pool in the form of a dicitionary whose keys are item types
|
||||
# and values list of items of this type
|
||||
def getPoolDict(self):
|
||||
poolDict = {}
|
||||
for item in self.itemPool:
|
||||
if item.Type not in poolDict:
|
||||
poolDict[item.Type] = []
|
||||
poolDict[item.Type].append(item)
|
||||
return poolDict
|
||||
|
||||
def getLocs(self, predicate):
|
||||
return [loc for loc in self.unusedLocations if predicate(loc) == True]
|
||||
|
||||
def getItems(self, predicate):
|
||||
return [item for item in self.itemPool if predicate(item) == True]
|
||||
|
||||
def getUsedLocs(self, predicate):
|
||||
return [il.Location for il in self.itemLocations if predicate(il.Location) == True]
|
||||
|
||||
def getItemLoc(self, loc):
|
||||
for il in self.itemLocations:
|
||||
if il.Location == loc:
|
||||
return il
|
||||
|
||||
def getCollectedItems(self, predicate):
|
||||
return [item for item in self.currentItems if predicate(item) == True]
|
||||
|
||||
def hasUnrestrictedLocWithItemType(self, itemType):
|
||||
return itemType in self.unrestrictedItems
|
||||
|
||||
def getLocsForSolver(self):
|
||||
locs = []
|
||||
for il in self.itemLocations:
|
||||
loc = il.Location
|
||||
self.log.debug("getLocsForSolver: {}".format(loc.Name))
|
||||
# filter out restricted locations
|
||||
if loc.restricted:
|
||||
self.log.debug("getLocsForSolver: restricted, remove {}".format(loc.Name))
|
||||
continue
|
||||
loc.itemName = il.Item.Type
|
||||
locs.append(loc)
|
||||
return locs
|
||||
|
||||
def cleanLocsAfterSolver(self):
|
||||
# restricted locs can have their difficulty set, which can cause them to be reported in the
|
||||
# post randomization warning message about locs with diff > max diff.
|
||||
for il in self.itemLocations:
|
||||
loc = il.Location
|
||||
if loc.restricted and loc.difficulty == True:
|
||||
loc.difficulty = smboolFalse
|
||||
|
||||
def getDistinctItems(self):
|
||||
itemTypes = {item.Type for item in self.itemPool}
|
||||
return [self.getNextItemInPool(itemType) for itemType in itemTypes]
|
||||
790
worlds/sm/variaRandomizer/rando/Items.py
Normal file
790
worlds/sm/variaRandomizer/rando/Items.py
Normal file
@@ -0,0 +1,790 @@
|
||||
from utils.utils import randGaussBounds, getRangeDict, chooseFromRange
|
||||
import utils.log, logging, copy, random
|
||||
|
||||
class Item:
|
||||
__slots__ = ( 'Category', 'Class', 'Name', 'Code', 'Type', 'BeamBits', 'ItemBits', 'Id' )
|
||||
|
||||
def __init__(self, Category, Class, Name, Type, Code=None, BeamBits=0, ItemBits=0, Id=None):
|
||||
self.Category = Category
|
||||
self.Class = Class
|
||||
self.Code = Code
|
||||
self.Name = Name
|
||||
self.Type = Type
|
||||
self.BeamBits = BeamBits
|
||||
self.ItemBits = ItemBits
|
||||
self.Id = Id
|
||||
|
||||
def withClass(self, Class):
|
||||
return Item(self.Category, Class, self.Name, self.Type, self.Code, self.BeamBits, self.ItemBits)
|
||||
|
||||
def __eq__(self, other):
|
||||
# used to remove an item from a list
|
||||
return self.Type == other.Type and self.Class == other.Class
|
||||
|
||||
def __hash__(self):
|
||||
# as we define __eq__ we have to also define __hash__ to use items as dictionnary keys
|
||||
# https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||
return id(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "Item({}, {}, {}, {}, {})".format(self.Category,
|
||||
self.Class, self.Code, self.Name, self.Type)
|
||||
|
||||
def json(self):
|
||||
# as we have slots instead of dict
|
||||
return {key : getattr(self, key, None) for key in self.__slots__}
|
||||
|
||||
class ItemManager:
|
||||
Items = {
|
||||
'ETank': Item(
|
||||
Category='Energy',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Energy Tank",
|
||||
Type='ETank',
|
||||
Id=0
|
||||
),
|
||||
'Missile': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Name="Missile",
|
||||
Type='Missile',
|
||||
Id=1
|
||||
),
|
||||
'Super': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Name="Super Missile",
|
||||
Type='Super',
|
||||
Id=2
|
||||
),
|
||||
'PowerBomb': Item(
|
||||
Category='Ammo',
|
||||
Class='Minor',
|
||||
Code=0xf870,
|
||||
Name="Power Bomb",
|
||||
Type='PowerBomb',
|
||||
Id=3
|
||||
),
|
||||
'Bomb': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Bomb",
|
||||
Type='Bomb',
|
||||
ItemBits=0x1000,
|
||||
Id=4
|
||||
),
|
||||
'Charge': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Charge Beam",
|
||||
Type='Charge',
|
||||
BeamBits=0x1000,
|
||||
Id=5
|
||||
),
|
||||
'Ice': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Ice Beam",
|
||||
Type='Ice',
|
||||
BeamBits=0x2,
|
||||
Id=6
|
||||
),
|
||||
'HiJump': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Hi-Jump Boots",
|
||||
Type='HiJump',
|
||||
ItemBits=0x100,
|
||||
Id=7
|
||||
),
|
||||
'SpeedBooster': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Speed Booster",
|
||||
Type='SpeedBooster',
|
||||
ItemBits=0x2000,
|
||||
Id=8
|
||||
),
|
||||
'Wave': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Wave Beam",
|
||||
Type='Wave',
|
||||
BeamBits=0x1,
|
||||
Id=9
|
||||
),
|
||||
'Spazer': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Spazer",
|
||||
Type='Spazer',
|
||||
BeamBits=0x4,
|
||||
Id=10
|
||||
),
|
||||
'SpringBall': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Spring Ball",
|
||||
Type='SpringBall',
|
||||
ItemBits=0x2,
|
||||
Id=11
|
||||
),
|
||||
'Varia': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Varia Suit",
|
||||
Type='Varia',
|
||||
ItemBits=0x1,
|
||||
Id=12
|
||||
),
|
||||
'Plasma': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Plasma Beam",
|
||||
Type='Plasma',
|
||||
BeamBits=0x8,
|
||||
Id=15
|
||||
),
|
||||
'Grapple': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Grappling Beam",
|
||||
Type='Grapple',
|
||||
ItemBits=0x4000,
|
||||
Id=16
|
||||
),
|
||||
'Morph': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Morph Ball",
|
||||
Type='Morph',
|
||||
ItemBits=0x4,
|
||||
Id=19
|
||||
),
|
||||
'Reserve': Item(
|
||||
Category='Energy',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Reserve Tank",
|
||||
Type='Reserve',
|
||||
Id=20
|
||||
),
|
||||
'Gravity': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Gravity Suit",
|
||||
Type='Gravity',
|
||||
ItemBits=0x20,
|
||||
Id=13
|
||||
),
|
||||
'XRayScope': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="X-Ray Scope",
|
||||
Type='XRayScope',
|
||||
ItemBits=0x8000,
|
||||
Id=14
|
||||
),
|
||||
'SpaceJump': Item(
|
||||
Category='Progression',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Space Jump",
|
||||
Type='SpaceJump',
|
||||
ItemBits=0x200,
|
||||
Id=17
|
||||
),
|
||||
'ScrewAttack': Item(
|
||||
Category='Misc',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Screw Attack",
|
||||
Type='ScrewAttack',
|
||||
ItemBits= 0x8,
|
||||
Id=18
|
||||
),
|
||||
'Nothing': Item(
|
||||
Category='Nothing',
|
||||
Class='Minor',
|
||||
Code=0xbae9, # new nothing plm
|
||||
Name="Nothing",
|
||||
Type='Nothing',
|
||||
Id=22
|
||||
),
|
||||
'NoEnergy': Item(
|
||||
Category='Nothing',
|
||||
Class='Major',
|
||||
Code=0xbae9, # see above
|
||||
Name="No Energy",
|
||||
Type='NoEnergy',
|
||||
Id=23
|
||||
),
|
||||
'Kraid': Item(
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Kraid",
|
||||
Type='Kraid',
|
||||
),
|
||||
'Phantoon': Item(
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Phantoon",
|
||||
Type='Phantoon'
|
||||
),
|
||||
'Draygon': Item(
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Draygon",
|
||||
Type='Draygon',
|
||||
),
|
||||
'Ridley': Item(
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Ridley",
|
||||
Type='Ridley',
|
||||
),
|
||||
'MotherBrain': Item(
|
||||
Category='Boss',
|
||||
Class='Boss',
|
||||
Name="Mother Brain",
|
||||
Type='MotherBrain',
|
||||
),
|
||||
# used only during escape path check
|
||||
'Hyper': Item(
|
||||
Category='Beam',
|
||||
Class='Major',
|
||||
Code=0xffff,
|
||||
Name="Hyper Beam",
|
||||
Type='Hyper',
|
||||
),
|
||||
'ArchipelagoItem': Item(
|
||||
Category='ArchipelagoItem',
|
||||
Class='Major',
|
||||
Code=0xf870,
|
||||
Name="Generic",
|
||||
Type='ArchipelagoItem',
|
||||
Id=21
|
||||
)
|
||||
}
|
||||
|
||||
for itemType, item in Items.items():
|
||||
if item.Type != itemType:
|
||||
raise RuntimeError("Wrong item type for {} (expected {})".format(item, itemType))
|
||||
|
||||
@staticmethod
|
||||
def isBeam(item):
|
||||
return item.BeamBits != 0
|
||||
|
||||
@staticmethod
|
||||
def getItemTypeCode(item, itemVisibility):
|
||||
if item.Category == 'Nothing':
|
||||
if itemVisibility in ['Visible', 'Chozo']:
|
||||
modifier = 0
|
||||
elif itemVisibility == 'Hidden':
|
||||
modifier = 4
|
||||
else:
|
||||
if itemVisibility == 'Visible':
|
||||
modifier = 0
|
||||
elif itemVisibility == 'Chozo':
|
||||
modifier = 4
|
||||
elif itemVisibility == 'Hidden':
|
||||
modifier = 8
|
||||
|
||||
itemCode = item.Code + modifier
|
||||
return itemCode
|
||||
|
||||
def __init__(self, majorsSplit, qty, sm, nLocs, maxDiff):
|
||||
self.qty = qty
|
||||
self.sm = sm
|
||||
self.majorsSplit = majorsSplit
|
||||
self.nLocs = nLocs
|
||||
self.maxDiff = maxDiff
|
||||
self.majorClass = 'Chozo' if majorsSplit == 'Chozo' else 'Major'
|
||||
self.itemPool = []
|
||||
|
||||
def newItemPool(self, addBosses=True):
|
||||
self.itemPool = []
|
||||
if addBosses == True:
|
||||
# for the bosses
|
||||
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
self.addMinor(boss)
|
||||
|
||||
def getItemPool(self):
|
||||
return self.itemPool
|
||||
|
||||
def setItemPool(self, pool):
|
||||
self.itemPool = pool
|
||||
|
||||
def addItem(self, itemType, itemClass=None):
|
||||
self.itemPool.append(ItemManager.getItem(itemType, itemClass))
|
||||
|
||||
def addMinor(self, minorType):
|
||||
self.addItem(minorType, 'Minor')
|
||||
|
||||
# remove from pool an item of given type. item type has to be in original Items list.
|
||||
def removeItem(self, itemType):
|
||||
for idx, item in enumerate(self.itemPool):
|
||||
if item.Type == itemType:
|
||||
self.itemPool = self.itemPool[0:idx] + self.itemPool[idx+1:]
|
||||
return item
|
||||
|
||||
def removeForbiddenItems(self, forbiddenItems):
|
||||
# the pool is the one managed by the Randomizer
|
||||
for itemType in forbiddenItems:
|
||||
self.removeItem(itemType)
|
||||
self.addItem('NoEnergy', self.majorClass)
|
||||
return self.itemPool
|
||||
|
||||
@staticmethod
|
||||
def getItem(itemType, itemClass=None):
|
||||
if itemClass is None:
|
||||
return copy.copy(ItemManager.Items[itemType])
|
||||
else:
|
||||
return ItemManager.Items[itemType].withClass(itemClass)
|
||||
|
||||
def createItemPool(self, exclude=None):
|
||||
itemPoolGenerator = ItemPoolGenerator.factory(self.majorsSplit, self, self.qty, self.sm, exclude, self.nLocs, self.maxDiff)
|
||||
self.itemPool = itemPoolGenerator.getItemPool()
|
||||
|
||||
@staticmethod
|
||||
def getProgTypes():
|
||||
return [item for item in ItemManager.Items if ItemManager.Items[item].Category == 'Progression']
|
||||
|
||||
def hasItemInPoolCount(self, itemName, count):
|
||||
return len([item for item in self.itemPool if item.Type == itemName]) >= count
|
||||
|
||||
class ItemPoolGenerator(object):
|
||||
@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:
|
||||
if majorsSplit == "Scavenger":
|
||||
return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff)
|
||||
else:
|
||||
return ItemPoolGeneratorMajors(itemManager, qty, sm, maxDiff)
|
||||
else:
|
||||
return ItemPoolGeneratorMinimizer(itemManager, qty, sm, nLocs, maxDiff)
|
||||
|
||||
def __init__(self, itemManager, qty, sm, maxDiff):
|
||||
self.itemManager = itemManager
|
||||
self.qty = qty
|
||||
self.sm = sm
|
||||
self.maxItems = 105 # 100 item locs and 5 bosses
|
||||
self.maxEnergy = 18 # 14E, 4R
|
||||
self.maxDiff = maxDiff
|
||||
self.log = utils.log.get('ItemPool')
|
||||
|
||||
def isUltraSparseNoTanks(self):
|
||||
# if low stuff botwoon is not known there is a hard energy req of one tank, even
|
||||
# with both suits
|
||||
lowStuffBotwoon = self.sm.knowsLowStuffBotwoon()
|
||||
return random.random() < 0.5 and (lowStuffBotwoon.bool == True and lowStuffBotwoon.difficulty <= self.maxDiff)
|
||||
|
||||
def calcMaxMinors(self):
|
||||
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
|
||||
else:
|
||||
# if energy has been placed, we can be as accurate as possible
|
||||
self.maxMinors = self.maxItems - len(pool) + self.nbMinorsAlready
|
||||
|
||||
def calcMaxAmmo(self):
|
||||
self.nbMinorsAlready = 5
|
||||
# always add enough minors to pass zebetites (1100 damages) and mother brain 1 (3000 damages)
|
||||
# accounting for missile refill. so 15-10, or 10-10 if ice zeb skip is known (Ice is always in item pool)
|
||||
zebSkip = self.sm.knowsIceZebSkip()
|
||||
if zebSkip.bool == False or zebSkip.difficulty > self.maxDiff:
|
||||
self.log.debug("Add missile because ice zeb skip is not known")
|
||||
self.itemManager.addMinor('Missile')
|
||||
self.nbMinorsAlready += 1
|
||||
self.calcMaxMinors()
|
||||
self.log.debug("maxMinors: "+str(self.maxMinors))
|
||||
self.minorLocations = max(0, self.maxMinors*self.qty['minors']/100.0 - self.nbMinorsAlready)
|
||||
self.log.debug("minorLocations: {}".format(self.minorLocations))
|
||||
|
||||
# add ammo given quantity settings
|
||||
def addAmmo(self):
|
||||
self.calcMaxAmmo()
|
||||
# we have to remove the minors already added
|
||||
maxItems = min(len(self.itemManager.getItemPool()) + int(self.minorLocations), self.maxItems)
|
||||
self.log.debug("maxItems: {}, (self.maxItems={})".format(maxItems, self.maxItems))
|
||||
ammoQty = self.qty['ammo']
|
||||
if not self.qty['strictMinors']:
|
||||
rangeDict = getRangeDict(ammoQty)
|
||||
self.log.debug("rangeDict: {}".format(rangeDict))
|
||||
while len(self.itemManager.getItemPool()) < maxItems:
|
||||
item = chooseFromRange(rangeDict)
|
||||
self.itemManager.addMinor(item)
|
||||
else:
|
||||
minorsTypes = ['Missile', 'Super', 'PowerBomb']
|
||||
totalProps = sum(ammoQty[m] for m in minorsTypes)
|
||||
minorsByProp = sorted(minorsTypes, key=lambda m: ammoQty[m])
|
||||
totalMinorLocations = self.minorLocations + self.nbMinorsAlready
|
||||
self.log.debug("totalMinorLocations: {}".format(totalMinorLocations))
|
||||
def ammoCount(ammo):
|
||||
return float(len([item for item in self.itemManager.getItemPool() if item.Type == ammo]))
|
||||
def targetRatio(ammo):
|
||||
return round(float(ammoQty[ammo])/totalProps, 3)
|
||||
def cmpRatio(ammo, ratio):
|
||||
thisAmmo = ammoCount(ammo)
|
||||
thisRatio = round(thisAmmo/totalMinorLocations, 3)
|
||||
nextRatio = round((thisAmmo + 1)/totalMinorLocations, 3)
|
||||
self.log.debug("{} current, next/target ratio: {}, {}/{}".format(ammo, thisRatio, nextRatio, ratio))
|
||||
return abs(nextRatio - ratio) < abs(thisRatio - ratio)
|
||||
def fillAmmoType(ammo, checkRatio=True):
|
||||
ratio = targetRatio(ammo)
|
||||
self.log.debug("{}: target ratio: {}".format(ammo, ratio))
|
||||
while len(self.itemManager.getItemPool()) < maxItems and (not checkRatio or cmpRatio(ammo, ratio)):
|
||||
self.log.debug("Add {}".format(ammo))
|
||||
self.itemManager.addMinor(ammo)
|
||||
for m in minorsByProp:
|
||||
fillAmmoType(m)
|
||||
# now that the ratios have been matched as exactly as possible, we distribute the error
|
||||
def getError(m, countOffset=0):
|
||||
return abs((ammoCount(m)+countOffset)/totalMinorLocations - targetRatio(m))
|
||||
while len(self.itemManager.getItemPool()) < maxItems:
|
||||
minNextError = 1000
|
||||
chosenAmmo = None
|
||||
for m in minorsByProp:
|
||||
nextError = getError(m, 1)
|
||||
if nextError < minNextError:
|
||||
minNextError = nextError
|
||||
chosenAmmo = m
|
||||
self.itemManager.addMinor(chosenAmmo)
|
||||
# fill up the rest with blank items
|
||||
for i in range(self.maxItems - maxItems):
|
||||
self.itemManager.addMinor('Nothing')
|
||||
|
||||
class ItemPoolGeneratorChozo(ItemPoolGenerator):
|
||||
def addEnergy(self):
|
||||
total = 18
|
||||
energyQty = self.qty['energy']
|
||||
if energyQty == 'ultra sparse':
|
||||
# 0-1, remove reserve tank and two etanks, check if it also remove the last etank
|
||||
self.itemManager.removeItem('Reserve')
|
||||
self.itemManager.addItem('NoEnergy', 'Chozo')
|
||||
self.itemManager.removeItem('ETank')
|
||||
self.itemManager.addItem('NoEnergy', 'Chozo')
|
||||
self.itemManager.removeItem('ETank')
|
||||
self.itemManager.addItem('NoEnergy', 'Chozo')
|
||||
if self.isUltraSparseNoTanks():
|
||||
# no etank nor reserve
|
||||
self.itemManager.removeItem('ETank')
|
||||
self.itemManager.addItem('NoEnergy', 'Chozo')
|
||||
elif random.random() < 0.5:
|
||||
# replace only etank with reserve
|
||||
self.itemManager.removeItem('ETank')
|
||||
self.itemManager.addItem('Reserve', 'Chozo')
|
||||
|
||||
# complete up to 18 energies with nothing item
|
||||
alreadyInPool = 4
|
||||
for i in range(total - alreadyInPool):
|
||||
self.itemManager.addItem('Nothing', 'Minor')
|
||||
elif energyQty == 'sparse':
|
||||
# 4-6
|
||||
# already 3E and 1R
|
||||
alreadyInPool = 4
|
||||
rest = randGaussBounds(2, 5)
|
||||
if rest >= 1:
|
||||
if random.random() < 0.5:
|
||||
self.itemManager.addItem('Reserve', 'Minor')
|
||||
else:
|
||||
self.itemManager.addItem('ETank', 'Minor')
|
||||
for i in range(rest-1):
|
||||
self.itemManager.addItem('ETank', 'Minor')
|
||||
# complete up to 18 energies with nothing item
|
||||
for i in range(total - alreadyInPool - rest):
|
||||
self.itemManager.addItem('Nothing', 'Minor')
|
||||
elif energyQty == 'medium':
|
||||
# 8-12
|
||||
# add up to 3 Reserves or ETanks (cannot add more than 3 reserves)
|
||||
for i in range(3):
|
||||
if random.random() < 0.5:
|
||||
self.itemManager.addItem('Reserve', 'Minor')
|
||||
else:
|
||||
self.itemManager.addItem('ETank', 'Minor')
|
||||
# 7 already in the pool (3 E, 1 R, + the previous 3)
|
||||
alreadyInPool = 7
|
||||
rest = 1 + randGaussBounds(4, 3.7)
|
||||
for i in range(rest):
|
||||
self.itemManager.addItem('ETank', 'Minor')
|
||||
# fill the rest with NoEnergy
|
||||
for i in range(total - alreadyInPool - rest):
|
||||
self.itemManager.addItem('Nothing', 'Minor')
|
||||
else:
|
||||
# add the vanilla 3 reserves and 13 Etanks
|
||||
for i in range(3):
|
||||
self.itemManager.addItem('Reserve', 'Minor')
|
||||
for i in range(11):
|
||||
self.itemManager.addItem('ETank', 'Minor')
|
||||
|
||||
def getItemPool(self):
|
||||
self.itemManager.newItemPool()
|
||||
# 25 locs: 16 majors, 3 etanks, 1 reserve, 2 missile, 2 supers, 1 pb
|
||||
for itemType in ['ETank', 'ETank', 'ETank', 'Reserve', 'Missile', 'Missile', 'Super', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack']:
|
||||
self.itemManager.addItem(itemType, 'Chozo')
|
||||
|
||||
self.addEnergy()
|
||||
self.addAmmo()
|
||||
|
||||
return self.itemManager.getItemPool()
|
||||
|
||||
class ItemPoolGeneratorMajors(ItemPoolGenerator):
|
||||
def __init__(self, itemManager, qty, sm, maxDiff):
|
||||
super(ItemPoolGeneratorMajors, self).__init__(itemManager, qty, sm, maxDiff)
|
||||
self.sparseRest = 1 + randGaussBounds(2, 5)
|
||||
self.mediumRest = 3 + randGaussBounds(4, 3.7)
|
||||
self.ultraSparseNoTanks = self.isUltraSparseNoTanks()
|
||||
|
||||
def addNoEnergy(self):
|
||||
self.itemManager.addItem('NoEnergy')
|
||||
|
||||
def addEnergy(self):
|
||||
total = self.maxEnergy
|
||||
alreadyInPool = 2
|
||||
def getE(toAdd):
|
||||
nonlocal total, alreadyInPool
|
||||
d = total - alreadyInPool - toAdd
|
||||
if d < 0:
|
||||
toAdd += d
|
||||
return toAdd
|
||||
energyQty = self.qty['energy']
|
||||
if energyQty == 'ultra sparse':
|
||||
# 0-1, add up to one energy (etank or reserve)
|
||||
self.itemManager.removeItem('Reserve')
|
||||
self.itemManager.removeItem('ETank')
|
||||
self.addNoEnergy()
|
||||
if self.ultraSparseNoTanks:
|
||||
# no energy at all
|
||||
self.addNoEnergy()
|
||||
else:
|
||||
if random.random() < 0.5:
|
||||
self.itemManager.addItem('ETank')
|
||||
else:
|
||||
self.itemManager.addItem('Reserve')
|
||||
|
||||
# complete with nothing item
|
||||
for i in range(total - alreadyInPool):
|
||||
self.addNoEnergy()
|
||||
|
||||
elif energyQty == 'sparse':
|
||||
# 4-6
|
||||
if random.random() < 0.5:
|
||||
self.itemManager.addItem('Reserve')
|
||||
else:
|
||||
self.itemManager.addItem('ETank')
|
||||
# 3 in the pool (1 E, 1 R + the previous one)
|
||||
alreadyInPool = 3
|
||||
rest = self.sparseRest
|
||||
for i in range(rest):
|
||||
self.itemManager.addItem('ETank')
|
||||
# complete with nothing item
|
||||
for i in range(total - alreadyInPool - rest):
|
||||
self.addNoEnergy()
|
||||
|
||||
elif energyQty == 'medium':
|
||||
# 8-12
|
||||
# add up to 3 Reserves or ETanks (cannot add more than 3 reserves)
|
||||
alreadyInPool = 2
|
||||
n = getE(3)
|
||||
for i in range(n):
|
||||
if random.random() < 0.5:
|
||||
self.itemManager.addItem('Reserve')
|
||||
else:
|
||||
self.itemManager.addItem('ETank')
|
||||
alreadyInPool += n
|
||||
rest = getE(self.mediumRest)
|
||||
for i in range(rest):
|
||||
self.itemManager.addItem('ETank')
|
||||
# fill the rest with NoEnergy
|
||||
for i in range(total - alreadyInPool - rest):
|
||||
self.addNoEnergy()
|
||||
else:
|
||||
nE = getE(13)
|
||||
alreadyInPool += nE
|
||||
nR = getE(3)
|
||||
alreadyInPool += nR
|
||||
for i in range(nR):
|
||||
self.itemManager.addItem('Reserve')
|
||||
for i in range(nE):
|
||||
self.itemManager.addItem('ETank')
|
||||
for i in range(total - alreadyInPool):
|
||||
self.addNoEnergy()
|
||||
|
||||
def getItemPool(self):
|
||||
self.itemManager.newItemPool()
|
||||
|
||||
for itemType in ['ETank', 'Reserve', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack']:
|
||||
self.itemManager.addItem(itemType, 'Major')
|
||||
for itemType in ['Missile', 'Missile', 'Super', 'Super', 'PowerBomb']:
|
||||
self.itemManager.addItem(itemType, 'Minor')
|
||||
|
||||
self.addEnergy()
|
||||
self.addAmmo()
|
||||
|
||||
return self.itemManager.getItemPool()
|
||||
|
||||
class ItemPoolGeneratorScavenger(ItemPoolGeneratorMajors):
|
||||
def __init__(self, itemManager, qty, sm, maxDiff):
|
||||
super(ItemPoolGeneratorScavenger, self).__init__(itemManager, qty, sm, maxDiff)
|
||||
|
||||
def addNoEnergy(self):
|
||||
self.itemManager.addItem('Nothing')
|
||||
|
||||
class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors):
|
||||
def __init__(self, itemManager, qty, sm, nLocs, maxDiff):
|
||||
super(ItemPoolGeneratorMinimizer, self).__init__(itemManager, qty, sm, maxDiff)
|
||||
self.maxItems = nLocs
|
||||
self.calcMaxAmmo()
|
||||
nMajors = len([itemName for itemName,item in ItemManager.Items.items() if item.Class == 'Major' and item.Category != 'Energy'])
|
||||
energyQty = self.qty['energy']
|
||||
if energyQty == 'medium':
|
||||
if nLocs < 40:
|
||||
self.maxEnergy = 5
|
||||
elif nLocs < 55:
|
||||
self.maxEnergy = 6
|
||||
else:
|
||||
self.maxEnergy = 5 + self.mediumRest
|
||||
elif energyQty == 'vanilla':
|
||||
if nLocs < 40:
|
||||
self.maxEnergy = 6
|
||||
elif nLocs < 55:
|
||||
self.maxEnergy = 8
|
||||
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
|
||||
self.maxEnergy = int(max(self.maxEnergy, maxItems - nMajors - self.minorLocations))
|
||||
if self.maxEnergy > 18:
|
||||
self.maxEnergy = 18
|
||||
elif energyQty == 'ultra sparse':
|
||||
self.maxEnergy = 0 if self.ultraSparseNoTanks else 1
|
||||
elif energyQty == 'sparse':
|
||||
self.maxEnergy = 3 + self.sparseRest
|
||||
self.log.debug("maxEnergy: "+str(self.maxEnergy))
|
||||
|
||||
class ItemPoolGeneratorPlando(ItemPoolGenerator):
|
||||
def __init__(self, itemManager, qty, sm, exclude, nLocs, maxDiff):
|
||||
super(ItemPoolGeneratorPlando, self).__init__(itemManager, qty, sm, maxDiff)
|
||||
# in exclude dict:
|
||||
# in alreadyPlacedItems:
|
||||
# dict of 'itemType: count' of items already added in the plando.
|
||||
# also a 'total: count' with the total number of items already added in the plando.
|
||||
# in forbiddenItems: list of item forbidden in the pool
|
||||
self.exclude = exclude
|
||||
self.maxItems = nLocs
|
||||
self.log.debug("maxItems: {}".format(self.maxItems))
|
||||
self.log.debug("exclude: {}".format(self.exclude))
|
||||
|
||||
def getItemPool(self):
|
||||
exceptionMessage = "Too many items already placed by the plando or not enough available locations:"
|
||||
self.itemManager.newItemPool(addBosses=False)
|
||||
|
||||
# add the already placed items by the plando
|
||||
for item, count in self.exclude['alreadyPlacedItems'].items():
|
||||
if item == 'total':
|
||||
continue
|
||||
itemClass = 'Major'
|
||||
if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
itemClass = 'Minor'
|
||||
for i in range(count):
|
||||
self.itemManager.addItem(item, itemClass)
|
||||
|
||||
remain = self.maxItems - self.exclude['alreadyPlacedItems']['total']
|
||||
self.log.debug("Plando: remain start: {}".format(remain))
|
||||
if remain > 0:
|
||||
# add missing bosses
|
||||
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
||||
if self.exclude['alreadyPlacedItems'][boss] == 0:
|
||||
self.itemManager.addItem(boss, 'Minor')
|
||||
self.exclude['alreadyPlacedItems'][boss] = 1
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after bosses: {}".format(remain))
|
||||
if remain < 0:
|
||||
raise Exception("{} can't add the remaining bosses".format(exceptionMessage))
|
||||
|
||||
# add missing majors
|
||||
majors = []
|
||||
for itemType in ['Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack']:
|
||||
if self.exclude['alreadyPlacedItems'][itemType] == 0 and itemType not in self.exclude['forbiddenItems']:
|
||||
self.itemManager.addItem(itemType, 'Major')
|
||||
self.exclude['alreadyPlacedItems'][itemType] = 1
|
||||
majors.append(itemType)
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after majors: {}".format(remain))
|
||||
if remain < 0:
|
||||
raise Exception("{} can't add the remaining majors: {}".format(exceptionMessage, ', '.join(majors)))
|
||||
|
||||
# add minimum minors to finish the game
|
||||
for (itemType, minimum) in [('Missile', 3), ('Super', 2), ('PowerBomb', 1)]:
|
||||
while self.exclude['alreadyPlacedItems'][itemType] < minimum and itemType not in self.exclude['forbiddenItems']:
|
||||
self.itemManager.addItem(itemType, 'Minor')
|
||||
self.exclude['alreadyPlacedItems'][itemType] += 1
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after minimum minors: {}".format(remain))
|
||||
if remain < 0:
|
||||
raise Exception("{} can't add the minimum minors to finish the game".format(exceptionMessage))
|
||||
|
||||
# add energy
|
||||
energyQty = self.qty['energy']
|
||||
limits = {
|
||||
"sparse": [('ETank', 4), ('Reserve', 1)],
|
||||
"medium": [('ETank', 8), ('Reserve', 2)],
|
||||
"vanilla": [('ETank', 14), ('Reserve', 4)]
|
||||
}
|
||||
for (itemType, minimum) in limits[energyQty]:
|
||||
while self.exclude['alreadyPlacedItems'][itemType] < minimum and itemType not in self.exclude['forbiddenItems']:
|
||||
self.itemManager.addItem(itemType, 'Major')
|
||||
self.exclude['alreadyPlacedItems'][itemType] += 1
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after energy: {}".format(remain))
|
||||
if remain < 0:
|
||||
raise Exception("{} can't add energy".format(exceptionMessage))
|
||||
|
||||
# add ammo
|
||||
nbMinorsAlready = self.exclude['alreadyPlacedItems']['Missile'] + self.exclude['alreadyPlacedItems']['Super'] + self.exclude['alreadyPlacedItems']['PowerBomb']
|
||||
minorLocations = max(0, 0.66*self.qty['minors'] - nbMinorsAlready)
|
||||
maxItems = len(self.itemManager.getItemPool()) + int(minorLocations)
|
||||
ammoQty = {itemType: qty for itemType, qty in self.qty['ammo'].items() if itemType not in self.exclude['forbiddenItems']}
|
||||
if ammoQty:
|
||||
rangeDict = getRangeDict(ammoQty)
|
||||
while len(self.itemManager.getItemPool()) < maxItems and remain > 0:
|
||||
item = chooseFromRange(rangeDict)
|
||||
self.itemManager.addMinor(item)
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after ammo: {}".format(remain))
|
||||
|
||||
# add nothing
|
||||
while remain > 0:
|
||||
self.itemManager.addMinor('Nothing')
|
||||
remain -= 1
|
||||
|
||||
self.log.debug("Plando: remain after nothing: {}".format(remain))
|
||||
|
||||
return self.itemManager.getItemPool()
|
||||
63
worlds/sm/variaRandomizer/rando/MiniSolver.py
Normal file
63
worlds/sm/variaRandomizer/rando/MiniSolver.py
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
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])
|
||||
93
worlds/sm/variaRandomizer/rando/RandoExec.py
Normal file
93
worlds/sm/variaRandomizer/rando/RandoExec.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import sys, random, time, utils.log
|
||||
|
||||
from logic.logic import Logic
|
||||
from graph.graph_utils import GraphUtils, getAccessPoint
|
||||
from rando.Restrictions import Restrictions
|
||||
from rando.RandoServices import RandoServices
|
||||
from rando.GraphBuilder import GraphBuilder
|
||||
from rando.RandoSetup import RandoSetup
|
||||
from rando.Items import ItemManager
|
||||
from rando.ItemLocContainer import ItemLocation
|
||||
from utils.vcr import VCR
|
||||
from utils.doorsmanager import DoorsManager
|
||||
|
||||
# entry point for rando execution ("randomize" method)
|
||||
class RandoExec(object):
|
||||
def __init__(self, seedName, vcr, randoSettings, graphSettings, player):
|
||||
self.errorMsg = ""
|
||||
self.seedName = seedName
|
||||
self.vcr = vcr
|
||||
self.randoSettings = randoSettings
|
||||
self.graphSettings = graphSettings
|
||||
self.log = utils.log.get('RandoExec')
|
||||
self.player = player
|
||||
|
||||
# processes settings to :
|
||||
# - create Restrictions and GraphBuilder objects
|
||||
# - create graph and item loc container using a RandoSetup instance: in area rando, if it fails, iterate on possible graph layouts
|
||||
# return container
|
||||
def randomize(self):
|
||||
vcr = VCR(self.seedName, 'rando') if self.vcr == True else None
|
||||
self.errorMsg = ""
|
||||
split = self.randoSettings.restrictions['MajorMinor']
|
||||
graphBuilder = GraphBuilder(self.graphSettings)
|
||||
container = None
|
||||
i = 0
|
||||
attempts = 500 if self.graphSettings.areaRando or self.graphSettings.doorsColorsRando or split == 'Scavenger' else 1
|
||||
now = time.process_time()
|
||||
endDate = sys.maxsize
|
||||
if self.randoSettings.runtimeLimit_s < endDate:
|
||||
endDate = now + self.randoSettings.runtimeLimit_s
|
||||
self.updateLocationsClass(split)
|
||||
while container is None and i < attempts and now <= endDate:
|
||||
self.restrictions = Restrictions(self.randoSettings)
|
||||
if self.graphSettings.doorsColorsRando == True:
|
||||
DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player)
|
||||
self.areaGraph = graphBuilder.createGraph()
|
||||
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)
|
||||
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.areaGraph.printGraph()
|
||||
return container
|
||||
|
||||
def updateLocationsClass(self, split):
|
||||
if split != 'Full' and split != 'Scavenger':
|
||||
startAP = getAccessPoint(self.graphSettings.startAP)
|
||||
possibleMajLocs, preserveMajLocs, nMaj, nChozo = Logic.LocationsHelper.getStartMajors(startAP.Name)
|
||||
if split == 'Major':
|
||||
n = nMaj
|
||||
elif split == 'Chozo':
|
||||
n = nChozo
|
||||
GraphUtils.updateLocClassesStart(startAP.GraphArea, split, possibleMajLocs, preserveMajLocs, n)
|
||||
|
||||
def postProcessItemLocs(self, itemLocs, hide):
|
||||
# hide some items like in dessy's
|
||||
if hide == True:
|
||||
for itemLoc in itemLocs:
|
||||
item = itemLoc.Item
|
||||
loc = itemLoc.Location
|
||||
if (item.Category != "Nothing"
|
||||
and loc.CanHidden == True
|
||||
and loc.Visibility == 'Visible'):
|
||||
if bool(random.getrandbits(1)) == True:
|
||||
loc.Visibility = 'Hidden'
|
||||
# put nothing in unfilled locations
|
||||
filledLocNames = [il.Location.Name for il in itemLocs]
|
||||
unfilledLocs = [loc for loc in Logic.locations if loc.Name not in filledLocNames]
|
||||
nothing = ItemManager.getItem('Nothing')
|
||||
for loc in unfilledLocs:
|
||||
loc.restricted = True
|
||||
itemLocs.append(ItemLocation(nothing, loc, False))
|
||||
391
worlds/sm/variaRandomizer/rando/RandoServices.py
Normal file
391
worlds/sm/variaRandomizer/rando/RandoServices.py
Normal file
@@ -0,0 +1,391 @@
|
||||
|
||||
import utils.log, copy, random, sys, logging
|
||||
from enum import Enum, unique
|
||||
from utils.parameters import infinity
|
||||
from rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation
|
||||
from logic.helpers import Bosses
|
||||
|
||||
# used to specify whether we want to come back from locations
|
||||
@unique
|
||||
class ComebackCheckType(Enum):
|
||||
Undefined = 0
|
||||
# do not check whether we should come back
|
||||
NoCheck = 1
|
||||
# come back with the placed item
|
||||
JustComeback = 2
|
||||
# come back without the placed item
|
||||
ComebackWithoutItem = 3
|
||||
|
||||
# collection of stateless services to be used mainly by fillers
|
||||
class RandoServices(object):
|
||||
def __init__(self, graph, restrictions, cache=None):
|
||||
self.restrictions = restrictions
|
||||
self.settings = restrictions.settings
|
||||
self.areaGraph = graph
|
||||
self.cache = cache
|
||||
self.log = utils.log.get('RandoServices')
|
||||
|
||||
# collect an item/loc with logic in a container from a given AP
|
||||
# return new AP
|
||||
def collect(self, ap, container, itemLoc, pickup=True):
|
||||
if pickup == True:
|
||||
# walk the graph to update AP
|
||||
if self.cache:
|
||||
self.cache.reset()
|
||||
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()
|
||||
return itemLoc.Location.accessPoint if pickup == True else ap
|
||||
|
||||
# gives all the possible theoretical locations for a given item
|
||||
def possibleLocations(self, item, ap, emptyContainer, bossesKilled=True):
|
||||
assert len(emptyContainer.currentItems) == 0, "Invalid call to possibleLocations. emptyContainer had collected items"
|
||||
emptyContainer.sm.resetItems()
|
||||
self.log.debug('possibleLocations. item='+item.Type)
|
||||
if bossesKilled:
|
||||
itemLambda = lambda it: it.Type != item.Type
|
||||
else:
|
||||
itemLambda = lambda it: it.Type != item.Type and it.Category != 'Boss'
|
||||
allBut = emptyContainer.getItems(itemLambda)
|
||||
self.log.debug('possibleLocations. allBut='+getItemListStr(allBut))
|
||||
emptyContainer.sm.addItems([it.Type for it in allBut])
|
||||
ret = [loc for loc in self.currentLocations(ap, emptyContainer, post=True) if self.restrictions.canPlaceAtLocation(item, loc, emptyContainer)]
|
||||
self.log.debug('possibleLocations='+getLocListStr(ret))
|
||||
emptyContainer.sm.resetItems()
|
||||
return ret
|
||||
|
||||
# gives current accessible locations within a container from an AP, given an optional item.
|
||||
# post: checks post available?
|
||||
# diff: max difficulty to use (None for max diff from settings)
|
||||
def currentLocations(self, ap, container, item=None, post=False, diff=None):
|
||||
if self.cache is not None:
|
||||
request = self.cache.request('currentLocations', ap, container, None if item is None else item.Type, post, diff)
|
||||
ret = self.cache.get(request)
|
||||
if ret is not None:
|
||||
return ret
|
||||
sm = container.sm
|
||||
if diff is None:
|
||||
diff = self.settings.maxDiff
|
||||
itemType = None
|
||||
if item is not None:
|
||||
itemType = item.Type
|
||||
sm.addItem(itemType)
|
||||
ret = sorted(self.getAvailLocs(container, ap, diff),
|
||||
key=lambda loc: loc.Name)
|
||||
if post is True:
|
||||
ret = [loc for loc in ret if self.locPostAvailable(sm, loc, itemType)]
|
||||
if item is not None:
|
||||
sm.removeItem(itemType)
|
||||
if self.cache is not None:
|
||||
self.cache.store(request, ret)
|
||||
return ret
|
||||
|
||||
def locPostAvailable(self, sm, loc, item):
|
||||
if loc.PostAvailable is None:
|
||||
return True
|
||||
result = sm.withItem(item, loc.PostAvailable) if item is not None else loc.PostAvailable(sm)
|
||||
return result.bool == True and result.difficulty <= self.settings.maxDiff
|
||||
|
||||
def getAvailLocs(self, container, ap, diff):
|
||||
sm = container.sm
|
||||
locs = container.unusedLocations
|
||||
return self.areaGraph.getAvailableLocations(locs, sm, diff, ap)
|
||||
|
||||
# gives current accessible APs within a container from an AP, given an optional item.
|
||||
def currentAccessPoints(self, ap, container, item=None):
|
||||
if self.cache is not None:
|
||||
request = self.cache.request('currentAccessPoints', ap, container, None if item is None else item.Type)
|
||||
ret = self.cache.get(request)
|
||||
if ret is not None:
|
||||
return ret
|
||||
sm = container.sm
|
||||
if item is not None:
|
||||
itemType = item.Type
|
||||
sm.addItem(itemType)
|
||||
nodes = sorted(self.areaGraph.getAvailableAccessPoints(self.areaGraph.accessPoints[ap],
|
||||
sm, self.settings.maxDiff),
|
||||
key=lambda ap: ap.Name)
|
||||
if item is not None:
|
||||
sm.removeItem(itemType)
|
||||
if self.cache is not None:
|
||||
self.cache.store(request, nodes)
|
||||
|
||||
return nodes
|
||||
|
||||
def isSoftlockPossible(self, container, ap, item, loc, comebackCheck):
|
||||
sm = container.sm
|
||||
# usually early game
|
||||
if comebackCheck == ComebackCheckType.NoCheck:
|
||||
return False
|
||||
# some specific early/late game checks
|
||||
if loc.Name == 'Bomb' or loc.Name == 'Mother Brain':
|
||||
return False
|
||||
# if the loc forces us to go to an area we can't come back from
|
||||
comeBack = loc.accessPoint == ap or \
|
||||
self.areaGraph.canAccess(sm, loc.accessPoint, ap, self.settings.maxDiff, item.Type if item is not None else None)
|
||||
if not comeBack:
|
||||
self.log.debug("KO come back from " + loc.accessPoint + " to " + ap + " when trying to place " + ("None" if item is None else item.Type) + " at " + loc.Name)
|
||||
return True
|
||||
# else:
|
||||
# self.log.debug("OK come back from " + loc.accessPoint + " to " + ap + " when trying to place " + item.Type + " at " + loc.Name)
|
||||
if item is not None and comebackCheck == ComebackCheckType.ComebackWithoutItem and self.isProgression(item, ap, container):
|
||||
# we know that loc is avail and post avail with the item
|
||||
# if it is not post avail without it, then the item prevents the
|
||||
# possible softlock
|
||||
if not self.locPostAvailable(sm, loc, None):
|
||||
return True
|
||||
# item allows us to come back from a softlock possible zone
|
||||
comeBackWithout = self.areaGraph.canAccess(sm, loc.accessPoint,
|
||||
ap,
|
||||
self.settings.maxDiff,
|
||||
None)
|
||||
if not comeBackWithout:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def fullComebackCheck(self, container, ap, item, loc, comebackCheck):
|
||||
sm = container.sm
|
||||
tmpItems = []
|
||||
# draygon special case: there are two locations, and we can
|
||||
# place one item, but we might need both the item and the boss
|
||||
# dead to get out
|
||||
if loc.SolveArea == "Draygon Boss" and Bosses.bossDead(sm, 'Draygon').bool == False:
|
||||
# temporary kill draygon
|
||||
tmpItems.append('Draygon')
|
||||
sm.addItems(tmpItems)
|
||||
ret = self.locPostAvailable(sm, loc, item.Type if item is not None else None) and not self.isSoftlockPossible(container, ap, item, loc, comebackCheck)
|
||||
for tmp in tmpItems:
|
||||
sm.removeItem(tmp)
|
||||
return ret
|
||||
|
||||
def isProgression(self, item, ap, container):
|
||||
sm = container.sm
|
||||
# no need to test nothing items
|
||||
if item.Category == 'Nothing':
|
||||
return False
|
||||
if self.cache is not None:
|
||||
request = self.cache.request('isProgression', item.Type, ap, container)
|
||||
ret = self.cache.get(request)
|
||||
if ret is not None:
|
||||
return ret
|
||||
oldLocations = self.currentLocations(ap, container)
|
||||
ret = any(self.restrictions.canPlaceAtLocation(item, loc, container) for loc in oldLocations)
|
||||
if ret == True:
|
||||
newLocations = [loc for loc in self.currentLocations(ap, container, item) if loc not in oldLocations]
|
||||
ret = len(newLocations) > 0 and any(self.restrictions.isItemLocMatching(item, loc) for loc in newLocations)
|
||||
self.log.debug('isProgression. item=' + item.Type + ', newLocs=' + str([loc.Name for loc in newLocations]))
|
||||
if ret == False and len(newLocations) > 0 and self.restrictions.split == 'Major':
|
||||
# in major/minor split, still consider minor locs as
|
||||
# progression if not all types are distributed
|
||||
ret = not sm.haveItem('Missile').bool \
|
||||
or not sm.haveItem('Super').bool \
|
||||
or not sm.haveItem('PowerBomb').bool
|
||||
if self.cache is not None:
|
||||
self.cache.store(request, ret)
|
||||
return ret
|
||||
|
||||
def getPlacementLocs(self, ap, container, comebackCheck, itemObj, locs):
|
||||
return [loc for loc in locs if (itemObj is None or self.restrictions.canPlaceAtLocation(itemObj, loc, container)) and self.fullComebackCheck(container, ap, itemObj, loc, comebackCheck)]
|
||||
|
||||
def processEarlyMorph(self, ap, container, comebackCheck, itemLocDict, curLocs):
|
||||
morph = container.getNextItemInPool('Morph')
|
||||
if morph is not None:
|
||||
self.log.debug("processEarlyMorph. morph not placed yet")
|
||||
morphLocItem = next((item for item in itemLocDict if item.Type == morph.Type), None)
|
||||
if morphLocItem is not None:
|
||||
morphLocs = itemLocDict[morphLocItem]
|
||||
itemLocDict.clear()
|
||||
itemLocDict[morphLocItem] = morphLocs
|
||||
elif len(curLocs) >= 2:
|
||||
self.log.debug("processEarlyMorph. early morph placement check")
|
||||
# we have to place morph early, it's still not placed, and not detected as placeable
|
||||
# let's see if we can place it anyway in the context of a combo
|
||||
morphLocs = self.getPlacementLocs(ap, container, comebackCheck, morph, curLocs)
|
||||
if len(morphLocs) > 0:
|
||||
# copy our context to do some destructive checks
|
||||
containerCpy = copy.copy(container)
|
||||
# choose a morph item location in that context
|
||||
morphItemLoc = ItemLocation(
|
||||
morph,
|
||||
random.choice(morphLocs)
|
||||
)
|
||||
# acquire morph in new context and see if we can still open new locs
|
||||
newAP = self.collect(ap, containerCpy, morphItemLoc)
|
||||
(ild, poss) = self.getPossiblePlacements(newAP, containerCpy, comebackCheck)
|
||||
if poss:
|
||||
# it's possible, only offer morph as possibility
|
||||
itemLocDict.clear()
|
||||
itemLocDict[morph] = morphLocs
|
||||
|
||||
def processLateMorph(self, container, itemLocDict):
|
||||
morphLocItem = next((item for item in itemLocDict if item.Type == 'Morph'), None)
|
||||
if morphLocItem is None or len(itemLocDict) == 1:
|
||||
# no morph, or it is the only possibility: nothing to do
|
||||
return
|
||||
morphLocs = self.restrictions.lateMorphCheck(container, itemLocDict[morphLocItem])
|
||||
if morphLocs is not None:
|
||||
itemLocDict[morphLocItem] = morphLocs
|
||||
else:
|
||||
del itemLocDict[morphLocItem]
|
||||
|
||||
def processNoComeback(self, ap, container, itemLocDict):
|
||||
comebackDict = {}
|
||||
for item,locList in itemLocDict.items():
|
||||
comebackLocs = [loc for loc in locList if self.fullComebackCheck(container, ap, item, loc, ComebackCheckType.JustComeback)]
|
||||
if len(comebackLocs) > 0:
|
||||
comebackDict[item] = comebackLocs
|
||||
if len(comebackDict) > 0:
|
||||
itemLocDict.clear()
|
||||
itemLocDict.update(comebackDict)
|
||||
|
||||
def processPlacementRestrictions(self, ap, container, comebackCheck, itemLocDict, curLocs):
|
||||
if self.restrictions.isEarlyMorph():
|
||||
self.processEarlyMorph(ap, container, comebackCheck, itemLocDict, curLocs)
|
||||
elif self.restrictions.isLateMorph():
|
||||
self.processLateMorph(container, itemLocDict)
|
||||
if comebackCheck == ComebackCheckType.NoCheck:
|
||||
self.processNoComeback(ap, container, itemLocDict)
|
||||
|
||||
# main logic function to be used by fillers. gives possible locations for each item.
|
||||
# ap: AP to check from
|
||||
# container: our item/loc container
|
||||
# comebackCheck: how to check for comebacks (cf ComebackCheckType)
|
||||
# return a dictionary with Item instances as keys and locations lists as values
|
||||
def getPossiblePlacements(self, ap, container, comebackCheck):
|
||||
curLocs = self.currentLocations(ap, container)
|
||||
self.log.debug('getPossiblePlacements. nCurLocs='+str(len(curLocs)))
|
||||
self.log.debug('getPossiblePlacements. curLocs='+getLocListStr(curLocs))
|
||||
self.log.debug('getPossiblePlacements. comebackCheck='+str(comebackCheck))
|
||||
sm = container.sm
|
||||
poolDict = container.getPoolDict()
|
||||
itemLocDict = {}
|
||||
possibleProg = False
|
||||
nonProgList = None
|
||||
def getLocList(itemObj):
|
||||
nonlocal curLocs
|
||||
return self.getPlacementLocs(ap, container, comebackCheck, itemObj, curLocs)
|
||||
def getNonProgLocList():
|
||||
nonlocal nonProgList
|
||||
if nonProgList is None:
|
||||
nonProgList = [loc for loc in self.currentLocations(ap, container) if self.fullComebackCheck(container, ap, None, loc, comebackCheck)]
|
||||
self.log.debug("nonProgLocList="+str([loc.Name for loc in nonProgList]))
|
||||
return [loc for loc in nonProgList if self.restrictions.canPlaceAtLocation(itemObj, loc, container)]
|
||||
for itemType,items in sorted(poolDict.items()):
|
||||
itemObj = items[0]
|
||||
cont = True
|
||||
prog = False
|
||||
if self.isProgression(itemObj, ap, container):
|
||||
cont = False
|
||||
prog = True
|
||||
elif not possibleProg:
|
||||
cont = False
|
||||
if cont: # ignore non prog items if a prog item has already been found
|
||||
continue
|
||||
# check possible locations for this item type
|
||||
# self.log.debug('getPossiblePlacements. itemType=' + itemType + ', curLocs='+str([loc.Name for loc in curLocs]))
|
||||
locations = getLocList(itemObj) if prog else getNonProgLocList()
|
||||
if len(locations) == 0:
|
||||
continue
|
||||
if prog and not possibleProg:
|
||||
possibleProg = True
|
||||
itemLocDict = {} # forget all the crap ones we stored just in case
|
||||
# self.log.debug('getPossiblePlacements. itemType=' + itemType + ', locs='+str([loc.Name for loc in locations]))
|
||||
for item in items:
|
||||
itemLocDict[item] = locations
|
||||
self.processPlacementRestrictions(ap, container, comebackCheck, itemLocDict, curLocs)
|
||||
self.printItemLocDict(itemLocDict)
|
||||
self.log.debug('possibleProg='+str(possibleProg))
|
||||
return (itemLocDict, possibleProg)
|
||||
|
||||
def printItemLocDict(self, itemLocDict):
|
||||
if self.log.getEffectiveLevel() == logging.DEBUG:
|
||||
debugDict = {}
|
||||
for item, locList in itemLocDict.items():
|
||||
if item.Type not in debugDict:
|
||||
debugDict[item.Type] = [loc.Name for loc in locList]
|
||||
self.log.debug('itemLocDict='+str(debugDict))
|
||||
|
||||
# same as getPossiblePlacements, without any logic check
|
||||
def getPossiblePlacementsNoLogic(self, container):
|
||||
poolDict = container.getPoolDict()
|
||||
itemLocDict = {}
|
||||
def getLocList(itemObj, baseList):
|
||||
return [loc for loc in baseList if self.restrictions.canPlaceAtLocation(itemObj, loc, container)]
|
||||
for itemType,items in sorted(poolDict.items()):
|
||||
itemObj = items[0]
|
||||
locList = getLocList(itemObj, container.unusedLocations)
|
||||
for item in items:
|
||||
itemLocDict[item] = locList
|
||||
self.printItemLocDict(itemLocDict)
|
||||
return (itemLocDict, False)
|
||||
|
||||
# check if bosses are blocking the last remaining locations.
|
||||
# accurate most of the time, still a heuristic
|
||||
def onlyBossesLeft(self, ap, container):
|
||||
if self.settings.maxDiff == infinity:
|
||||
return False
|
||||
self.log.debug('onlyBossesLeft, diff=' + str(self.settings.maxDiff) + ", ap="+ap)
|
||||
sm = container.sm
|
||||
bossesLeft = container.getAllItemsInPoolFromCategory('Boss')
|
||||
if len(bossesLeft) == 0:
|
||||
return False
|
||||
def getLocList():
|
||||
curLocs = self.currentLocations(ap, container)
|
||||
self.log.debug('onlyBossesLeft, curLocs=' + getLocListStr(curLocs))
|
||||
return self.getPlacementLocs(ap, container, ComebackCheckType.JustComeback, None, curLocs)
|
||||
prevLocs = getLocList()
|
||||
self.log.debug("onlyBossesLeft. prevLocs="+getLocListStr(prevLocs))
|
||||
# fake kill remaining bosses and see if we can access the rest of the game
|
||||
if self.cache is not None:
|
||||
self.cache.reset()
|
||||
for boss in bossesLeft:
|
||||
self.log.debug('onlyBossesLeft. kill '+boss.Name)
|
||||
sm.addItem(boss.Type)
|
||||
# get bosses locations and newly accessible locations (for bosses that open up locs)
|
||||
newLocs = getLocList()
|
||||
self.log.debug("onlyBossesLeft. newLocs="+getLocListStr(newLocs))
|
||||
locs = newLocs + container.getLocs(lambda loc: loc.isBoss() and not loc in newLocs)
|
||||
self.log.debug("onlyBossesLeft. locs="+getLocListStr(locs))
|
||||
ret = (len(locs) > len(prevLocs) and len(locs) == len(container.unusedLocations))
|
||||
# restore bosses killed state
|
||||
for boss in bossesLeft:
|
||||
self.log.debug('onlyBossesLeft. revive '+boss.Name)
|
||||
sm.removeItem(boss.Type)
|
||||
if self.cache is not None:
|
||||
self.cache.reset()
|
||||
self.log.debug("onlyBossesLeft? " +str(ret))
|
||||
return ret
|
||||
|
||||
def canEndGame(self, container):
|
||||
return not any(loc.Name == 'Mother Brain' for loc in container.unusedLocations)
|
||||
|
||||
def can100percent(self, ap, container):
|
||||
if not self.canEndGame(container):
|
||||
return False
|
||||
curLocs = self.currentLocations(ap, container, post=True)
|
||||
return len(curLocs) == len(container.unusedLocations)
|
||||
|
||||
def findStartupProgItemPair(self, ap, container):
|
||||
self.log.debug("findStartupProgItemPair")
|
||||
(itemLocDict, isProg) = self.getPossiblePlacements(ap, container, ComebackCheckType.NoCheck)
|
||||
assert not isProg
|
||||
items = list(itemLocDict.keys())
|
||||
random.shuffle(items)
|
||||
for item in items:
|
||||
cont = copy.copy(container)
|
||||
loc = random.choice(itemLocDict[item])
|
||||
itemLoc1 = ItemLocation(item, loc)
|
||||
self.log.debug("itemLoc1 attempt: "+getItemLocStr(itemLoc1))
|
||||
newAP = self.collect(ap, cont, itemLoc1)
|
||||
if self.cache is not None:
|
||||
self.cache.reset()
|
||||
(ild, isProg) = self.getPossiblePlacements(newAP, cont, ComebackCheckType.NoCheck)
|
||||
if isProg:
|
||||
item2 = random.choice(list(ild.keys()))
|
||||
itemLoc2 = ItemLocation(item2, random.choice(ild[item2]))
|
||||
self.log.debug("itemLoc2: "+getItemLocStr(itemLoc2))
|
||||
return (itemLoc1, itemLoc2)
|
||||
return None
|
||||
270
worlds/sm/variaRandomizer/rando/RandoSettings.py
Normal file
270
worlds/sm/variaRandomizer/rando/RandoSettings.py
Normal file
@@ -0,0 +1,270 @@
|
||||
|
||||
import sys, random
|
||||
from collections import defaultdict
|
||||
from rando.Items import ItemManager
|
||||
from utils.utils import getRangeDict, chooseFromRange
|
||||
from rando.ItemLocContainer import ItemLocation
|
||||
|
||||
# Holder for settings and a few utility functions related to them
|
||||
# (especially for plando/rando).
|
||||
# Holds settings not related to graph layout.
|
||||
class RandoSettings(object):
|
||||
def __init__(self, maxDiff, progSpeed, progDiff, qty, restrictions,
|
||||
superFun, runtimeLimit_s, plandoSettings, minDiff):
|
||||
self.progSpeed = progSpeed.lower()
|
||||
self.progDiff = progDiff.lower()
|
||||
self.maxDiff = maxDiff
|
||||
self.qty = qty
|
||||
self.restrictions = restrictions
|
||||
self.superFun = superFun
|
||||
self.runtimeLimit_s = runtimeLimit_s
|
||||
if self.runtimeLimit_s <= 0:
|
||||
self.runtimeLimit_s = sys.maxsize
|
||||
self.plandoSettings = plandoSettings
|
||||
self.minDiff = minDiff
|
||||
|
||||
def getSuperFun(self):
|
||||
return self.superFun[:]
|
||||
|
||||
def updateSuperFun(self, superFun):
|
||||
self.superFun = superFun[:]
|
||||
|
||||
def isPlandoRando(self):
|
||||
return self.plandoSettings is not None
|
||||
|
||||
def getItemManager(self, smbm, nLocs):
|
||||
if not self.isPlandoRando():
|
||||
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, self.maxDiff)
|
||||
else:
|
||||
return ItemManager('Plando', self.qty, smbm, nLocs, self.maxDiff)
|
||||
|
||||
def getExcludeItems(self, locations):
|
||||
if not self.isPlandoRando():
|
||||
return None
|
||||
exclude = {'alreadyPlacedItems': defaultdict(int), 'forbiddenItems': []}
|
||||
# locsItems is a dict {'loc name': 'item type'}
|
||||
for locName,itemType in self.plandoSettings["locsItems"].items():
|
||||
if not any(loc.Name == locName for loc in locations):
|
||||
continue
|
||||
exclude['alreadyPlacedItems'][itemType] += 1
|
||||
exclude['alreadyPlacedItems']['total'] += 1
|
||||
|
||||
exclude['forbiddenItems'] = self.plandoSettings['forbiddenItems']
|
||||
|
||||
return exclude
|
||||
|
||||
def collectAlreadyPlacedItemLocations(self, container):
|
||||
if not self.isPlandoRando():
|
||||
return
|
||||
for locName,itemType in self.plandoSettings["locsItems"].items():
|
||||
if not any(loc.Name == locName for loc in container.unusedLocations):
|
||||
continue
|
||||
item = container.getNextItemInPool(itemType)
|
||||
assert item is not None, "Invalid plando item pool"
|
||||
location = container.getLocs(lambda loc: loc.Name == locName)[0]
|
||||
itemLoc = ItemLocation(item, location)
|
||||
container.collect(itemLoc, pickup=False)
|
||||
|
||||
# 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):
|
||||
self.startAP = startAP
|
||||
self.areaRando = areaRando
|
||||
self.lightAreaRando = lightAreaRando
|
||||
self.bossRando = bossRando
|
||||
self.escapeRando = escapeRando
|
||||
self.minimizerN = minimizerN
|
||||
self.dotFile = dotFile
|
||||
self.doorsColorsRando = doorsColorsRando
|
||||
self.allowGreyDoors = allowGreyDoors
|
||||
self.plandoRandoTransitions = plandoRandoTransitions
|
||||
|
||||
def isMinimizer(self):
|
||||
return self.minimizerN is not None
|
||||
|
||||
# algo settings depending on prog speed (slowest to fastest+variable,
|
||||
# other "speeds" are actually different algorithms)
|
||||
class ProgSpeedParameters(object):
|
||||
def __init__(self, restrictions, nLocs):
|
||||
self.restrictions = restrictions
|
||||
self.nLocs = nLocs
|
||||
|
||||
def getVariableSpeed(self):
|
||||
ranges = getRangeDict({
|
||||
'slowest':7,
|
||||
'slow':20,
|
||||
'medium':35,
|
||||
'fast':27,
|
||||
'fastest':11
|
||||
})
|
||||
return chooseFromRange(ranges)
|
||||
|
||||
def getMinorHelpProb(self, progSpeed):
|
||||
if self.restrictions.split != 'Major':
|
||||
return 0
|
||||
if progSpeed == 'slowest':
|
||||
return 0.16
|
||||
elif progSpeed == 'slow':
|
||||
return 0.33
|
||||
elif progSpeed == 'medium':
|
||||
return 0.5
|
||||
return 1
|
||||
|
||||
def getLateDoorsProb(self, progSpeed):
|
||||
if progSpeed == 'slowest':
|
||||
return 1
|
||||
elif progSpeed == 'slow':
|
||||
return 0.8
|
||||
elif progSpeed == 'medium':
|
||||
return 0.66
|
||||
elif progSpeed == 'fast':
|
||||
return 0.5
|
||||
elif progSpeed == 'fastest':
|
||||
return 0.33
|
||||
return 0
|
||||
|
||||
def getItemLimit(self, progSpeed):
|
||||
itemLimit = self.nLocs
|
||||
if progSpeed == 'slow':
|
||||
itemLimit = int(self.nLocs*0.209) # 21 for 105
|
||||
elif progSpeed == 'medium':
|
||||
itemLimit = int(self.nLocs*0.095) # 9 for 105
|
||||
elif progSpeed == 'fast':
|
||||
itemLimit = int(self.nLocs*0.057) # 5 for 105
|
||||
elif progSpeed == 'fastest':
|
||||
itemLimit = int(self.nLocs*0.019) # 1 for 105
|
||||
minLimit = itemLimit - int(itemLimit/5)
|
||||
maxLimit = itemLimit + int(itemLimit/5)
|
||||
if minLimit == maxLimit:
|
||||
itemLimit = minLimit
|
||||
else:
|
||||
itemLimit = random.randint(minLimit, maxLimit)
|
||||
return itemLimit
|
||||
|
||||
def getLocLimit(self, progSpeed):
|
||||
locLimit = -1
|
||||
if progSpeed == 'slow':
|
||||
locLimit = 1
|
||||
elif progSpeed == 'medium':
|
||||
locLimit = 2
|
||||
elif progSpeed == 'fast':
|
||||
locLimit = 3
|
||||
elif progSpeed == 'fastest':
|
||||
locLimit = 4
|
||||
return locLimit
|
||||
|
||||
def getProgressionItemTypes(self, progSpeed):
|
||||
progTypes = ItemManager.getProgTypes()
|
||||
if self.restrictions.isLateDoors():
|
||||
progTypes += ['Wave','Spazer','Plasma']
|
||||
progTypes.append('Charge')
|
||||
if progSpeed == 'slowest':
|
||||
return progTypes
|
||||
else:
|
||||
progTypes.remove('HiJump')
|
||||
progTypes.remove('Charge')
|
||||
if progSpeed == 'slow':
|
||||
return progTypes
|
||||
else:
|
||||
progTypes.remove('Bomb')
|
||||
progTypes.remove('Grapple')
|
||||
if progSpeed == 'medium':
|
||||
return progTypes
|
||||
else:
|
||||
if not self.restrictions.isLateDoors():
|
||||
progTypes.remove('Ice')
|
||||
progTypes.remove('SpaceJump')
|
||||
if progSpeed == 'fast':
|
||||
return progTypes
|
||||
else:
|
||||
progTypes.remove('SpeedBooster')
|
||||
if progSpeed == 'fastest':
|
||||
return progTypes # only morph, varia, gravity
|
||||
raise RuntimeError("Unknown prog speed " + progSpeed)
|
||||
|
||||
def getPossibleSoftlockProb(self, progSpeed):
|
||||
if progSpeed == 'slowest':
|
||||
return 1
|
||||
if progSpeed == 'slow':
|
||||
return 0.66
|
||||
if progSpeed == 'medium':
|
||||
return 0.33
|
||||
if progSpeed == 'fast':
|
||||
return 0.1
|
||||
if progSpeed == 'fastest':
|
||||
return 0
|
||||
raise RuntimeError("Unknown prog speed " + progSpeed)
|
||||
|
||||
def getChooseLocDict(self, progDiff):
|
||||
if progDiff == 'normal':
|
||||
return {
|
||||
'Random' : 1,
|
||||
'MinDiff' : 0,
|
||||
'MaxDiff' : 0
|
||||
}
|
||||
elif progDiff == 'easier':
|
||||
return {
|
||||
'Random' : 2,
|
||||
'MinDiff' : 1,
|
||||
'MaxDiff' : 0
|
||||
}
|
||||
elif progDiff == 'harder':
|
||||
return {
|
||||
'Random' : 2,
|
||||
'MinDiff' : 0,
|
||||
'MaxDiff' : 1
|
||||
}
|
||||
|
||||
def getChooseItemDict(self, progSpeed):
|
||||
if progSpeed == 'slowest':
|
||||
return {
|
||||
'MinProgression' : 1,
|
||||
'Random' : 2,
|
||||
'MaxProgression' : 0
|
||||
}
|
||||
elif progSpeed == 'slow':
|
||||
return {
|
||||
'MinProgression' : 25,
|
||||
'Random' : 75,
|
||||
'MaxProgression' : 0
|
||||
}
|
||||
elif progSpeed == 'medium':
|
||||
return {
|
||||
'MinProgression' : 0,
|
||||
'Random' : 1,
|
||||
'MaxProgression' : 0
|
||||
}
|
||||
elif progSpeed == 'fast':
|
||||
return {
|
||||
'MinProgression' : 0,
|
||||
'Random' : 85,
|
||||
'MaxProgression' : 15
|
||||
}
|
||||
elif progSpeed == 'fastest':
|
||||
return {
|
||||
'MinProgression' : 0,
|
||||
'Random' : 2,
|
||||
'MaxProgression' : 1
|
||||
}
|
||||
|
||||
def getSpreadFactor(self, progSpeed):
|
||||
if progSpeed == 'slowest':
|
||||
return 0.9
|
||||
elif progSpeed == 'slow':
|
||||
return 0.7
|
||||
elif progSpeed == 'medium':
|
||||
return 0.4
|
||||
elif progSpeed == 'fast':
|
||||
return 0.1
|
||||
return 0
|
||||
|
||||
def getChozoSecondPhaseRestrictionProb(self, progSpeed):
|
||||
if progSpeed == 'slowest':
|
||||
return 0
|
||||
if progSpeed == 'slow':
|
||||
return 0.16
|
||||
if progSpeed == 'medium':
|
||||
return 0.5
|
||||
if progSpeed == 'fast':
|
||||
return 0.9
|
||||
return 1
|
||||
390
worlds/sm/variaRandomizer/rando/RandoSetup.py
Normal file
390
worlds/sm/variaRandomizer/rando/RandoSetup.py
Normal file
@@ -0,0 +1,390 @@
|
||||
import copy, utils.log, random
|
||||
|
||||
from utils.utils import randGaussBounds
|
||||
from logic.smbool import SMBool, smboolFalse
|
||||
from logic.smboolmanager import SMBoolManager
|
||||
from logic.helpers import Bosses
|
||||
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.parameters import infinity
|
||||
|
||||
# checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions
|
||||
# the entry point is createItemLocContainer
|
||||
class RandoSetup(object):
|
||||
def __init__(self, graphSettings, locations, services, player):
|
||||
self.sm = SMBoolManager(player, services.settings.maxDiff)
|
||||
self.settings = services.settings
|
||||
self.graphSettings = graphSettings
|
||||
self.startAP = graphSettings.startAP
|
||||
self.superFun = self.settings.getSuperFun()
|
||||
self.container = None
|
||||
self.services = services
|
||||
self.restrictions = services.restrictions
|
||||
self.areaGraph = services.areaGraph
|
||||
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))
|
||||
self.forbiddenItems = []
|
||||
self.restrictedLocs = []
|
||||
self.lastRestricted = []
|
||||
self.bossesLocs = sorted(['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'])
|
||||
self.suits = ['Varia', 'Gravity']
|
||||
# organized by priority
|
||||
self.movementItems = ['SpaceJump', 'HiJump', 'SpeedBooster', 'Bomb', 'Grapple', 'SpringBall']
|
||||
# organized by priority
|
||||
self.combatItems = ['ScrewAttack', 'Plasma', 'Wave', 'Spazer']
|
||||
# OMG
|
||||
self.bossChecks = {
|
||||
'Kraid' : self.sm.enoughStuffsKraid,
|
||||
'Phantoon' : self.sm.enoughStuffsPhantoon,
|
||||
'Draygon' : self.sm.enoughStuffsDraygon,
|
||||
'Ridley' : self.sm.enoughStuffsRidley,
|
||||
'Mother Brain': self.sm.enoughStuffsMotherbrain
|
||||
}
|
||||
self.okay = lambda: SMBool(True, 0)
|
||||
exclude = self.settings.getExcludeItems(self.locations)
|
||||
# we have to use item manager only once, otherwise pool will change
|
||||
self.itemManager.createItemPool(exclude)
|
||||
self.basePool = self.itemManager.getItemPool()[:]
|
||||
self.log = utils.log.get('RandoSetup')
|
||||
if len(locations) != len(self.locations):
|
||||
self.log.debug("inaccessible locations :"+getLocListStr([loc for loc in locations if loc not in self.locations]))
|
||||
|
||||
# processes everything and returns an ItemLocContainer, or None if failed (invalid init conditions/settings)
|
||||
def createItemLocContainer(self, endDate, vcr=None):
|
||||
self.getForbidden()
|
||||
self.log.debug("LAST CHECKPOOL")
|
||||
if not self.checkPool():
|
||||
self.log.debug("createItemLocContainer: last checkPool fail")
|
||||
return None
|
||||
# reset restricted in locs from previous attempt
|
||||
for loc in self.locations:
|
||||
loc.restricted = False
|
||||
for loc in self.restrictedLocs:
|
||||
self.log.debug("createItemLocContainer: loc is restricted: {}".format(loc.Name))
|
||||
loc.restricted = True
|
||||
self.checkDoorBeams()
|
||||
self.container = ItemLocContainer(self.sm, self.getItemPool(), self.locations)
|
||||
if self.restrictions.isLateMorph():
|
||||
self.restrictions.lateMorphInit(self.startAP, self.container, self.services)
|
||||
isStdStart = GraphUtils.isStandardStart(self.startAP)
|
||||
# ensure we have an area layout that can put morph outside start area
|
||||
# TODO::allow for custom start which doesn't require morph early
|
||||
if self.graphSettings.areaRando and isStdStart and not self.restrictions.suitsRestrictions and self.restrictions.lateMorphForbiddenArea is None:
|
||||
self.container = None
|
||||
self.log.debug("createItemLocContainer: checkLateMorph fail")
|
||||
return None
|
||||
# checkStart needs the container
|
||||
if not self.checkStart():
|
||||
self.container = None
|
||||
self.log.debug("createItemLocContainer: checkStart fail")
|
||||
return None
|
||||
self.settings.updateSuperFun(self.superFun)
|
||||
return self.container
|
||||
|
||||
def getRestrictionsDict(self):
|
||||
itemTypes = {item.Type for item in self.container.itemPool if item.Category not in Restrictions.NoCheckCat}
|
||||
allAreas = {loc.GraphArea for loc in self.locations}
|
||||
items = [self.container.getNextItemInPool(itemType) for itemType in itemTypes]
|
||||
restrictionDict = {}
|
||||
for area in allAreas:
|
||||
restrictionDict[area] = {}
|
||||
for itemType in itemTypes:
|
||||
restrictionDict[area][itemType] = set()
|
||||
for item in items:
|
||||
itemType = item.Type
|
||||
poss = self.services.possibleLocations(item, self.startAP, self.container)
|
||||
for loc in poss:
|
||||
restrictionDict[loc.GraphArea][itemType].add(loc.Name)
|
||||
if self.restrictions.isEarlyMorph() and GraphUtils.isStandardStart(self.startAP):
|
||||
morphLocs = ['Morphing Ball']
|
||||
if self.restrictions.split in ['Full', 'Major']:
|
||||
dboost = self.sm.knowsCeilingDBoost()
|
||||
if dboost.bool == True and dboost.difficulty <= self.settings.maxDiff:
|
||||
morphLocs.append('Energy Tank, Brinstar Ceiling')
|
||||
for area, locDict in restrictionDict.items():
|
||||
if area == 'Crateria':
|
||||
locDict['Morph'] = set(morphLocs)
|
||||
else:
|
||||
locDict['Morph'] = set()
|
||||
return restrictionDict
|
||||
|
||||
# fill up unreachable locations with "junk" to maximize the chance of the ROM
|
||||
# to be finishable
|
||||
def fillRestrictedLocations(self):
|
||||
def getPred(itemType, loc):
|
||||
return lambda item: (itemType is None or item.Type == itemType) and self.restrictions.canPlaceAtLocation(item, loc, self.container)
|
||||
locs = self.restrictedLocs
|
||||
self.log.debug("fillRestrictedLocations. locs="+getLocListStr(locs))
|
||||
for loc in locs:
|
||||
itemLocation = ItemLocation(None, loc)
|
||||
if 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))
|
||||
elif self.container.countItems(getPred('Missile', loc)) > 3:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Missile', loc))
|
||||
elif self.container.countItems(getPred('Super', loc)) > 2:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Super', loc))
|
||||
elif self.container.countItems(getPred('PowerBomb', loc)) > 1:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('PowerBomb', loc))
|
||||
elif self.container.countItems(getPred('Reserve', loc)) > 1:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Reserve', loc))
|
||||
elif self.container.countItems(getPred('ETank', loc)) > 3:
|
||||
itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('ETank', loc))
|
||||
else:
|
||||
raise RuntimeError("Cannot fill restricted locations")
|
||||
self.log.debug("Fill: {}/{} at {}".format(itemLocation.Item.Type, itemLocation.Item.Class, itemLocation.Location.Name))
|
||||
self.container.collect(itemLocation, False)
|
||||
|
||||
def getItemPool(self, forbidden=[]):
|
||||
self.itemManager.setItemPool(self.basePool[:]) # reuse base pool to have constant base item set
|
||||
return self.itemManager.removeForbiddenItems(self.forbiddenItems + forbidden)
|
||||
|
||||
# if needed, do a simplified "pre-randomization" of a few items to check start AP/area layout validity
|
||||
def checkStart(self):
|
||||
ap = getAccessPoint(self.startAP)
|
||||
if not self.graphSettings.areaRando or ap.Start is None or \
|
||||
(('needsPreRando' not in ap.Start or not ap.Start['needsPreRando']) and\
|
||||
('areaMode' not in ap.Start or not ap.Start['areaMode'])):
|
||||
return True
|
||||
self.log.debug("********* PRE RANDO START")
|
||||
container = copy.copy(self.container)
|
||||
filler = FrontFiller(self.startAP, self.areaGraph, self.restrictions, container)
|
||||
condition = filler.createStepCountCondition(4)
|
||||
(isStuck, itemLocations, progItems) = filler.generateItems(condition)
|
||||
self.log.debug("********* PRE RANDO END")
|
||||
return not isStuck and len(self.services.currentLocations(filler.ap, filler.container)) > 0
|
||||
|
||||
# in door color rando, determine mandatory beams
|
||||
def checkDoorBeams(self):
|
||||
if self.restrictions.isLateDoors():
|
||||
doorBeams = ['Wave','Ice','Spazer','Plasma']
|
||||
self.restrictions.mandatoryBeams = [beam for beam in doorBeams if not self.checkPool(forbidden=[beam])]
|
||||
self.log.debug("checkDoorBeams. mandatoryBeams="+str(self.restrictions.mandatoryBeams))
|
||||
|
||||
def checkPool(self, forbidden=None):
|
||||
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")
|
||||
return False
|
||||
ret = True
|
||||
if forbidden is not None:
|
||||
pool = self.getItemPool(forbidden)
|
||||
else:
|
||||
pool = self.getItemPool()
|
||||
# get restricted locs
|
||||
totalAvailLocs = []
|
||||
comeBack = {}
|
||||
try:
|
||||
container = ItemLocContainer(self.sm, pool, self.locations)
|
||||
except AssertionError as e:
|
||||
# invalid graph altogether
|
||||
self.log.debug("checkPool: AssertionError when creating ItemLocContainer: {}".format(e))
|
||||
return False
|
||||
# restrict item pool in chozo: game should be finishable with chozo items only
|
||||
contPool = []
|
||||
contPool += [item for item in pool if item in container.itemPool]
|
||||
# give us everything and beat every boss to see what we can access
|
||||
self.disableBossChecks()
|
||||
self.sm.resetItems()
|
||||
self.sm.addItems([item.Type for item in contPool]) # will add bosses as well
|
||||
self.log.debug('pool={}'.format(getItemListStr(container.itemPool)))
|
||||
locs = self.services.currentLocations(self.startAP, container, post=True)
|
||||
self.areaGraph.useCache(True)
|
||||
for loc in locs:
|
||||
ap = loc.accessPoint
|
||||
if ap not in comeBack:
|
||||
# we chose Golden Four because it is always there.
|
||||
# Start APs might not have comeback transitions
|
||||
# possible start AP issues are handled in checkStart
|
||||
comeBack[ap] = self.areaGraph.canAccess(self.sm, ap, 'Golden Four', self.settings.maxDiff)
|
||||
if comeBack[ap]:
|
||||
totalAvailLocs.append(loc)
|
||||
self.areaGraph.useCache(False)
|
||||
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))
|
||||
ret = False
|
||||
if not ret:
|
||||
self.log.debug("checkPool. inter-area APs check failed")
|
||||
# cleanup
|
||||
self.sm.resetItems()
|
||||
self.restoreBossChecks()
|
||||
# check if we can reach/beat all bosses
|
||||
if ret:
|
||||
for loc in self.lastRestricted:
|
||||
if loc.Name in self.bossesLocs:
|
||||
ret = False
|
||||
self.log.debug("unavail Boss: " + loc.Name)
|
||||
if ret:
|
||||
# revive bosses
|
||||
self.sm.addItems([item.Type for item in contPool if item.Category != 'Boss'])
|
||||
maxDiff = self.settings.maxDiff
|
||||
# see if phantoon doesn't block himself, and if we can reach draygon if she's alive
|
||||
ret = self.areaGraph.canAccess(self.sm, self.startAP, 'PhantoonRoomIn', maxDiff)\
|
||||
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()])
|
||||
self.log.debug("checkPool. beatableBosses="+str(beatableBosses))
|
||||
ret = beatableBosses == Bosses.Golden4()
|
||||
if ret:
|
||||
# check that we can then kill mother brain
|
||||
self.sm.addItems(Bosses.Golden4())
|
||||
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:
|
||||
self.log.debug('checkPool. locked by Phantoon or Draygon')
|
||||
self.log.debug('checkPool. boss access sanity check: '+str(ret))
|
||||
|
||||
if self.restrictions.isChozo() or self.restrictions.isScavenger():
|
||||
# in chozo or scavenger, we cannot put other items than NoEnergy in the restricted locations,
|
||||
# we would be forced to put majors in there, which can make seed generation fail:
|
||||
# don't put more restricted major locations than removed major items
|
||||
# FIXME something to do there for chozo/ultra sparse, it gives us up to 3 more spots for nothing items
|
||||
restrictedLocs = self.restrictedLocs + [loc for loc in self.lastRestricted if loc not in self.restrictedLocs]
|
||||
nRestrictedMajor = sum(1 for loc in restrictedLocs if self.restrictions.isLocMajor(loc))
|
||||
nNothingMajor = sum(1 for item in pool if self.restrictions.isItemMajor(item) and item.Category == 'Nothing')
|
||||
ret &= nRestrictedMajor <= nNothingMajor
|
||||
self.log.debug('checkPool. nRestrictedMajor='+str(nRestrictedMajor)+', nNothingMajor='+str(nNothingMajor))
|
||||
self.log.debug('checkPool. result: '+str(ret))
|
||||
return ret
|
||||
|
||||
def disableBossChecks(self):
|
||||
self.sm.enoughStuffsKraid = self.okay
|
||||
self.sm.enoughStuffsPhantoon = self.okay
|
||||
self.sm.enoughStuffsDraygon = self.okay
|
||||
self.sm.enoughStuffsRidley = self.okay
|
||||
def mbCheck():
|
||||
(possible, energyDiff) = self.sm.mbEtankCheck()
|
||||
if possible == True:
|
||||
return self.okay()
|
||||
return smboolFalse
|
||||
self.sm.enoughStuffsMotherbrain = mbCheck
|
||||
|
||||
def restoreBossChecks(self):
|
||||
self.sm.enoughStuffsKraid = self.bossChecks['Kraid']
|
||||
self.sm.enoughStuffsPhantoon = self.bossChecks['Phantoon']
|
||||
self.sm.enoughStuffsDraygon = self.bossChecks['Draygon']
|
||||
self.sm.enoughStuffsRidley = self.bossChecks['Ridley']
|
||||
self.sm.enoughStuffsMotherbrain = self.bossChecks['Mother Brain']
|
||||
|
||||
def addRestricted(self):
|
||||
self.checkPool()
|
||||
for r in self.lastRestricted:
|
||||
if r not in self.restrictedLocs:
|
||||
self.restrictedLocs.append(r)
|
||||
|
||||
def getForbiddenItemsFromList(self, itemList):
|
||||
self.log.debug('getForbiddenItemsFromList: ' + str(itemList))
|
||||
remove = []
|
||||
n = randGaussBounds(len(itemList))
|
||||
for i in range(n):
|
||||
idx = random.randint(0, len(itemList) - 1)
|
||||
item = itemList.pop(idx)
|
||||
if item is not None:
|
||||
remove.append(item)
|
||||
return remove
|
||||
|
||||
def addForbidden(self, removable):
|
||||
forb = None
|
||||
# it can take several tries if some item combination removal
|
||||
# forbids access to more stuff than each individually
|
||||
tries = 0
|
||||
while forb is None and tries < 100:
|
||||
forb = self.getForbiddenItemsFromList(removable[:])
|
||||
self.log.debug("addForbidden. forb="+str(forb))
|
||||
if self.checkPool(forb) == False:
|
||||
forb = None
|
||||
tries += 1
|
||||
if forb is None:
|
||||
# we couldn't find a combination, just pick an item
|
||||
firstItem = next((itemType for itemType in removable if itemType is not None), None)
|
||||
if firstItem is not None:
|
||||
forb = [firstItem]
|
||||
else:
|
||||
forb = []
|
||||
self.forbiddenItems += forb
|
||||
self.checkPool()
|
||||
self.addRestricted()
|
||||
return len(forb)
|
||||
|
||||
def getForbiddenSuits(self):
|
||||
self.log.debug("getForbiddenSuits BEGIN. forbidden="+str(self.forbiddenItems)+",ap="+self.startAP)
|
||||
removableSuits = [suit for suit in self.suits if self.checkPool([suit])]
|
||||
if 'Varia' in removableSuits and self.startAP in ['Bubble Mountain', 'Firefleas Top']:
|
||||
# Varia has to be first item there, and checkPool can't detect it
|
||||
removableSuits.remove('Varia')
|
||||
self.log.debug("getForbiddenSuits removable="+str(removableSuits))
|
||||
if len(removableSuits) > 0:
|
||||
# remove at least one
|
||||
if self.addForbidden(removableSuits) == 0:
|
||||
self.forbiddenItems.append(removableSuits.pop())
|
||||
self.checkPool()
|
||||
self.addRestricted()
|
||||
else:
|
||||
self.superFun.remove('Suits')
|
||||
self.log.debug("Super Fun : Could not remove any suit")
|
||||
self.log.debug("getForbiddenSuits END. forbidden="+str(self.forbiddenItems))
|
||||
|
||||
def getForbiddenMovement(self):
|
||||
self.log.debug("getForbiddenMovement BEGIN. forbidden="+str(self.forbiddenItems))
|
||||
removableMovement = [mvt for mvt in self.movementItems if self.checkPool([mvt])]
|
||||
self.log.debug("getForbiddenMovement removable="+str(removableMovement))
|
||||
if len(removableMovement) > 0:
|
||||
# remove at least the most important
|
||||
self.forbiddenItems.append(removableMovement.pop(0))
|
||||
self.addForbidden(removableMovement + [None])
|
||||
else:
|
||||
self.superFun.remove('Movement')
|
||||
self.log.debug('Super Fun : Could not remove any movement item')
|
||||
self.log.debug("getForbiddenMovement END. forbidden="+str(self.forbiddenItems))
|
||||
|
||||
def getForbiddenCombat(self):
|
||||
self.log.debug("getForbiddenCombat BEGIN. forbidden="+str(self.forbiddenItems))
|
||||
removableCombat = [cbt for cbt in self.combatItems if self.checkPool([cbt])]
|
||||
self.log.debug("getForbiddenCombat removable="+str(removableCombat))
|
||||
if len(removableCombat) > 0:
|
||||
fake = [] # placeholders to avoid tricking the gaussian into removing too much stuff
|
||||
if len(removableCombat) > 0:
|
||||
# remove at least one if possible (will be screw or plasma)
|
||||
self.forbiddenItems.append(removableCombat.pop(0))
|
||||
fake.append(None)
|
||||
# if plasma is still available, remove it as well if we can
|
||||
if len(removableCombat) > 0 and removableCombat[0] == 'Plasma' and self.checkPool([removableCombat[0]]):
|
||||
self.forbiddenItems.append(removableCombat.pop(0))
|
||||
fake.append(None)
|
||||
self.addForbidden(removableCombat + fake)
|
||||
else:
|
||||
self.superFun.remove('Combat')
|
||||
self.log.debug('Super Fun : Could not remove any combat item')
|
||||
self.log.debug("getForbiddenCombat END. forbidden="+str(self.forbiddenItems))
|
||||
|
||||
def getForbidden(self):
|
||||
self.forbiddenItems = []
|
||||
self.restrictedLocs = []
|
||||
self.errorMsgs = []
|
||||
if 'Suits' in self.superFun: # impact on movement item
|
||||
self.getForbiddenSuits()
|
||||
if 'Movement' in self.superFun:
|
||||
self.getForbiddenMovement()
|
||||
if 'Combat' in self.superFun:
|
||||
self.getForbiddenCombat()
|
||||
# if no super fun, check that there's no restricted locations (for ultra sparse)
|
||||
if len(self.superFun) == 0:
|
||||
self.addRestricted()
|
||||
self.log.debug("forbiddenItems: {}".format(self.forbiddenItems))
|
||||
self.log.debug("restrictedLocs: {}".format([loc.Name for loc in self.restrictedLocs]))
|
||||
194
worlds/sm/variaRandomizer/rando/Restrictions.py
Normal file
194
worlds/sm/variaRandomizer/rando/Restrictions.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import copy, random, utils.log
|
||||
|
||||
from graph.graph_utils import getAccessPoint
|
||||
from rando.ItemLocContainer import getLocListStr
|
||||
|
||||
# Holds settings related to item placement restrictions.
|
||||
# canPlaceAtLocation is the main entry point here
|
||||
class Restrictions(object):
|
||||
def __init__(self, settings):
|
||||
self.log = utils.log.get('Restrictions')
|
||||
self.settings = settings
|
||||
# Item split : Major, Chozo, Full, Scavenger
|
||||
self.split = settings.restrictions['MajorMinor']
|
||||
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 = {}
|
||||
self.dynamic = {}
|
||||
# only useful in door color rando
|
||||
self.mandatoryBeams = []
|
||||
|
||||
def disable(self):
|
||||
self.split = "Full"
|
||||
self.suitsRestrictions = False
|
||||
self.checkers = []
|
||||
|
||||
def setScavengerLocs(self, scavLocs):
|
||||
self.scavLocs = scavLocs
|
||||
self.log.debug("scavLocs="+getLocListStr(scavLocs))
|
||||
self.scavItemTypes = [loc.VanillaItemType for loc in scavLocs]
|
||||
|
||||
def isEarlyMorph(self):
|
||||
return self.settings.restrictions['Morph'] == 'early'
|
||||
|
||||
def isLateMorph(self):
|
||||
return self.settings.restrictions['Morph'] == 'late'
|
||||
|
||||
def isLateDoors(self):
|
||||
return self.settings.restrictions['doors'] == 'late'
|
||||
|
||||
def isChozo(self):
|
||||
return self.split == 'Chozo'
|
||||
|
||||
def isScavenger(self):
|
||||
return self.split == "Scavenger"
|
||||
|
||||
def lateMorphInit(self, ap, emptyContainer, services):
|
||||
assert self.isLateMorph()
|
||||
morph = emptyContainer.getNextItemInPool('Morph')
|
||||
assert morph is not None
|
||||
locs = services.possibleLocations(morph, ap, emptyContainer, bossesKilled=False)
|
||||
self.lateMorphLimit = len(locs)
|
||||
self.log.debug('lateMorphInit. {} locs: {}'.format(self.lateMorphLimit, getLocListStr(locs)))
|
||||
areas = {}
|
||||
for loc in locs:
|
||||
areas[loc.GraphArea] = areas.get(loc.GraphArea, 0) + 1
|
||||
self.log.debug('lateMorphLimit. areas: {}'.format(areas))
|
||||
if len(areas) > 1:
|
||||
self.lateMorphForbiddenArea = getAccessPoint(ap).GraphArea
|
||||
self.log.debug('lateMorphLimit. forbid start area: {}'.format(self.lateMorphForbiddenArea))
|
||||
else:
|
||||
self.lateMorphForbiddenArea = None
|
||||
|
||||
NoCheckCat = set(['Energy', 'Nothing', 'Boss'])
|
||||
|
||||
def setPlacementRestrictions(self, restrictionDict):
|
||||
self.log.debug("set placement restrictions")
|
||||
self.log.debug(restrictionDict)
|
||||
if self.restrictionDictChecker is not None:
|
||||
self.checkers.remove(self.restrictionDictChecker)
|
||||
self.restrictionDictChecker = None
|
||||
if restrictionDict is None:
|
||||
return
|
||||
self.restrictionDictChecker = lambda item, loc, cont: item.Category in Restrictions.NoCheckCat\
|
||||
or (item.Category == 'Ammo' and cont.hasUnrestrictedLocWithItemType(item.Type))\
|
||||
or loc.Name in restrictionDict[loc.GraphArea][item.Type]
|
||||
self.checkers.append(self.restrictionDictChecker)
|
||||
|
||||
def isLocMajor(self, loc):
|
||||
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))
|
||||
|
||||
def isItemMajor(self, item):
|
||||
if self.split == "Full":
|
||||
return True
|
||||
elif self.split == 'Scavenger':
|
||||
return not self.isItemMinor(item)
|
||||
else:
|
||||
return item.Class == self.split
|
||||
|
||||
def isItemMinor(self, item):
|
||||
if self.split == "Full":
|
||||
return True
|
||||
elif self.split == 'Scavenger':
|
||||
return item.Class != "Major" or item.Category == "Energy"
|
||||
else:
|
||||
return item.Class == "Minor"
|
||||
|
||||
def isItemLocMatching(self, item, loc):
|
||||
if self.split == "Full":
|
||||
return True
|
||||
if loc.isClass(self.split):
|
||||
return item.Class == self.split
|
||||
else:
|
||||
return item.Class == "Minor"
|
||||
|
||||
# return True if we can keep morph as a possibility
|
||||
def lateMorphCheck(self, container, possibleLocs):
|
||||
# the closer we get to the limit the higher the chances of allowing morph
|
||||
proba = random.randint(0, self.lateMorphLimit)
|
||||
if self.split == 'Full':
|
||||
nbItems = len(container.currentItems)
|
||||
else:
|
||||
nbItems = len([item for item in container.currentItems if self.split == item.Class])
|
||||
if proba > nbItems:
|
||||
return None
|
||||
if self.lateMorphForbiddenArea is not None:
|
||||
morphLocs = [loc for loc in possibleLocs if loc.GraphArea != self.lateMorphForbiddenArea]
|
||||
forbidden = len(morphLocs) == 0
|
||||
possibleLocs = morphLocs if not forbidden else None
|
||||
return possibleLocs
|
||||
|
||||
def isSuit(self, item):
|
||||
return item.Type == 'Varia' or item.Type == 'Gravity'
|
||||
|
||||
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))
|
||||
if self.split != 'Full':
|
||||
if self.split != 'Scavenger':
|
||||
self.log.debug("add majorsSplit restriction")
|
||||
checkers.append(lambda item, loc, cont: self.isItemLocMatching(item, loc))
|
||||
else:
|
||||
self.log.debug("add scavenger restriction")
|
||||
baseScavCheck = lambda item, loc: ((loc.VanillaItemType is None and self.isItemMinor(item))
|
||||
or (loc.VanillaItemType is not None and self.isItemMajor(item)))
|
||||
vanillaScavCheck = lambda item, loc: (self.scavLocs is None
|
||||
or (loc not in self.scavLocs and item.Type not in self.scavItemTypes)
|
||||
or (item.Type == loc.VanillaItemType and loc in self.scavLocs))
|
||||
nonVanillaScavCheck = lambda item, loc: (self.scavLocs is None
|
||||
or loc not in self.scavLocs
|
||||
or (loc in self.scavLocs and item.Category != 'Nothing'))
|
||||
if self.scavIsVanilla:
|
||||
checkers.append(lambda item, loc, cont: baseScavCheck(item, loc) and vanillaScavCheck(item, loc))
|
||||
else:
|
||||
checkers.append(lambda item, loc, cont: baseScavCheck(item, loc) and nonVanillaScavCheck(item, loc))
|
||||
if self.suitsRestrictions:
|
||||
self.log.debug("add suits restriction")
|
||||
checkers.append(lambda item, loc, cont: not self.isSuit(item) or loc.GraphArea != 'Crateria')
|
||||
return checkers
|
||||
|
||||
# return bool telling whether we can place a given item at a given location
|
||||
def canPlaceAtLocation(self, item, location, container):
|
||||
ret = True
|
||||
for chk in self.checkers:
|
||||
ret = ret and chk(item, location, container)
|
||||
if not ret:
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
### Below : faster implementation tailored for random fill
|
||||
|
||||
def precomputeRestrictions(self, container):
|
||||
# precompute the values for canPlaceAtLocation. only for random filler.
|
||||
# dict (loc name, item type) -> bool
|
||||
items = container.getDistinctItems()
|
||||
for item in items:
|
||||
for location in container.unusedLocations:
|
||||
self.static[(location.Name, item.Type)] = self.canPlaceAtLocation(item, location, container)
|
||||
|
||||
container.unrestrictedItems = set(['Super', 'PowerBomb'])
|
||||
for item in items:
|
||||
if item.Type not in ['Super', 'PowerBomb']:
|
||||
continue
|
||||
for location in container.unusedLocations:
|
||||
self.dynamic[(location.Name, item.Type)] = self.canPlaceAtLocation(item, location, container)
|
||||
container.unrestrictedItems = set()
|
||||
|
||||
def canPlaceAtLocationFast(self, itemType, locName, container):
|
||||
if itemType in ['Super', 'PowerBomb'] and container.hasUnrestrictedLocWithItemType(itemType):
|
||||
return self.dynamic.get((locName, itemType))
|
||||
else:
|
||||
return self.static.get((locName, itemType))
|
||||
0
worlds/sm/variaRandomizer/rando/__init__.py
Normal file
0
worlds/sm/variaRandomizer/rando/__init__.py
Normal file
23489
worlds/sm/variaRandomizer/rando/palettes.py
Normal file
23489
worlds/sm/variaRandomizer/rando/palettes.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user