class SMBoolManager(object): items = [ 'ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper' ] + Bosses.Golden4() + Bosses.miniBosses() countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve'] percentItems = [ 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack' ] def __init__(self): self._items = {} self._counts = {} # cache related self.cacheKey = 0 self.computeItemsPositions() Cache.reset() self.helpers = Logic.HelpersGraph(self) self.doorsManager = DoorsManager() self.objectives = Objectives() self.createFacadeFunctions() self.createKnowsFunctions() self.resetItems() def computeItemsPositions(self): # compute index in cache key for each items self.itemsPositions = {} maxBitsForCountItem = 7 # 128 values with 7 bits for (i, item) in enumerate(self.countItems): pos = i * maxBitsForCountItem bitMask = (2 << (maxBitsForCountItem - 1)) - 1 bitMask = bitMask << pos self.itemsPositions[item] = (pos, bitMask) for (i, item) in enumerate(self.items, (i + 1) * maxBitsForCountItem + 1): if item in self.countItems: continue self.itemsPositions[item] = (i, 1 << i) def computeNewCacheKey(self, item, value): # generate an unique integer for each items combinations which is use as key in the cache. if item in ['Nothing', 'NoEnergy']: return (pos, bitMask) = self.itemsPositions[item] # print("--------------------- {} {} ----------------------------".format(item, value)) # print("old: "+format(self.cacheKey, '#067b')) self.cacheKey = (self.cacheKey & (~bitMask)) | (value << pos) # print("new: "+format(self.cacheKey, '#067b')) # self.printItemsInKey(self.cacheKey) def printItemsInKey(self, key): # for debug purpose print("key: " + format(key, '#067b')) msg = "" for (item, (pos, bitMask)) in self.itemsPositions.items(): value = (key & bitMask) >> pos if value != 0: msg += " {}: {}".format(item, value) print("items:{}".format(msg)) def isEmpty(self): for item in self.items: if self.haveItem(item): return False for item in self.countItems: if self.itemCount(item) > 0: return False return True def getItems(self): # get a dict of collected items and how many (to be displayed on the solver spoiler) itemsDict = {} for item in self.items: itemsDict[item] = 1 if self._items[item] == True else 0 for item in self.countItems: itemsDict[item] = self._counts[item] return itemsDict def withItem(self, item, func): self.addItem(item) ret = func(self) self.removeItem(item) return ret def resetItems(self): self._items = {item: smboolFalse for item in self.items} self._counts = {item: 0 for item in self.countItems} self.cacheKey = 0 Cache.update(self.cacheKey) def addItem(self, item): # a new item is available self._items[item] = SMBool(True, items=[item]) if self.isCountItem(item): count = self._counts[item] + 1 self._counts[item] = count self.computeNewCacheKey(item, count) else: self.computeNewCacheKey(item, 1) Cache.update(self.cacheKey) def addItems(self, items): if len(items) == 0: return for item in items: self._items[item] = SMBool(True, items=[item]) if self.isCountItem(item): count = self._counts[item] + 1 self._counts[item] = count self.computeNewCacheKey(item, count) else: self.computeNewCacheKey(item, 1) Cache.update(self.cacheKey) def removeItem(self, item): # randomizer removed an item (or the item was added to test a post available) if self.isCountItem(item): count = self._counts[item] - 1 self._counts[item] = count if count == 0: self._items[item] = smboolFalse self.computeNewCacheKey(item, count) else: self._items[item] = smboolFalse self.computeNewCacheKey(item, 0) Cache.update(self.cacheKey) def createFacadeFunctions(self): for fun in dir(self.helpers): if fun != 'smbm' and fun[0:2] != '__': setattr(self, fun, getattr(self.helpers, fun)) def traverse(self, doorName): return self.doorsManager.traverse(self, doorName) def canPassG4(self): return self.objectives.canClearGoals(self, 'Golden Four') def hasItemsPercent(self, percent, totalItemsCount=None): if totalItemsCount is None: totalItemsCount = self.objectives.getTotalItemsCount() currentItemsCount = self.getCollectedItemsCount() return SMBool(100 * (currentItemsCount / totalItemsCount) >= percent) def getCollectedItemsCount(self): return (len([ item for item in self._items if self.haveItem(item) and item in self.percentItems ]) + sum([ self.itemCount(item) for item in self._items if self.isCountItem(item) ])) def createKnowsFunctions(self): # for each knows we have a function knowsKnows (ex: knowsAlcatrazEscape()) which # take no parameter for knows in Knows.__dict__: if isKnows(knows): self._createKnowsFunction(knows) def _setKnowsFunction(self, knows, k): setattr(self, 'knows' + knows, lambda: SMBool(k.bool, k.difficulty, knows=[knows])) def _createKnowsFunction(self, knows): self._setKnowsFunction(knows, Knows.__dict__[knows]) def changeKnows(self, knows, newVal): if isKnows(knows): self._setKnowsFunction(knows, newVal) Cache.reset() else: raise ValueError("Invalid knows " + str(knows)) def restoreKnows(self, knows): if isKnows(knows): self._createKnowsFunction(knows) Cache.reset() else: raise ValueError("Invalid knows " + str(knows)) def isCountItem(self, item): return item in self.countItems def itemCount(self, item): # return integer return self._counts[item] def haveItem(self, item): return self._items[item] def haveItems(self, items): for item in items: if not self.haveItem(item): return smboolFalse return SMBool(True) wand = staticmethod(SMBool.wand) wandmax = staticmethod(SMBool.wandmax) wor = staticmethod(SMBool.wor) wnot = staticmethod(SMBool.wnot) def itemCountOk(self, item, count, difficulty=0): if self.itemCount(item) >= count: if item in ['ETank', 'Reserve']: item = str(count) + '-' + item return SMBool(True, difficulty, items=[item]) else: return smboolFalse def energyReserveCountOk(self, count, difficulty=0): if self.energyReserveCount() >= count: nEtank = self.itemCount('ETank') if nEtank > count: nEtank = int(count) items = str(nEtank) + '-ETank' nReserve = self.itemCount('Reserve') if nEtank < count: nReserve = int(count) - nEtank items += ' - ' + str(nReserve) + '-Reserve' return SMBool(True, difficulty, items=[items]) else: return smboolFalse
def getMiniBossesEscapeAccessPoints(n): return (n, [Bosses.accessPoints[boss] for boss in Bosses.miniBosses()])
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 = [] if self.restrictions.isChozo(): container.restrictItemPool(isChozoItem) missile = container.getNextItemInPool('Missile') if missile is not None: # add missile (if zeb skip not known) contPool.append(missile) 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 objectives are compatible with accessible APs startAP = self.areaGraph.accessPoints[self.startAP] availAPs = [ap.Name for ap in self.areaGraph.getAvailableAccessPoints(startAP, self.sm, self.settings.maxDiff)] self.log.debug("availAPs="+str(availAPs)) for goal in Objectives.activeGoals: n, aps = goal.escapeAccessPoints if len(aps) == 0: continue escAPs = [ap for ap in aps if ap in availAPs] self.log.debug("escAPs="+str(escAPs)) if len(escAPs) < n: self.log.debug("checkPool. goal '"+goal.name+"' impossible to complete due to area layout") ret = False continue for ap in escAPs: if not self.areaGraph.canAccess(self.sm, ap, "Golden Four", self.settings.maxDiff): self.log.debug("checkPool. goal '"+goal.name+"' impossible to complete due to area layout") ret = False break # check if all inter-area APs can reach each other if ret: interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()] for startAp in interAPs: availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff) for ap in interAPs: if not ap in availAccessPoints: self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name)) ret = False if not ret: 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: # always add G4 to mandatory bosses, even if not required by objectives mandatoryBosses = set(Objectives.getMandatoryBosses() + Bosses.Golden4()) 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.BossItemType for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()]) self.log.debug("checkPool. beatableBosses="+str(beatableBosses)) self.log.debug("checkPool. mandatoryBosses: {}".format(mandatoryBosses)) ret = mandatoryBosses.issubset(set(beatableBosses)) and Objectives.checkLimitObjectives(beatableBosses) if ret: # check that we can then kill mother brain self.sm.addItems(Bosses.Golden4() + Bosses.miniBosses()) beatableMotherBrain = [loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.Name == 'Mother Brain'] ret = len(beatableMotherBrain) > 0 self.log.debug("checkPool. beatable Mother Brain={}".format(ret)) else: 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