Added Super Metroid support (#46)

Varia Randomizer based implementation
LttPClient -> SNIClient
This commit is contained in:
lordlou
2021-11-12 08:00:11 -05:00
committed by GitHub
parent 61ae51b30c
commit 77ec8d4141
141 changed files with 43859 additions and 106 deletions

View 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

View 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

View 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

View 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]

View 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()

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

View 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))

View 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

View 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

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

View 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))

File diff suppressed because it is too large Load Diff