class CommonSolver(object):
    def loadRom(self, rom, interactive=False, magic=None, startAP=None):
        # startAP param is only use for seedless
        if rom == None:
            # TODO::add a --logic parameter for seedless
            Logic.factory('varia')
            self.romFileName = 'seedless'
            self.majorsSplit = 'Full'
            self.areaRando = True
            self.bossRando = True
            self.escapeRando = False
            self.escapeTimer = "03:00"
            self.startAP = startAP
            RomPatches.setDefaultPatches(startAP)
            self.startArea = getAccessPoint(startAP).Start['solveArea']
            # in seedless load all the vanilla transitions
            self.areaTransitions = vanillaTransitions[:]
            self.bossTransitions = vanillaBossesTransitions[:]
            self.escapeTransition = [vanillaEscapeTransitions[0]]
            # in seedless we allow mixing of area and boss transitions
            self.hasMixedTransitions = True
            self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition
            self.locations = Logic.locations
            for loc in self.locations:
                loc.itemName = 'Nothing'
            # set doors related to default patches
            DoorsManager.setDoorsColor()
            self.doorsRando = False
        else:
            self.romFileName = rom
            self.romLoader = RomLoader.factory(rom, magic)
            Logic.factory(self.romLoader.readLogic())
            self.romLoader.readNothingId()
            self.locations = Logic.locations
            self.majorsSplit = self.romLoader.assignItems(self.locations)
            (self.startAP, self.startArea,
             startPatches) = self.romLoader.getStartAP()
            (self.areaRando, self.bossRando,
             self.escapeRando) = self.romLoader.loadPatches()
            RomPatches.ActivePatches += startPatches
            self.escapeTimer = self.romLoader.getEscapeTimer()
            self.doorsRando = self.romLoader.loadDoorsColor()

            if interactive == False:
                print(
                    "ROM {} majors: {} area: {} boss: {} escape: {} patches: {} activePatches: {}"
                    .format(rom, self.majorsSplit, self.areaRando,
                            self.bossRando, self.escapeRando,
                            sorted(self.romLoader.getPatches()),
                            sorted(RomPatches.ActivePatches)))
            else:
                print(
                    "majors: {} area: {} boss: {} escape: {} activepatches: {}"
                    .format(self.majorsSplit, self.areaRando,
                            self.bossRando, self.escapeRando,
                            sorted(RomPatches.ActivePatches)))

            (self.areaTransitions, self.bossTransitions, self.escapeTransition,
             self.hasMixedTransitions) = self.romLoader.getTransitions()
            if interactive == True and self.debug == False:
                # in interactive area mode we build the graph as we play along
                if self.areaRando == True and self.bossRando == True:
                    self.curGraphTransitions = []
                elif self.areaRando == True:
                    self.curGraphTransitions = self.bossTransitions[:]
                elif self.bossRando == True:
                    self.curGraphTransitions = self.areaTransitions[:]
                else:
                    self.curGraphTransitions = self.bossTransitions + self.areaTransitions
                if self.escapeRando == False:
                    self.curGraphTransitions += self.escapeTransition
            else:
                self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition

        self.smbm = SMBoolManager()
        self.areaGraph = AccessGraph(Logic.accessPoints,
                                     self.curGraphTransitions)

        # store at each step how many locations are available
        self.nbAvailLocs = []

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.log.debug("Display items at locations:")
            for loc in self.locations:
                self.log.debug('{:>50}: {:>16}'.format(loc.Name, loc.itemName))

    def loadPreset(self, presetFileName):
        presetLoader = PresetLoader.factory(presetFileName)
        presetLoader.load()
        self.smbm.createKnowsFunctions()

        if self.log.getEffectiveLevel() == logging.DEBUG:
            presetLoader.printToScreen()

    def getLoc(self, locName):
        for loc in self.locations:
            if loc.Name == locName:
                return loc

    def getNextDifficulty(self, difficulty):
        nextDiffs = {
            0: easy,
            easy: medium,
            medium: hard,
            hard: harder,
            harder: hardcore,
            hardcore: mania,
            mania: infinity
        }
        return nextDiffs[difficulty]

    def computeLocationsDifficulty(self, locations, phase="major"):
        difficultyTarget = Conf.difficultyTarget
        nextLocations = locations

        # before looping on all diff targets, get only the available locations with diff target infinity
        if difficultyTarget != infinity:
            self.areaGraph.getAvailableLocations(nextLocations, self.smbm,
                                                 infinity, self.lastAP)
            nextLocations = [loc for loc in nextLocations if loc.difficulty]

        while True:
            self.areaGraph.getAvailableLocations(nextLocations, self.smbm,
                                                 difficultyTarget, self.lastAP)
            # check post available functions too
            for loc in nextLocations:
                loc.evalPostAvailable(self.smbm)

            self.areaGraph.useCache(True)
            # also check if we can come back to current AP from the location
            for loc in nextLocations:
                loc.evalComeBack(self.smbm, self.areaGraph, self.lastAP)
            self.areaGraph.useCache(False)

            nextLocations = [
                loc for loc in nextLocations if not loc.difficulty
            ]
            if not nextLocations:
                break

            if difficultyTarget == infinity:
                # we've tested all the difficulties
                break

            # start a new loop with next difficulty
            difficultyTarget = self.getNextDifficulty(difficultyTarget)

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.log.debug("available {} locs:".format(phase))
            for loc in locations:
                if loc.difficulty.bool == True:
                    print("{:>48}: {:>8}".format(
                        loc.Name, round(loc.difficulty.difficulty, 2)))
                    print(
                        "                                          smbool: {}".
                        format(loc.difficulty))
                    print(
                        "                                            path: {}".
                        format([ap.Name for ap in loc.path]))

    def collectMajor(self, loc, itemName=None):
        self.majorLocations.remove(loc)
        self.visitedLocations.append(loc)
        self.collectItem(loc, itemName)

    def collectMinor(self, loc):
        self.minorLocations.remove(loc)
        self.visitedLocations.append(loc)
        self.collectItem(loc)

    def collectItem(self, loc, item=None):
        if item == None:
            item = loc.itemName

        if self.vcr != None:
            self.vcr.addLocation(loc.Name, item)

        if self.firstLogFile is not None:
            if item not in self.collectedItems:
                self.firstLogFile.write("{};{};{};{}\n".format(
                    item, loc.Name, loc.Area, loc.GraphArea))

        if item not in Conf.itemsForbidden:
            self.collectedItems.append(item)
            if self.checkDuplicateMajor == True:
                if item not in [
                        'Nothing', 'NoEnergy', 'Missile', 'Super', 'PowerBomb',
                        'ETank', 'Reserve'
                ]:
                    if self.smbm.haveItem(item):
                        print("WARNING: {} has already been picked up".format(
                            item))

            self.smbm.addItem(item)
        else:
            # update the name of the item
            item = "-{}-".format(item)
            loc.itemName = item
            self.collectedItems.append(item)
            # we still need the boss difficulty
            if not loc.isBoss():
                loc.difficulty = smboolFalse

        if self.log.getEffectiveLevel() == logging.DEBUG:
            print(
                "---------------------------------------------------------------"
            )
            print("collectItem: {:<16} at {:<48}".format(item, loc.Name))
            print(
                "---------------------------------------------------------------"
            )

        # last loc is used as root node for the graph.
        # when loading a plando we can load locations from non connected areas, so they don't have an access point.
        if loc.accessPoint is not None:
            self.lastAP = loc.accessPoint
            self.lastArea = loc.SolveArea

    def getLocIndex(self, locName):
        for (i, loc) in enumerate(self.visitedLocations):
            if loc.Name == locName:
                return i

    def removeItemAt(self, locNameWeb):
        locName = self.locNameWeb2Internal(locNameWeb)
        locIndex = self.getLocIndex(locName)
        if locIndex is None:
            self.errorMsg = "Location '{}' has not been visited".format(
                locName)
            return

        loc = self.visitedLocations.pop(locIndex)
        # removeItemAt is only used from the tracker, so all the locs are in majorLocations
        self.majorLocations.append(loc)

        # access point
        if len(self.visitedLocations) == 0:
            self.lastAP = self.startAP
            self.lastArea = self.startArea
        else:
            self.lastAP = self.visitedLocations[-1].accessPoint
            self.lastArea = self.visitedLocations[-1].SolveArea

        # delete location params which are set when the location is available
        if loc.difficulty is not None:
            loc.difficulty = None
        if loc.distance is not None:
            loc.distance = None
        if loc.accessPoint is not None:
            loc.accessPoint = None
        if loc.path is not None:
            loc.path = None

        # item
        item = loc.itemName

        if self.mode in ['seedless', 'race', 'debug']:
            # in seedless remove the first nothing found as collectedItems is not ordered
            self.collectedItems.remove(item)
        else:
            self.collectedItems.pop(locIndex)

        # if multiple majors in plando mode, remove it from smbm only when it's the last occurence of it
        if self.smbm.isCountItem(item):
            self.smbm.removeItem(item)
        else:
            if item not in self.collectedItems:
                self.smbm.removeItem(item)

    def cancelLastItems(self, count):
        if self.vcr != None:
            self.vcr.addRollback(count)

        if self.interactive == False:
            self.nbAvailLocs = self.nbAvailLocs[:-count]

        for _ in range(count):
            if len(self.visitedLocations) == 0:
                return

            loc = self.visitedLocations.pop()
            if self.majorsSplit == 'Full':
                self.majorLocations.append(loc)
            else:
                if loc.isClass(self.majorsSplit) or loc.isBoss():
                    self.majorLocations.append(loc)
                else:
                    self.minorLocations.append(loc)

            # access point
            if len(self.visitedLocations) == 0:
                self.lastAP = self.startAP
                self.lastArea = self.startArea
            else:
                self.lastAP = self.visitedLocations[-1].accessPoint
                if self.lastAP is None:
                    # default to location first access from access point
                    self.lastAP = list(
                        self.visitedLocations[-1].AccessFrom.keys())[0]
                self.lastArea = self.visitedLocations[-1].SolveArea

            # delete location params which are set when the location is available
            if loc.difficulty is not None:
                loc.difficulty = None
            if loc.distance is not None:
                loc.distance = None
            if loc.accessPoint is not None:
                loc.accessPoint = None
            if loc.path is not None:
                loc.path = None

            # item
            item = loc.itemName
            if item != self.collectedItems[-1]:
                raise Exception(
                    "Item of last collected loc {}: {} is different from last collected item: {}"
                    .format(loc.Name, item, self.collectedItems[-1]))

            # in plando we have to remove the last added item,
            # else it could be used in computing the postAvailable of a location
            if self.mode in ['plando', 'seedless', 'race', 'debug']:
                loc.itemName = 'Nothing'

            self.collectedItems.pop()

            # if multiple majors in plando mode, remove it from smbm only when it's the last occurence of it
            if self.smbm.isCountItem(item):
                self.smbm.removeItem(item)
            else:
                if item not in self.collectedItems:
                    self.smbm.removeItem(item)

    def printLocs(self, locs, phase):
        if len(locs) > 0:
            print("{}:".format(phase))
            print('{:>48} {:>12} {:>8} {:>8} {:>34} {:>10}'.format(
                "Location Name", "Difficulty", "Distance", "ComeBack",
                "SolveArea", "AreaWeight"))
            for loc in locs:
                print('{:>48} {:>12} {:>8} {:>8} {:>34} {:>10}'.format(
                    loc.Name, round(loc.difficulty[1], 2),
                    round(loc.distance, 2), loc.comeBack, loc.SolveArea,
                    loc.areaWeight if loc.areaWeight is not None else -1))

    def getAvailableItemsList(self, locations, threshold):
        # locations without distance are not available
        locations = [loc for loc in locations if loc.distance is not None]

        if len(locations) == 0:
            return []

        # add nocomeback locations which has been selected by the comeback step (areaWeight == 1)
        around = [
            loc for loc in locations
            if ((loc.areaWeight is not None and loc.areaWeight == 1) or (
                (loc.SolveArea == self.lastArea or loc.distance < 3)
                and loc.difficulty.difficulty <= threshold
                and not Bosses.areaBossDead(self.smbm, self.lastArea)
                and loc.comeBack is not None and loc.comeBack == True))
        ]
        outside = [loc for loc in locations if not loc in around]

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.printLocs(around, "around1")
            self.printLocs(outside, "outside1")

        around.sort(key=lambda loc: (
            # locs in the same area
            0 if loc.SolveArea == self.lastArea else 1,
            # nearest locs
            loc.distance,
            # beating a boss
            0 if loc.isBoss() else 1,
            # easiest first
            loc.difficulty.difficulty))

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.printLocs(around, "around2")

        # we want to sort the outside locations by putting the ones in the same area first,
        # then we sort the remaining areas starting whith boss dead status.
        # we also want to sort by range of difficulty and not only with the difficulty threshold.
        ranged = {
            "areaWeight": [],
            "easy": [],
            "medium": [],
            "hard": [],
            "harder": [],
            "hardcore": [],
            "mania": [],
            "noComeBack": []
        }
        for loc in outside:
            if loc.areaWeight is not None:
                ranged["areaWeight"].append(loc)
            elif loc.comeBack is None or loc.comeBack == False:
                ranged["noComeBack"].append(loc)
            else:
                difficulty = loc.difficulty.difficulty
                if difficulty < medium:
                    ranged["easy"].append(loc)
                elif difficulty < hard:
                    ranged["medium"].append(loc)
                elif difficulty < harder:
                    ranged["hard"].append(loc)
                elif difficulty < hardcore:
                    ranged["harder"].append(loc)
                elif difficulty < mania:
                    ranged["hardcore"].append(loc)
                else:
                    ranged["mania"].append(loc)

        for key in ranged:
            ranged[key].sort(key=lambda loc: (
                # first locs in the same area
                0 if loc.SolveArea == self.lastArea else 1,
                # first nearest locs
                loc.distance,
                # beating a boss
                loc.difficulty.difficulty if (not Bosses.areaBossDead(
                    self.smbm, loc.Area) and loc.isBoss()) else 100000,
                # areas with boss still alive
                loc.difficulty.difficulty
                if (not Bosses.areaBossDead(self.smbm, loc.Area)) else 100000,
                loc.difficulty.difficulty))

        if self.log.getEffectiveLevel() == logging.DEBUG:
            for key in [
                    "areaWeight", "easy", "medium", "hard", "harder",
                    "hardcore", "mania", "noComeBack"
            ]:
                self.printLocs(ranged[key], "outside2:{}".format(key))

        outside = []
        for key in [
                "areaWeight", "easy", "medium", "hard", "harder", "hardcore",
                "mania", "noComeBack"
        ]:
            outside += ranged[key]

        return around + outside

    def nextDecision(self, majorsAvailable, minorsAvailable, hasEnoughMinors,
                     diffThreshold):
        # first take major items of acceptable difficulty in the current area
        if (len(majorsAvailable) > 0
                and majorsAvailable[0].SolveArea == self.lastArea
                and majorsAvailable[0].difficulty.difficulty <= diffThreshold
                and majorsAvailable[0].comeBack == True):
            return self.collectMajor(majorsAvailable.pop(0))
        # next item decision
        elif len(minorsAvailable) == 0 and len(majorsAvailable) > 0:
            self.log.debug('MAJOR')
            return self.collectMajor(majorsAvailable.pop(0))
        elif len(majorsAvailable) == 0 and len(minorsAvailable) > 0:
            # we don't check for hasEnoughMinors here, because we would be stuck, so pickup
            # what we can and hope it gets better
            self.log.debug('MINOR')
            return self.collectMinor(minorsAvailable.pop(0))
        elif len(majorsAvailable) > 0 and len(minorsAvailable) > 0:
            self.log.debug('BOTH|M={}, m={}'.format(majorsAvailable[0].Name,
                                                    minorsAvailable[0].Name))
            # if both are available, decide based on area, difficulty and comeBack
            nextMajDifficulty = majorsAvailable[0].difficulty.difficulty
            nextMinDifficulty = minorsAvailable[0].difficulty.difficulty
            nextMajArea = majorsAvailable[0].SolveArea
            nextMinArea = minorsAvailable[0].SolveArea
            nextMajComeBack = majorsAvailable[0].comeBack
            nextMinComeBack = minorsAvailable[0].comeBack
            nextMajDistance = majorsAvailable[0].distance
            nextMinDistance = minorsAvailable[0].distance
            maxAreaWeigth = 10000
            nextMajAreaWeight = majorsAvailable[
                0].areaWeight if majorsAvailable[
                    0].areaWeight is not None else maxAreaWeigth
            nextMinAreaWeight = minorsAvailable[
                0].areaWeight if minorsAvailable[
                    0].areaWeight is not None else maxAreaWeigth

            if self.log.getEffectiveLevel() == logging.DEBUG:
                print("     : {:>4} {:>32} {:>4} {:>4} {:>6}".format(
                    "diff", "area", "back", "dist", "weight"))
                print("major: {:>4} {:>32} {:>4} {:>4} {:>6}".format(
                    round(nextMajDifficulty, 2), nextMajArea, nextMajComeBack,
                    round(nextMajDistance, 2), nextMajAreaWeight))
                print("minor: {:>4} {:>32} {:>4} {:>4} {:>6}".format(
                    round(nextMinDifficulty, 2), nextMinArea, nextMinComeBack,
                    round(nextMinDistance, 2), nextMinAreaWeight))

            if hasEnoughMinors == True and self.haveAllMinorTypes(
            ) == True and self.smbm.haveItem(
                    'Charge') and nextMajAreaWeight != maxAreaWeigth:
                # we have charge, no longer need minors
                self.log.debug(
                    "we have charge, no longer need minors, take major")
                return self.collectMajor(majorsAvailable.pop(0))
            else:
                # respect areaweight first
                if nextMajAreaWeight != nextMinAreaWeight:
                    self.log.debug("maj/min != area weight")
                    if nextMajAreaWeight < nextMinAreaWeight:
                        return self.collectMajor(majorsAvailable.pop(0))
                    else:
                        return self.collectMinor(minorsAvailable.pop(0))
                # then take item from loc where you can come back
                elif nextMajComeBack != nextMinComeBack:
                    self.log.debug("maj/min != combeback")
                    if nextMajComeBack == True:
                        return self.collectMajor(majorsAvailable.pop(0))
                    else:
                        return self.collectMinor(minorsAvailable.pop(0))
                # difficulty over area (this is a difficulty estimator, not a speedrunning simulator)
                elif nextMinDifficulty <= diffThreshold and nextMajDifficulty <= diffThreshold:
                    # take the closer one
                    if nextMajDistance != nextMinDistance:
                        self.log.debug("!= distance and <= diffThreshold")
                        if nextMajDistance < nextMinDistance:
                            return self.collectMajor(majorsAvailable.pop(0))
                        else:
                            return self.collectMinor(minorsAvailable.pop(0))
                    # take the easier
                    elif nextMinDifficulty < nextMajDifficulty:
                        self.log.debug("min easier and not enough minors")
                        return self.collectMinor(minorsAvailable.pop(0))
                    elif nextMajDifficulty < nextMinDifficulty:
                        self.log.debug("maj easier")
                        return self.collectMajor(majorsAvailable.pop(0))
                    # same difficulty and distance for minor and major, take major first
                    else:
                        return self.collectMajor(majorsAvailable.pop(0))
                # if not all the minors type are collected, start with minors
                elif nextMinDifficulty <= diffThreshold and not self.haveAllMinorTypes(
                ):
                    self.log.debug("not all minors types")
                    return self.collectMinor(minorsAvailable.pop(0))
                elif nextMinArea == self.lastArea and nextMinDifficulty <= diffThreshold:
                    self.log.debug("not enough minors")
                    return self.collectMinor(minorsAvailable.pop(0))
                elif nextMinDifficulty > diffThreshold and nextMajDifficulty > diffThreshold:
                    # take the easier
                    if nextMinDifficulty < nextMajDifficulty:
                        self.log.debug("min easier and not enough minors")
                        return self.collectMinor(minorsAvailable.pop(0))
                    elif nextMajDifficulty < nextMinDifficulty:
                        self.log.debug("maj easier")
                        return self.collectMajor(majorsAvailable.pop(0))
                    # take the closer one
                    elif nextMajDistance != nextMinDistance:
                        self.log.debug("!= distance and > diffThreshold")
                        if nextMajDistance < nextMinDistance:
                            return self.collectMajor(majorsAvailable.pop(0))
                        else:
                            return self.collectMinor(minorsAvailable.pop(0))
                    # same difficulty and distance for minor and major, take major first
                    else:
                        return self.collectMajor(majorsAvailable.pop(0))
                else:
                    if nextMinDifficulty < nextMajDifficulty:
                        self.log.debug("min easier and not enough minors")
                        return self.collectMinor(minorsAvailable.pop(0))
                    else:
                        self.log.debug("maj easier")
                        return self.collectMajor(majorsAvailable.pop(0))

        raise Exception("Can't take a decision")

    def checkMB(self, mbLoc):
        # add mother brain loc and check if it's accessible
        self.majorLocations.append(mbLoc)
        self.computeLocationsDifficulty(self.majorLocations)
        if mbLoc.difficulty == True:
            self.log.debug("MB loc accessible")
            self.collectMajor(mbLoc)
            return True
        else:
            self.log.debug("MB loc not accessible")
            self.majorLocations.remove(mbLoc)
            return False

    def computeDifficulty(self):
        # loop on the available locations depending on the collected items.
        # before getting a new item, loop on all of them and get their difficulty,
        # the next collected item is the one with the smallest difficulty,
        # if equality between major and minor, take major first.

        # remove mother brain location (there items pickup conditions on top of going to mother brain location)
        mbLoc = self.getLoc('Mother Brain')
        self.locations.remove(mbLoc)

        if self.majorsSplit == 'Major':
            self.majorLocations = [
                loc for loc in self.locations if loc.isMajor() or loc.isBoss()
            ]
            self.minorLocations = [
                loc for loc in self.locations if loc.isMinor()
            ]
        elif self.majorsSplit == 'Chozo':
            self.majorLocations = [
                loc for loc in self.locations if loc.isChozo() or loc.isBoss()
            ]
            self.minorLocations = [
                loc for loc in self.locations
                if not loc.isChozo() and not loc.isBoss()
            ]
        else:
            # Full
            self.majorLocations = self.locations[:]  # copy
            self.minorLocations = self.majorLocations

        self.visitedLocations = []
        self.collectedItems = []

        self.log.debug(
            "{}: available major: {}, available minor: {}, visited: {}".format(
                Conf.itemsPickup, len(self.majorLocations),
                len(self.minorLocations), len(self.visitedLocations)))

        isEndPossible = False
        endDifficulty = mania
        diffThreshold = self.getDiffThreshold()
        while True:
            # actual while condition
            hasEnoughMinors = self.pickup.enoughMinors(self.smbm,
                                                       self.minorLocations)
            hasEnoughMajors = self.pickup.enoughMajors(self.smbm,
                                                       self.majorLocations)
            hasEnoughItems = hasEnoughMajors and hasEnoughMinors
            canEndGame = self.canEndGame()
            (isEndPossible, endDifficulty) = (canEndGame.bool,
                                              canEndGame.difficulty)
            if isEndPossible and hasEnoughItems and endDifficulty <= diffThreshold:
                if self.checkMB(mbLoc):
                    self.log.debug("END")
                    break
                else:
                    self.log.debug("canEnd but MB loc not accessible")

            # check time limit
            if self.runtimeLimit_s > 0:
                if time.process_time() - self.startTime > self.runtimeLimit_s:
                    self.log.debug("time limit exceeded ({})".format(
                        self.runtimeLimit_s))
                    return (-1, False)

            self.log.debug("Current AP/Area: {}/{}".format(
                self.lastAP, self.lastArea))

            # compute the difficulty of all the locations
            self.computeLocationsDifficulty(self.majorLocations)
            if self.majorsSplit != 'Full':
                self.computeLocationsDifficulty(self.minorLocations,
                                                phase="minor")

            # keep only the available locations
            majorsAvailable = [
                loc for loc in self.majorLocations
                if loc.difficulty is not None and loc.difficulty.bool == True
            ]
            minorsAvailable = [
                loc for loc in self.minorLocations
                if loc.difficulty is not None and loc.difficulty.bool == True
            ]

            if self.majorsSplit == 'Full':
                locs = majorsAvailable
            else:
                locs = majorsAvailable + minorsAvailable

            self.nbAvailLocs.append(len(locs))

            # check if we're stuck
            if len(majorsAvailable) == 0 and len(minorsAvailable) == 0:
                if not isEndPossible:
                    self.log.debug("STUCK MAJORS and MINORS")
                    if self.comeBack.rewind(len(self.collectedItems)) == True:
                        continue
                    else:
                        # we're really stucked
                        self.log.debug("STUCK CAN'T REWIND")
                        break
                else:
                    self.log.debug("HARD END 2")
                    self.checkMB(mbLoc)
                    break

            # handle no comeback locations
            rewindRequired = self.comeBack.handleNoComeBack(
                locs, len(self.collectedItems))
            if rewindRequired == True:
                if self.comeBack.rewind(len(self.collectedItems)) == True:
                    continue
                else:
                    # we're really stucked
                    self.log.debug("STUCK CAN'T REWIND")
                    break

            # sort them on difficulty and proximity
            self.log.debug("getAvailableItemsList majors")
            majorsAvailable = self.getAvailableItemsList(
                majorsAvailable, diffThreshold)
            if self.majorsSplit == 'Full':
                minorsAvailable = majorsAvailable
            else:
                self.log.debug("getAvailableItemsList minors")
                minorsAvailable = self.getAvailableItemsList(
                    minorsAvailable, diffThreshold)

            # choose one to pick up
            self.nextDecision(majorsAvailable, minorsAvailable,
                              hasEnoughMinors, diffThreshold)

            self.comeBack.cleanNoComeBack(locs)

        # compute difficulty value
        (difficulty, itemsOk) = self.computeDifficultyValue()

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.log.debug("difficulty={}".format(difficulty))
            self.log.debug("itemsOk={}".format(itemsOk))
            self.log.debug(
                "{}: remaining major: {}, remaining minor: {}, visited: {}".
                format(Conf.itemsPickup, len(self.majorLocations),
                       len(self.minorLocations), len(self.visitedLocations)))

            self.log.debug("remaining majors:")
            for loc in self.majorLocations:
                self.log.debug("{} ({})".format(loc.Name, loc.itemName))

            self.log.debug("bosses: {}".format([
                (boss, Bosses.bossDead(self.smbm, boss))
                for boss in Bosses.Golden4()
            ]))

        return (difficulty, itemsOk)

    def haveAllMinorTypes(self):
        # the first minor of each type can be seen as a major, so check for them first before going to far in zebes
        hasPB = 'PowerBomb' in self.collectedItems
        hasSuper = 'Super' in self.collectedItems
        hasMissile = 'Missile' in self.collectedItems
        return (hasPB and hasSuper and hasMissile)

    def canEndGame(self):
        # to finish the game you must :
        # - beat golden 4 : we force pickup of the 4 items
        #   behind the bosses to ensure that
        # - defeat metroids
        # - destroy/skip the zebetites
        # - beat Mother Brain
        return self.smbm.wand(Bosses.allBossesDead(self.smbm),
                              self.smbm.enoughStuffTourian())

    def computeDifficultyValue(self):
        if not self.canEndGame().bool:
            # we have aborted
            return (-1, False)
        else:
            # return the maximum difficulty
            difficultyMax = 0
            for loc in self.visitedLocations:
                difficultyMax = max(difficultyMax, loc.difficulty.difficulty)
            difficulty = difficultyMax

            # check if we have taken all the requested items
            if (self.pickup.enoughMinors(self.smbm, self.minorLocations) and
                    self.pickup.enoughMajors(self.smbm, self.majorLocations)):
                return (difficulty, True)
            else:
                # can finish but can't take all the requested items
                return (difficulty, False)
    def loadRom(self, rom, interactive=False, magic=None, startAP=None):
        # startAP param is only use for seedless
        if rom == None:
            # TODO::add a --logic parameter for seedless
            Logic.factory('varia')
            self.romFileName = 'seedless'
            self.majorsSplit = 'Full'
            self.areaRando = True
            self.bossRando = True
            self.escapeRando = False
            self.escapeTimer = "03:00"
            self.startAP = startAP
            RomPatches.setDefaultPatches(startAP)
            self.startArea = getAccessPoint(startAP).Start['solveArea']
            # in seedless load all the vanilla transitions
            self.areaTransitions = vanillaTransitions[:]
            self.bossTransitions = vanillaBossesTransitions[:]
            self.escapeTransition = [vanillaEscapeTransitions[0]]
            # in seedless we allow mixing of area and boss transitions
            self.hasMixedTransitions = True
            self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition
            self.locations = Logic.locations
            for loc in self.locations:
                loc.itemName = 'Nothing'
            # set doors related to default patches
            DoorsManager.setDoorsColor()
            self.doorsRando = False
        else:
            self.romFileName = rom
            self.romLoader = RomLoader.factory(rom, magic)
            Logic.factory(self.romLoader.readLogic())
            self.romLoader.readNothingId()
            self.locations = Logic.locations
            self.majorsSplit = self.romLoader.assignItems(self.locations)
            (self.startAP, self.startArea,
             startPatches) = self.romLoader.getStartAP()
            (self.areaRando, self.bossRando,
             self.escapeRando) = self.romLoader.loadPatches()
            RomPatches.ActivePatches += startPatches
            self.escapeTimer = self.romLoader.getEscapeTimer()
            self.doorsRando = self.romLoader.loadDoorsColor()

            if interactive == False:
                print(
                    "ROM {} majors: {} area: {} boss: {} escape: {} patches: {} activePatches: {}"
                    .format(rom, self.majorsSplit, self.areaRando,
                            self.bossRando, self.escapeRando,
                            sorted(self.romLoader.getPatches()),
                            sorted(RomPatches.ActivePatches)))
            else:
                print(
                    "majors: {} area: {} boss: {} escape: {} activepatches: {}"
                    .format(self.majorsSplit, self.areaRando,
                            self.bossRando, self.escapeRando,
                            sorted(RomPatches.ActivePatches)))

            (self.areaTransitions, self.bossTransitions, self.escapeTransition,
             self.hasMixedTransitions) = self.romLoader.getTransitions()
            if interactive == True and self.debug == False:
                # in interactive area mode we build the graph as we play along
                if self.areaRando == True and self.bossRando == True:
                    self.curGraphTransitions = []
                elif self.areaRando == True:
                    self.curGraphTransitions = self.bossTransitions[:]
                elif self.bossRando == True:
                    self.curGraphTransitions = self.areaTransitions[:]
                else:
                    self.curGraphTransitions = self.bossTransitions + self.areaTransitions
                if self.escapeRando == False:
                    self.curGraphTransitions += self.escapeTransition
            else:
                self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition

        self.smbm = SMBoolManager()
        self.areaGraph = AccessGraph(Logic.accessPoints,
                                     self.curGraphTransitions)

        # store at each step how many locations are available
        self.nbAvailLocs = []

        if self.log.getEffectiveLevel() == logging.DEBUG:
            self.log.debug("Display items at locations:")
            for loc in self.locations:
                self.log.debug('{:>50}: {:>16}'.format(loc.Name, loc.itemName))
Exemple #3
0
 def buildGraph(self):
     self.areaGraph = AccessGraph(Logic.accessPoints,
                                  self.curGraphTransitions)
     Objectives.setGraph(self.areaGraph, infinity)