Exemple #1
0
class World(object):
    def __init__(self, settings):
        self.shuffle = 'vanilla'
        self.dungeons = []
        self.regions = []
        self.itempool = []
        self.state = State(self)
        self._cached_locations = None
        self._entrance_cache = {}
        self._region_cache = {}
        self._location_cache = {}
        self.required_locations = []
        self.shop_prices = {}
        self.scrub_prices = {}
        self.light_arrow_location = None

        # dump settings directly into world's namespace
        # this gives the world an attribute for every setting listed in Settings.py
        self.settings = settings
        self.__dict__.update(settings.__dict__)
        self.distribution = None

        # evaluate settings (important for logic, nice for spoiler)
        if self.big_poe_count_random:
            self.big_poe_count = random.randint(1, 10)
        if self.starting_tod == 'random':
            setting_info = get_setting_info('starting_tod')
            choices = [
                ch for ch in setting_info.choices
                if ch not in ['default', 'random']
            ]
            self.starting_tod = random.choice(choices)
        if self.starting_age == 'random':
            self.starting_age = random.choice(['child', 'adult'])

        # rename a few attributes...
        self.keysanity = self.shuffle_smallkeys != 'dungeon'
        self.check_beatable_only = not self.all_reachable
        self.shuffle_dungeon_entrances = self.entrance_shuffle != 'off'
        self.shuffle_grotto_entrances = self.entrance_shuffle in [
            'simple-indoors', 'all-indoors', 'all'
        ]
        self.shuffle_interior_entrances = self.entrance_shuffle in [
            'simple-indoors', 'all-indoors', 'all'
        ]
        self.shuffle_special_interior_entrances = self.entrance_shuffle in [
            'all-indoors', 'all'
        ]
        self.shuffle_overworld_entrances = self.entrance_shuffle == 'all'

        # trials that can be skipped will be decided later
        self.skipped_trials = {
            'Forest': False,
            'Fire': False,
            'Water': False,
            'Spirit': False,
            'Shadow': False,
            'Light': False
        }

        # dungeon forms will be decided later
        self.dungeon_mq = {
            'Deku Tree': False,
            'Dodongos Cavern': False,
            'Jabu Jabus Belly': False,
            'Bottom of the Well': False,
            'Ice Cavern': False,
            'Gerudo Training Grounds': False,
            'Forest Temple': False,
            'Fire Temple': False,
            'Water Temple': False,
            'Spirit Temple': False,
            'Shadow Temple': False,
            'Ganons Castle': False
        }

        self.can_take_damage = True

    def copy(self):
        new_world = World(self.settings)
        new_world.skipped_trials = copy.copy(self.skipped_trials)
        new_world.dungeon_mq = copy.copy(self.dungeon_mq)
        new_world.big_poe_count = copy.copy(self.big_poe_count)
        new_world.starting_tod = self.starting_tod
        new_world.starting_age = self.starting_age
        new_world.can_take_damage = self.can_take_damage
        new_world.shop_prices = copy.copy(self.shop_prices)
        new_world.id = self.id
        new_world.distribution = self.distribution

        new_world.regions = [region.copy(new_world) for region in self.regions]
        for region in new_world.regions:
            for exit in region.exits:
                exit.connect(new_world.get_region(exit.connected_region))

        new_world.dungeons = [
            dungeon.copy(new_world) for dungeon in self.dungeons
        ]
        new_world.itempool = [item.copy(new_world) for item in self.itempool]
        new_world.state = self.state.copy(new_world)

        return new_world

    def load_regions_from_json(self, file_path):
        json_string = ""
        with io.open(file_path, 'r') as file:
            for line in file.readlines():
                json_string += line.split('#')[0].replace('\n', ' ')
        json_string = re.sub(' +', ' ', json_string)
        try:
            region_json = json.loads(json_string)
        except json.JSONDecodeError as error:
            raise Exception("JSON parse error around text:\n" + \
                            json_string[error.pos-35:error.pos+35] + "\n" + \
                            "                                   ^^\n")

        for region in region_json:
            new_region = Region(region['region_name'])
            new_region.world = self
            if 'scene' in region:
                new_region.scene = region['scene']
            if 'hint' in region:
                new_region.hint = region['hint']
            if 'dungeon' in region:
                new_region.dungeon = region['dungeon']
            if 'time_passes' in region:
                new_region.time_passes = region['time_passes']
            if 'locations' in region:
                for location, rule in region['locations'].items():
                    new_location = LocationFactory(location)
                    new_location.parent_region = new_region
                    if self.logic_rules != 'none':
                        new_location.access_rule = parse_rule_string(
                            rule, self)
                    new_location.world = self
                    new_region.locations.append(new_location)
            if 'exits' in region:
                for exit, rule in region['exits'].items():
                    new_exit = Entrance('%s -> %s' % (new_region.name, exit),
                                        new_region)
                    new_exit.connected_region = exit
                    if self.logic_rules != 'none':
                        new_exit.access_rule = parse_rule_string(rule, self)
                    new_region.exits.append(new_exit)
            self.regions.append(new_region)

    def initialize_entrances(self):
        for region in self.regions:
            for exit in region.exits:
                exit.connect(self.get_region(exit.connected_region))
                exit.world = self

    def initialize_regions(self):
        for region in self.regions:
            region.world = self
            for location in region.locations:
                location.world = self

    def initialize_items(self):
        for item in self.itempool:
            item.world = self
        for region in self.regions:
            for location in region.locations:
                if location.item != None:
                    location.item.world = self
        for item in [
                item for dungeon in self.dungeons for item in dungeon.all_items
        ]:
            item.world = self

    def random_shop_prices(self):
        shop_item_indexes = ['7', '5', '8', '6']
        self.shop_prices = {}
        for region in self.regions:
            if self.shopsanity == 'random':
                shop_item_count = random.randint(0, 4)
            else:
                shop_item_count = int(self.shopsanity)

            for location in region.locations:
                if location.type == 'Shop':
                    if location.name[
                            -1:] in shop_item_indexes[:shop_item_count]:
                        self.shop_prices[location.name] = int(
                            random.betavariate(1.5, 2) * 60) * 5

    def set_scrub_prices(self):
        # Get Deku Scrub Locations
        scrub_locations = [
            location for location in self.get_locations()
            if 'Deku Scrub' in location.name
        ]
        scrub_dictionary = {}
        for location in scrub_locations:
            if location.default not in scrub_dictionary:
                scrub_dictionary[location.default] = []
            scrub_dictionary[location.default].append(location)

        # Loop through each type of scrub.
        for (scrub_item, default_price, text_id,
             text_replacement) in business_scrubs:
            price = default_price
            if self.shuffle_scrubs == 'low':
                price = 10
            elif self.shuffle_scrubs == 'random':
                # this is a random value between 0-99
                # average value is ~33 rupees
                price = int(random.betavariate(1, 2) * 99)

            # Set price in the dictionary as well as the location.
            self.scrub_prices[scrub_item] = price
            if scrub_item in scrub_dictionary:
                for location in scrub_dictionary[scrub_item]:
                    location.price = price
                    if location.item is not None:
                        location.item.price = price

    def get_region(self, regionname):
        if isinstance(regionname, Region):
            return regionname
        try:
            return self._region_cache[regionname]
        except KeyError:
            for region in self.regions:
                if region.name == regionname:
                    self._region_cache[regionname] = region
                    return region
            raise KeyError('No such region %s' % regionname)

    def get_entrance(self, entrance):
        if isinstance(entrance, Entrance):
            return entrance
        try:
            return self._entrance_cache[entrance]
        except KeyError:
            for region in self.regions:
                for exit in region.exits:
                    if exit.name == entrance:
                        self._entrance_cache[entrance] = exit
                        return exit
            raise KeyError('No such entrance %s' % entrance)

    def get_location(self, location):
        if isinstance(location, Location):
            return location
        try:
            return self._location_cache[location]
        except KeyError:
            for region in self.regions:
                for r_location in region.locations:
                    if r_location.name == location:
                        self._location_cache[location] = r_location
                        return r_location
        raise KeyError('No such location %s' % location)

    def get_items(self):
        return [loc.item
                for loc in self.get_filled_locations()] + self.itempool

    def get_itempool_with_dungeon_items(self):
        return self.get_restricted_dungeon_items(
        ) + self.get_unrestricted_dungeon_items() + self.itempool

    # get a list of items that should stay in their proper dungeon
    def get_restricted_dungeon_items(self):
        itempool = []
        if self.shuffle_mapcompass == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.dungeon_items
            ])
        if self.shuffle_smallkeys == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.small_keys
            ])
        if self.shuffle_bosskeys == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons for item in dungeon.boss_key
            ])

        for item in itempool:
            item.world = self
        return itempool

    # get a list of items that don't have to be in their proper dungeon
    def get_unrestricted_dungeon_items(self):
        itempool = []
        if self.shuffle_mapcompass == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.dungeon_items
            ])
        if self.shuffle_smallkeys == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.small_keys
            ])
        if self.shuffle_bosskeys == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons for item in dungeon.boss_key
            ])

        for item in itempool:
            item.world = self
        return itempool

    def find_items(self, item):
        return [
            location for location in self.get_locations()
            if location.item is not None and location.item.name == item
        ]

    def push_item(self, location, item, manual=False):
        if not isinstance(location, Location):
            location = self.get_location(location)

        # This check should never be false normally, but is here as a sanity check
        if location.can_fill_fast(item, manual):
            location.item = item
            item.location = location
            item.price = location.price if location.price is not None else item.price
            location.price = item.price

            logging.getLogger('').debug(
                'Placed %s [World %d] at %s [World %d]', item,
                item.world.id if hasattr(item, 'world') else -1, location,
                location.world.id if hasattr(location, 'world') else -1)
        else:
            raise RuntimeError('Cannot assign item %s to location %s.' %
                               (item, location))

    def get_locations(self):
        if self._cached_locations is None:
            self._cached_locations = []
            for region in self.regions:
                self._cached_locations.extend(region.locations)
        return self._cached_locations

    def get_unfilled_locations(self):
        return [
            location for location in self.get_locations()
            if location.item is None
        ]

    def get_filled_locations(self):
        return [
            location for location in self.get_locations()
            if location.item is not None
        ]

    def get_reachable_locations(self, state=None):
        if state is None:
            state = self.state
        return [
            location for location in self.get_locations()
            if state.can_reach(location)
        ]

    def get_placeable_locations(self, state=None):
        if state is None:
            state = self.state
        return [
            location for location in self.get_locations()
            if location.item is None and state.can_reach(location)
        ]

    def unlocks_new_location(self, item):
        temp_state = self.state.copy()
        temp_state.clear_cached_unreachable()
        temp_state.collect(item)

        for location in self.get_unfilled_locations():
            if temp_state.can_reach(
                    location) and not self.state.can_reach(location):
                return True

        return False

    def get_entrances(self):
        return [
            entrance for region in self.regions
            for entrance in region.entrances
        ]

    def get_shuffled_entrances(self, type=None):
        return [
            entrance for entrance in self.get_entrances()
            if entrance.shuffled and (type == None or entrance.type == type)
        ]

    def has_beaten_game(self, state):
        return state.has('Triforce')

    # Useless areas are areas that have contain no items that could ever
    # be used to complete the seed. Unfortunately this is very difficult
    # to calculate since that involves trying every possible path and item
    # set collected to know this. To simplify this we instead just get areas
    # that don't have any items that could ever be required in any seed.
    # We further cull this list with woth info. This is an overestimate of
    # the true list of possible useless areas, but this will generate a
    # reasonably sized list of areas that fit this property.
    def update_useless_areas(self, spoiler):
        areas = {}
        # Link's Pocket and None are not real areas
        excluded_areas = [None, "Link's Pocket"]
        for location in self.get_locations():
            location_hint = get_hint_area(location)

            # We exclude event and locked locations. This means that medallions
            # and stones are not considered here. This is not really an accurate
            # way of doing this, but it's the only way to allow dungeons to appear.
            # So barren hints do not include these dungeon rewards.
            if location_hint in excluded_areas or \
               location.locked or \
               location.item is None or \
               location.item.type == "Event":
                continue

            area = location_hint

            # Build the area list and their items
            if area not in areas:
                areas[area] = {
                    'locations': [],
                }
            areas[area]['locations'].append(location)

        # Generate area list meta data
        for area, area_info in areas.items():
            # whether an area is a dungeon is calculated to prevent too many
            # dungeon barren hints since they are quite powerful. The area
            # names don't quite match the internal dungeon names so we need to
            # check if any location in the area has a dungeon.
            area_info['dungeon'] = False
            for location in area_info['locations']:
                if location.parent_region.dungeon is not None:
                    area_info['dungeon'] = True
                    break
            # Weight the area's chance of being chosen based on its size.
            # Small areas are more likely to barren, so we apply this weight
            # to make all areas have a more uniform chance of being chosen
            area_info['weight'] = len(area_info['locations'])

        # these are items that can never be required but are still considered major items
        exclude_item_list = [
            'Double Defense',
            'Ice Arrows',
            'Biggoron Sword',
        ]
        if self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and self.shuffle_scrubs == 'off':
            # nayru's love may be required to prevent forced damage
            exclude_item_list.append('Nayrus Love')
        if self.hints != 'agony':
            # Stone of Agony only required if it's used for hints
            exclude_item_list.append('Stone of Agony')
        if not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances:
            # Serenade and Prelude are never required with vanilla Links House/ToT and overworld entrances
            exclude_item_list.append('Serenade of Water')
            exclude_item_list.append('Prelude of Light')

        # The idea here is that if an item shows up in woth, then the only way
        # that another copy of that major item could ever be required is if it
        # is a progressive item. Normally this applies to things like bows, bombs
        # bombchus, bottles, slingshot, magic and ocarina. However if plentiful
        # item pool is enabled this could be applied to any item.
        duplicate_item_woth = {}
        woth_loc = [
            location for world_woth in spoiler.required_locations.values()
            for location in world_woth
        ]
        for world in spoiler.worlds:
            duplicate_item_woth[world.id] = {}
        for location in woth_loc:
            world_id = location.item.world.id
            item = location.item

            if item.name == 'Bottle with Letter' and item.name in duplicate_item_woth[
                    world_id]:
                # Only the first Letter counts as a letter, subsequent ones are Bottles.
                # It doesn't matter which one is considered bottle/letter, since they will
                # both we considered not useless.
                item_name = 'Bottle'
            elif item.special.get('bottle', False):
                # Bottles can have many names but they are all generally the same in logic.
                # The letter and big poe bottles will give a bottle item, so no additional
                # checks are required for them.
                item_name = 'Bottle'
            else:
                item_name = item.name

            if item_name not in duplicate_item_woth[world_id]:
                duplicate_item_woth[world_id][item_name] = []
            duplicate_item_woth[world_id][item_name].append(location)

        # generate the empty area list
        self.empty_areas = {}

        for area, area_info in areas.items():
            useless_area = True
            for location in area_info['locations']:
                world_id = location.item.world.id
                item = location.item

                if (not location.item.majoritem) or (location.item.name
                                                     in exclude_item_list):
                    # Minor items are always useless in logic
                    continue

                is_bottle = False
                if item.name == 'Bottle with Letter' and item.name in duplicate_item_woth[
                        world_id]:
                    # If this is the required Letter then it is not useless
                    dupe_locations = duplicate_item_woth[world_id][item.name]
                    for dupe_location in dupe_locations:
                        if dupe_location.world.id == location.world.id and dupe_location.name == location.name:
                            useless_area = False
                            break
                    # Otherwise it is treated as a bottle
                    is_bottle = True

                if is_bottle or item.special.get('bottle', False):
                    # Bottle Items are all interchangable. Logic currently only needs
                    # a max on 1 bottle, but this might need to be changed in the
                    # future if using multiple bottles for fire temple diving is added
                    # to logic
                    dupe_locations = duplicate_item_woth[world_id].get(
                        'Bottle', [])
                    max_progressive = 1
                elif item.name == 'Bottle with Big Poe':
                    # The max number of requred Big Poe Bottles is based on the setting
                    dupe_locations = duplicate_item_woth[world_id].get(
                        item.name, [])
                    max_progressive = self.settings.big_poe_count
                else:
                    dupe_locations = duplicate_item_woth[world_id].get(
                        item.name, [])
                    max_progressive = item.special.get('progressive', 1)

                # If this is a required item location, then it is not useless
                for dupe_location in dupe_locations:
                    if dupe_location.world.id == location.world.id and dupe_location.name == location.name:
                        useless_area = False
                        break

                # If there are sufficient required item known, then the remaining
                # copies of the items are useless.
                if len(dupe_locations) < max_progressive:
                    useless_area = False
                    break

            if useless_area:
                self.empty_areas[area] = area_info
class World(object):
    def __init__(self, settings):
        self.shuffle = 'vanilla'
        self.dungeons = []
        self.regions = []
        self.itempool = []
        self.state = State(self)
        self._cached_locations = None
        self._entrance_cache = {}
        self._region_cache = {}
        self._location_cache = {}
        self.required_locations = []
        self.shop_prices = {}
        self.light_arrow_location = None

        # dump settings directly into world's namespace
        # this gives the world an attribute for every setting listed in Settings.py
        self.settings = settings
        self.__dict__.update(settings.__dict__)

        # evaluate settings (important for logic, nice for spoiler)
        if self.big_poe_count_random:
            self.big_poe_count = random.randint(1, 10)
        if self.starting_tod == 'random':
            setting_info = get_setting_info('starting_tod')
            choices = [
                ch for ch in setting_info.args_params['choices']
                if ch not in ['default', 'random']
            ]
            self.starting_tod = random.choice(choices)

        # rename a few attributes...
        self.keysanity = self.shuffle_smallkeys != 'dungeon'
        self.check_beatable_only = not self.all_reachable

        # trials that can be skipped will be decided later
        self.skipped_trials = {
            'Forest': False,
            'Fire': False,
            'Water': False,
            'Spirit': False,
            'Shadow': False,
            'Light': False
        }

        # dungeon forms will be decided later
        self.dungeon_mq = {
            'Deku Tree': False,
            'Dodongos Cavern': False,
            'Jabu Jabus Belly': False,
            'Bottom of the Well': False,
            'Ice Cavern': False,
            'Gerudo Training Grounds': False,
            'Forest Temple': False,
            'Fire Temple': False,
            'Water Temple': False,
            'Spirit Temple': False,
            'Shadow Temple': False,
            'Ganons Castle': False
        }

        self.can_take_damage = True

    def copy(self):
        new_world = World(self.settings)
        new_world.skipped_trials = copy.copy(self.skipped_trials)
        new_world.dungeon_mq = copy.copy(self.dungeon_mq)
        new_world.big_poe_count = copy.copy(self.big_poe_count)
        new_world.can_take_damage = self.can_take_damage
        new_world.shop_prices = copy.copy(self.shop_prices)
        new_world.id = self.id

        new_world.regions = [region.copy(new_world) for region in self.regions]
        for region in new_world.regions:
            for exit in region.exits:
                exit.connect(new_world.get_region(exit.connected_region))

        new_world.dungeons = [
            dungeon.copy(new_world) for dungeon in self.dungeons
        ]
        new_world.itempool = [item.copy(new_world) for item in self.itempool]
        new_world.state = self.state.copy(new_world)

        return new_world

    def load_regions_from_json(self, file_path):
        json_string = ""
        with io.open(file_path, 'r') as file:
            for line in file.readlines():
                json_string += line.split('#')[0].replace('\n', ' ')
        region_json = json.loads(json_string)

        for region in region_json:
            new_region = Region(region['region_name'])
            new_region.world = self
            if 'dungeon' in region:
                new_region.dungeon = region['dungeon']
            if 'locations' in region:
                for location, rule in region['locations'].items():
                    new_location = LocationFactory(location)
                    new_location.parent_region = new_region
                    if self.logic_rules != 'none':
                        new_location.access_rule = parse_rule_string(
                            rule, self)
                    new_location.world = self
                    new_region.locations.append(new_location)
            if 'exits' in region:
                for exit, rule in region['exits'].items():
                    new_exit = Entrance('%s -> %s' % (new_region.name, exit),
                                        new_region)
                    new_exit.connected_region = exit
                    if self.logic_rules != 'none':
                        new_exit.access_rule = parse_rule_string(rule, self)
                    new_region.exits.append(new_exit)
            self.regions.append(new_region)

    def initialize_entrances(self):
        for region in self.regions:
            for exit in region.exits:
                exit.connect(self.get_region(exit.connected_region))

    def initialize_regions(self):
        for region in self.regions:
            region.world = self
            for location in region.locations:
                location.world = self

    def initialize_items(self):
        for item in self.itempool:
            item.world = self
        for region in self.regions:
            for location in region.locations:
                if location.item != None:
                    location.item.world = self
        for item in [
                item for dungeon in self.dungeons for item in dungeon.all_items
        ]:
            item.world = self

    def random_shop_prices(self):
        shop_item_indexes = ['7', '5', '8', '6']
        self.shop_prices = {}
        for region in self.regions:
            if self.shopsanity == 'random':
                shop_item_count = random.randint(0, 4)
            else:
                shop_item_count = int(self.shopsanity)

            for location in region.locations:
                if location.type == 'Shop':
                    if location.name[
                            -1:] in shop_item_indexes[:shop_item_count]:
                        self.shop_prices[location.name] = int(
                            random.betavariate(1.5, 2) * 60) * 5

    def get_region(self, regionname):
        if isinstance(regionname, Region):
            return regionname
        try:
            return self._region_cache[regionname]
        except KeyError:
            for region in self.regions:
                if region.name == regionname:
                    self._region_cache[regionname] = region
                    return region
            raise RuntimeError('No such region %s' % regionname)

    def get_entrance(self, entrance):
        if isinstance(entrance, Entrance):
            return entrance
        try:
            return self._entrance_cache[entrance]
        except KeyError:
            for region in self.regions:
                for exit in region.exits:
                    if exit.name == entrance:
                        self._entrance_cache[entrance] = exit
                        return exit
            raise RuntimeError('No such entrance %s' % entrance)

    def get_location(self, location):
        if isinstance(location, Location):
            return location
        try:
            return self._location_cache[location]
        except KeyError:
            for region in self.regions:
                for r_location in region.locations:
                    if r_location.name == location:
                        self._location_cache[location] = r_location
                        return r_location
        raise RuntimeError('No such location %s' % location)

    def get_items(self):
        return [loc.item
                for loc in self.get_filled_locations()] + self.itempool

    # get a list of items that should stay in their proper dungeon
    def get_restricted_dungeon_items(self):
        itempool = []
        if self.shuffle_mapcompass == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.dungeon_items
            ])
        if self.shuffle_smallkeys == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.small_keys
            ])
        if self.shuffle_bosskeys == 'dungeon':
            itempool.extend([
                item for dungeon in self.dungeons for item in dungeon.boss_key
            ])

        for item in itempool:
            item.world = self
        return itempool

    # get a list of items that don't have to be in their proper dungeon
    def get_unrestricted_dungeon_items(self):
        itempool = []
        if self.shuffle_mapcompass == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.dungeon_items
            ])
        if self.shuffle_smallkeys == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons
                for item in dungeon.small_keys
            ])
        if self.shuffle_bosskeys == 'keysanity':
            itempool.extend([
                item for dungeon in self.dungeons for item in dungeon.boss_key
            ])

        for item in itempool:
            item.world = self
        return itempool

    def find_items(self, item):
        return [
            location for location in self.get_locations()
            if location.item is not None and location.item.name == item
        ]

    def push_item(self, location, item):
        if not isinstance(location, Location):
            location = self.get_location(location)

        # This check should never be false normally, but is here as a sanity check
        if location.can_fill_fast(item):
            location.item = item
            item.location = location
            item.price = location.price if location.price is not None else item.price
            location.price = item.price

            logging.getLogger('').debug(
                'Placed %s [World %d] at %s [World %d]', item,
                item.world.id if hasattr(item, 'world') else -1, location,
                location.world.id if hasattr(location, 'world') else -1)
        else:
            raise RuntimeError('Cannot assign item %s to location %s.' %
                               (item, location))

    def get_locations(self):
        if self._cached_locations is None:
            self._cached_locations = []
            for region in self.regions:
                self._cached_locations.extend(region.locations)
        return self._cached_locations

    def get_unfilled_locations(self):
        return [
            location for location in self.get_locations()
            if location.item is None
        ]

    def get_filled_locations(self):
        return [
            location for location in self.get_locations()
            if location.item is not None
        ]

    def get_reachable_locations(self, state=None):
        if state is None:
            state = self.state
        return [
            location for location in self.get_locations()
            if state.can_reach(location)
        ]

    def get_placeable_locations(self, state=None):
        if state is None:
            state = self.state
        return [
            location for location in self.get_locations()
            if location.item is None and state.can_reach(location)
        ]

    def unlocks_new_location(self, item):
        temp_state = self.state.copy()
        temp_state.clear_cached_unreachable()
        temp_state.collect(item)

        for location in self.get_unfilled_locations():
            if temp_state.can_reach(
                    location) and not self.state.can_reach(location):
                return True

        return False

    def has_beaten_game(self, state):
        return state.has('Triforce')

    # Useless areas are areas that have contain no items that could ever
    # be used to complete the seed. Unfortunately this is very difficult
    # to calculate since that involves trying every possible path and item
    # set collected to know this. To simplify this we instead just get areas
    # that don't have any items that could ever be required in any seed.
    # We further cull this list with woth info. This is an overestimate of
    # the true list of possible useless areas, but this will generate a
    # reasonably sized list of areas that fit this property.
    def update_useless_areas(self, spoiler):
        areas = {}
        # Link's Pocket and None are not real areas
        excluded_areas = [None, "Link's Pocket"]
        for location in self.get_locations():
            # We exclude event and locked locations. This means that medallions
            # and stones are not considered here. This is not really an accurate
            # way of doing this, but it's the only way to allow dungeons to appear.
            # So barren hints do not include these dungeon rewards.
            if location.hint in excluded_areas or \
               location.locked or \
               location.item is None or \
               location.item.type == "Event":
                continue

            # We should consider GT and GC as the same area or it's confusing.
            # You can get a hint GC is barren and a player might think that
            # GT is also barren when it is not. They are separate scenes in
            # the rom data, but one dungeon logically.
            if location.hint == "Ganon's Tower":
                area = "Ganon's Castle"
            else:
                area = location.hint

            # Build the area list and their items
            if area not in areas:
                areas[area] = {
                    'locations': [],
                }
            areas[area]['locations'].append(location)

        # Generate area list meta data
        for area, area_info in areas.items():
            # whether an area is a dungeon is calculated to prevent too many
            # dungeon barren hints since they are quite powerful. The area
            # names don't quite match the internal dungeon names so we need to
            # check if any location in the area has a dungeon.
            area_info['dungeon'] = False
            for location in area_info['locations']:
                if location.parent_region.dungeon is not None:
                    area_info['dungeon'] = True
                    break
            # Weight the area's chance of being chosen based on its size.
            # Small areas are more likely to barren, so we apply this weight
            # to make all areas have a more uniform chance of being chosen
            area_info['weight'] = len(area_info['locations'])

        # these are items that can never be required but are still considered major items
        exclude_item_list = [
            'Double Defense', 'Ice Arrows', 'Serenade of Water',
            'Prelude of Light'
        ]
        if self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and self.shuffle_scrubs == 'off':
            # nayru's love may be required to prevent forced damage
            exclude_item_list.append('Nayrus Love')

        # The idea here is that if an item shows up in woth, then the only way
        # that another copy of that major item could ever be required is if it
        # is a progressive item. Normally this applies to things like bows, bombs
        # bombchus, bottles, slingshot, magic and ocarina. However if plentiful
        # item pool is enabled this could be applied to any item.
        duplicate_item_woth = {}
        woth_loc = [
            location for world_woth in spoiler.required_locations.values()
            for location in world_woth
        ]
        for world in spoiler.worlds:
            duplicate_item_woth[world.id] = {}
        for location in woth_loc:
            if not location.item.special.get('progressive', False):
                # Progressive items may need multiple copies to make progression
                # so we can't make this culling for those kinds of items.
                duplicate_item_woth[location.item.world.id][
                    location.item.name] = location
            if 'Bottle' in location.item.name and \
                location.item.name not in ['Bottle with Letter', 'Bottle with Big Poe']:
                # Bottles can have many names but they are all generally the same in logic
                # The problem is that Ruto's Letter and Big Poe might not be usuable as a
                # Bottle immediately, so they might need to use a regular bottle in
                # addition to that one. Conversely finding a bottle might mean you still
                # need ruto's letter or big poe. So to work with this, we ignore those
                # two special bottles as being bottles
                duplicate_item_woth[
                    location.item.world.id]['Bottle'] = location

        # generate the empty area list
        self.empty_areas = {}
        for area, area_info in areas.items():
            useless_area = True
            for location in area_info['locations']:
                if location.item.majoritem:
                    if (location.item.name in exclude_item_list):
                        continue

                    if 'Bottle' in location.item.name and location.item.name not in [
                            'Bottle with Letter', 'Bottle with Big Poe'
                    ]:
                        dupe_location = duplicate_item_woth[
                            location.item.world.id].get('Bottle', location)
                    else:
                        dupe_location = duplicate_item_woth[
                            location.item.world.id].get(
                                location.item.name, location)

                    if (dupe_location.world.id != location.world.id
                            or dupe_location.name != location.name):
                        continue

                    useless_area = False
                    break
            if useless_area:
                self.empty_areas[area] = area_info