From 28b87428cc79de844a72aaf0f53c1f48b1484be8 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 5 Oct 2021 13:58:30 -0600 Subject: [PATCH] Changed bias named. Added districting --- Fill.py | 43 +++-- Main.py | 3 +- RaceRandom.py | 1 + Rom.py | 40 ++++- resources/app/cli/args.json | 9 +- resources/app/cli/lang/en.json | 17 +- resources/app/gui/lang/en.json | 8 +- resources/app/gui/randomize/item/widgets.json | 8 +- source/item/BiasedFill.py | 99 ++++++++-- source/item/District.py | 169 ++++++++++++++++++ 10 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 source/item/District.py diff --git a/Fill.py b/Fill.py index 872f09e7..a770ee71 100644 --- a/Fill.py +++ b/Fill.py @@ -27,7 +27,7 @@ def promote_dungeon_items(world): def dungeon_tracking(world): for dungeon in world.dungeons: layout = world.dungeon_layouts[dungeon.player][dungeon.name] - layout.dungeon_items = len(dungeon.all_items) + layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)]) layout.free_items = layout.location_cnt - layout.dungeon_items @@ -64,13 +64,10 @@ def fill_dungeons_restrictive(world, shuffled_locations): def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, - reserved_items=None): - if not reserved_items: - reserved_items = [] - + vanilla=False): def sweep_from_pool(): new_state = base_state.copy() - for item in itempool + reserved_items: + for item in itempool: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -99,7 +96,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No spot_to_fill = None - item_locations = filter_locations(item_to_place, locations, world) + item_locations = filter_locations(item_to_place, locations, world, vanilla) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, itempool, @@ -107,6 +104,9 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: + if vanilla: + unplaced_items.insert(0, item_to_place) + continue spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, base_state, itempool, perform_access_check, item_locations, keys_in_itempool, single_player_placement) @@ -166,7 +166,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this + return not item.is_inside_dungeon_item(world) def valid_reserved_placement(item, location, world): @@ -215,10 +215,11 @@ def is_dungeon_item(item, world): def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, keys_in_itempool=None, single_player_placement=False): + logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery') if world.algorithm in ['balanced', 'equitable']: return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, single_player_placement) - elif world.algorithm == 'vanilla_bias': + elif world.algorithm == 'vanilla_fill': if item_to_place.type == 'Crystal': possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, @@ -354,8 +355,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] - gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] - # dungeon bias may fill up the dungeon... and push items out into the overworld + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only'] + # dungeon only may fill up the dungeon... and push items out into the overworld # fill in gtower locations with trash first for player in range(1, world.players + 1): @@ -384,18 +385,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + if world.algorithm == 'vanilla_fill': + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True) fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) random.shuffle(fill_locations) if world.algorithm == 'balanced': fast_fill(world, prioitempool, fill_locations) - elif world.algorithm == 'vanilla_bias': + elif world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, prioitempool, fill_locations) - elif world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias', 'entangled']: + elif world.algorithm in ['major_only', 'dungeon_only', 'district']: filtered_fill(world, prioitempool, fill_locations) else: # just need to ensure dungeon items still get placed in dungeons fast_equitable_fill(world, prioitempool, fill_locations) # placeholder work - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if world.algorithm == 'district': random.shuffle(fill_locations) placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] num_ph_items = len(placeholder_items) @@ -412,7 +415,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, restitempool, fill_locations) else: fast_fill(world, restitempool, fill_locations) @@ -443,8 +446,18 @@ def filtered_fill(world, item_pool, fill_locations): def fast_vanilla_fill(world, item_pool, fill_locations): + next_item_pool = [] while item_pool and fill_locations: item_to_place = item_pool.pop() + locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True) + if len(locations): + spot_to_fill = locations.pop() + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + else: + next_item_pool.append(item_to_place) + while next_item_pool and fill_locations: + item_to_place = next_item_pool.pop() spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) fill_locations.remove(spot_to_fill) world.push_item(spot_to_fill, item_to_place, False) diff --git a/Main.py b/Main.py index ca146f5d..09ebb7e5 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.BiasedFill import create_item_pool_config, massage_item_pool +from source.item.BiasedFill import create_item_pool_config, massage_item_pool, district_item_pool_config __version__ = '1.0.0.1-u' @@ -199,6 +199,7 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) + district_item_pool_config(world) massage_item_pool(world) logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) diff --git a/RaceRandom.py b/RaceRandom.py index 127d966d..fa882580 100644 --- a/RaceRandom.py +++ b/RaceRandom.py @@ -22,6 +22,7 @@ def _wrap(name): # These are for intellisense purposes only, and will be overwritten below choice = _prng_inst.choice +choices = _prng_inst.choices gauss = _prng_inst.gauss getrandbits = _prng_inst.getrandbits randint = _prng_inst.randint diff --git a/Rom.py b/Rom.py index 22f017e3..a7ab66ca 100644 --- a/Rom.py +++ b/Rom.py @@ -17,7 +17,8 @@ except ImportError: from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem from DoorShuffle import compass_data, DROptions, boss_indicator -from Dungeons import dungeon_music_addresses +from Dungeons import dungeon_music_addresses, dungeon_table +from DungeonGenerator import dungeon_portals from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -32,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '11f4f494e999a919aafd7d2624e67679' +RANDOMIZERBASEHASH = 'f2791b1fb0776849bd4a0851b75fca26' class JsonRom(object): @@ -2051,6 +2052,7 @@ def write_strings(rom, world, player, team): else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 + hint_count -= 2 if world.algorithm == 'district' and world.shuffle[player] not in ['simple', 'restricted'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: if hint_count > 0: @@ -2142,11 +2144,35 @@ def write_strings(rom, world, player, team): else: tt[hint_locations.pop(0)] = this_hint - # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice. - junk_hints = junk_texts.copy() - random.shuffle(junk_hints) - for location in hint_locations: - tt[location] = junk_hints.pop(0) + if world.shuffle[player] in ['full', 'crossed', 'insanity']: + # 3 hints for dungeons - todo: replace with overworld map code + hint_count = 3 + dungeon_candidates = list(dungeon_table.keys()) + dungeon_choices = random.choices(dungeon_candidates, k=hint_count) + for c in dungeon_choices: + portal_name = random.choice(dungeon_portals[c]) + portal_region = world.get_portal(portal_name, player).door.entrance.connected_region + entrance = next(ent for ent in portal_region.entrances + if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + district =next(d for d in world.districts[player].values() if entrance.name in d.entrances) + this_hint = f'The entrance to {c} can be found in {district.name}' + tt[hint_locations.pop(0)] = this_hint + + if world.algorithm == 'district': + hint_candidates = [] + for name, district in world.districts[player].items(): + if name not in world.item_pool_config.recorded_choices and not district.sphere_one: + hint_candidates.append(f'{name} is a foolish choice') + random.shuffle(hint_candidates) + for location in hint_locations: + tt[location] = hint_candidates.pop(0) + else: + # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint + # isn't selected twice. + junk_hints = junk_texts.copy() + random.shuffle(junk_hints) + for location in hint_locations: + tt[location] = junk_hints.pop(0) # We still need the older hints of course. Those are done here. diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 99ddec22..3a316a69 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -102,11 +102,10 @@ "choices": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias", - "entangled" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 7b7bb4f7..fe536d75 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -156,27 +156,26 @@ " algorithm.", "equitable: does not place dungeon items first allowing new potential", " but mixed with the normal advancement pool", - "biased placements: these consider all major items to be special and attempts", + "restricted placements: these consider all major items to be special and attempts", "to place items from fixed to semi-random locations. For purposes of these shuffles, all", "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", - "vanilla_bias Same as above, but attempts to place items in their vanilla", + "vanilla_fill As above, but attempts to place items in their vanilla", " location first. Major items that cannot be placed that way", " will attempt to be placed in other failed locations first.", - " Also attempts to place junk items in vanilla locations", - "major_bias same as above, but uses the major items' location preferentially", + " Also attempts to place all items in vanilla locations", + "major_only As above, but uses the major items' location preferentially", " major item location are defined as the group of location where", - " the items are found in the vanilla game. Backup locations for items", - " not in the vanilla game will be in the documentation", - "dungeon_bias same as above, but major items are preferentially placed", + " the items are found in the vanilla game.", + "dungeon_only As above, but major items are preferentially placed", " in dungeons locations first", - "cluster_bias same as above, but groups of locations are chosen randomly", + "district As above, but groups of locations are chosen randomly", " from a pool of fixed locations designed to be interesting", " and give major clues about the location of other", - " advancement items. These fixed groups will be documented" + " advancement items. These fixed groups will be documented." ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 4cfeaafb..a2aeb0a2 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -281,10 +281,10 @@ "randomizer.item.sortingalgo": "Item Sorting", "randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.equitable": "Equitable", - "randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla", - "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", - "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", - "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", + "randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill", + "randomizer.item.sortingalgo.major_only": "Major Location Restriction", + "randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction", + "randomizer.item.sortingalgo.district": "District Restriction", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 7f524a33..7b76d8fe 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -118,10 +118,10 @@ "options": [ "balanced", "equitable", - "vanilla_bias", - "major_bias", - "dungeon_bias", - "cluster_bias" + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 2eeb8a6b..96e33cfd 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -3,6 +3,7 @@ import logging from math import ceil from collections import defaultdict +from source.item.District import resolve_districts from DoorShuffle import validate_vanilla_reservation from Dungeons import dungeon_table from Items import item_table, ItemFactory @@ -17,6 +18,8 @@ class ItemPoolConfig(object): self.placeholders = None self.reserved_locations = defaultdict(set) + self.recorded_choices = [] + class LocationGroup(object): def __init__(self, name): @@ -57,7 +60,7 @@ def create_item_pool_config(world): for item in dungeon.all_items: if item.map or item.compass: item.advancement = True - if world.algorithm == 'vanilla_bias': + if world.algorithm == 'vanilla_fill': config.static_placement = {} config.location_groups = {} for player in range(1, world.players + 1): @@ -78,7 +81,7 @@ def create_item_pool_config(world): LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: config.reserved_locations[player].add(loc_name) - elif world.algorithm == 'major_bias': + elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), LocationGroup('Backup') @@ -111,7 +114,7 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) - elif world.algorithm == 'dungeon_bias': + elif world.algorithm == 'dungeon_only': config.location_groups = [ LocationGroup('Dungeons'), LocationGroup('Backup') @@ -205,6 +208,74 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def district_item_pool_config(world): + resolve_districts(world) + if world.algorithm == 'district': + config = world.item_pool_config + config.location_groups = [ + LocationGroup('Districts'), + ] + item_cnt = 0 + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + # set district choices + district_choices = {} + for p in range(1, world.players + 1): + for name, district in world.districts[p].items(): + adjustment = 0 + if district.dungeon: + adjustment = len([i for i in world.get_dungeon(name, p).all_items + if i.is_inside_dungeon_item(world)]) + dist_len = len(district.locations) - adjustment + if name not in district_choices: + district_choices[name] = (district.sphere_one, dist_len) + else: + so, amt = district_choices[name] + district_choices[name] = (so or district.sphere_one, amt + dist_len) + + chosen_locations = defaultdict(set) + location_cnt = 0 + + # choose a sphere one district + sphere_one_choices = [d for d, info in district_choices.items() if info[0]] + sphere_one = random.choice(sphere_one_choices) + so, amt = district_choices[sphere_one] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][sphere_one].locations: + chosen_locations[location].add(player) + del district_choices[sphere_one] + config.recorded_choices.append(sphere_one) + + scale_factors = defaultdict(int) + scale_total = 0 + for p in range(1, world.players + 1): + ent = 'Inverted Ganons Tower' if world.mode[p] == 'inverted' else 'Ganons Tower' + dungeon = world.get_entrance(ent, p).connected_region.dungeon + if dungeon: + scale = world.crystals_needed_for_gt[p] + scale_total += scale + scale_factors[dungeon.name] += scale + scale_total = max(1, scale_total) + scale_divisors = defaultdict(lambda: 1) + scale_divisors.update(scale_factors) + + while location_cnt < item_cnt: + weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] + choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] + so, amt = district_choices[choice] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][choice].locations: + chosen_locations[location].add(player) + del district_choices[choice] + config.recorded_choices.append(choice) + config.placeholders = location_cnt - item_cnt + config.location_groups[0].locations = chosen_locations + + def location_prefilled(location, world, player): if world.swords[player] == 'vanilla': return location in vanilla_swords @@ -390,6 +461,8 @@ def calc_dungeon_limits(world, player): def determine_major_items(world, player): major_item_set = set(major_items) + if world.progressive == 'off': + pass # now what? if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player]: @@ -412,16 +485,18 @@ def determine_major_items(world, player): def classify_major_items(world): - if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' - and world.players > 1): + if world.algorithm in ['major_only', 'dungeon_only', 'district']: config = world.item_pool_config for item in world.itempool: if item.name in config.item_pool[item.player]: - if not item.advancement or not item.priority: + if not item.advancement and not item.priority: if item.smallkey or item.bigkey: item.advancement = True else: item.priority = True + else: + if item.priority: + item.priority = False def figure_out_clustered_choices(world): @@ -488,13 +563,15 @@ def vanilla_fallback(item_to_place, locations, world): return [] -def filter_locations(item_to_place, locations, world): - if world.algorithm == 'vanilla_bias': +def filter_locations(item_to_place, locations, world, vanilla_skip=False): + if world.algorithm == 'vanilla_fill': config, filtered = world.item_pool_config, [] item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name if item_name in config.static_placement[item_to_place.player]: restricted = config.static_placement[item_to_place.player][item_name] filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + if vanilla_skip and len(filtered) == 0: + return filtered i = 0 while len(filtered) <= 0: if i >= len(config.location_groups[item_to_place.player]): @@ -503,7 +580,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] i += 1 return filtered - if world.algorithm in ['major_bias', 'dungeon_bias']: + if world.algorithm in ['major_only', 'dungeon_only']: config = world.item_pool_config if item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -513,7 +590,7 @@ def filter_locations(item_to_place, locations, world): filtered = [l for l in locations if l.name in restricted] # bias toward certain location in overflow? (thinking about this for major_bias) return filtered if len(filtered) > 0 else locations - if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'district': config = world.item_pool_config if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations @@ -835,7 +912,7 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', - 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', 'Progressive Bow', 'Progressive Bow (Alt)'} diff --git a/source/item/District.py b/source/item/District.py new file mode 100644 index 00000000..7b48ede8 --- /dev/null +++ b/source/item/District.py @@ -0,0 +1,169 @@ +from collections import deque + +from BaseClasses import CollectionState, RegionType +from Dungeons import dungeon_table + + +class District(object): + + def __init__(self, name, locations, entrances=None, dungeon=None): + self.name = name + self.dungeon = dungeon + self.locations = locations + self.entrances = entrances if entrances else [] + self.sphere_one = False + + +def create_districts(world): + world.districts = {} + for p in range(1, world.players + 1): + create_district_helper(world, p) + + +def create_district_helper(world, player): + inverted = world.mode[player] == 'inverted' + districts = {} + kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'} + nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'} + central_lw_locations = {'Sunken Treasure', 'Flute Spot'} + desert_locations = {'Purple Chest', 'Desert Ledge'} + lake_locations = {'Hobo'} + east_lw_locations = {"Zora's Ledge", 'King Zora'} + lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet'} + east_dw_locations = {'Pyramid', 'Catfish'} + south_dw_locations = {'Stumpy', 'Digging Game', 'Bombos Tablet', 'Lake Hylia Island'} + voo_north_locations = {'Bumper Cave Ledge'} + ddm_locations = {'Floating Island'} + + kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)', + 'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House', + 'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)', + 'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library', + 'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop'] + nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', + 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop', + 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] + central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs', + 'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Cave Shop (Lake Hylia)', + 'Lake Hylia Fortune Teller', 'Hyrule Castle Secret Entrance Drop'] + desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy', + 'Aginahs Cave', '50 Rupee Cave'] + lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave'] + east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut', + 'Long Fairy Cave', 'Potion Shop'] + lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave', + 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', + 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy'] + east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint', + 'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole'] + south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy', + 'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave', + 'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Cave 45'] + voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', + 'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop', 'Graveyard Cave', + 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'Checkerboard Cave'] + ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', + 'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave', + 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave'] + + if inverted: + south_dw_locations.remove('Bombos Tablet') + south_dw_locations.remove('Lake Hylia Island') + voo_north_locations.remove('Bumper Cave Ledge') + ddm_locations.remove('Floating Island') + desert_locations.add('Bombos Tablet') + lake_locations.add('Lake Hylia Island') + nw_lw_locations.add('Bumper Cave Ledge') + lw_dm_locations.add('Floating Island') + + south_dw_entrances.remove('Cave 45') + central_lw_entrances.append('Cave 45') + voo_north_entrances.remove('Graveyard Cave') + nw_lw_entrances.append('Graveyard Cave') + mire_entrances.remove('Checkerboard Cave') + desert_entrances.append('Checkerboard Cave') + ddm_entrances.remove('Mimic Cave') + lw_dm_entrances.append('Mimic Cave') + + south_dw_entrances.remove('Big Bomb Shop') + central_lw_entrances.append('Inverted Big Bomb Shop') + central_lw_entrances.remove('Links House') + south_dw_entrances.append('Inverted Links House') + voo_north_entrances.remove('Dark Sanctuary') + voo_north_entrances.append('Inverted Dark Sanctuary') + ddm_entrances.remove('Ganons Tower') + central_lw_entrances.append('Inverted Ganons Tower') + central_lw_entrances.remove('Agahnims Tower') + ddm_entrances.append('Inverted Agahnims Tower') + east_dw_entrances.remove('Pyramid Entrance') + central_lw_entrances.append('Inverted Pyramid Entrance') + east_dw_entrances.remove('Pyramid Hole') + central_lw_entrances.append('Inverted Pyramid Hole') + + districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances) + districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances) + districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances) + districts['Desert'] = District('Desert', desert_locations, entrances=desert_entrances) + districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances) + districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances) + districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances) + districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances) + districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances) + districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations, + entrances=voo_north_entrances) + districts['The Mire'] = District('The Mire', set(), entrances=mire_entrances) + districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances) + districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()}) + + world.districts[player] = districts + + +def resolve_districts(world): + create_districts(world) + state = CollectionState(world) + state.sweep_for_events() + for player in range(1, world.players + 1): + check_set = find_reachable_locations(state, player) + used_locations = {l for d in world.districts[player].values() for l in d.locations} + for name, district in world.districts[player].items(): + if district.dungeon: + layout = world.dungeon_layouts[player][district.dungeon] + district.locations.update([l.name for r in layout.master_sector.regions + for l in r.locations if not l.item and l.real]) + else: + for entrance in district.entrances: + ent = world.get_entrance(entrance, player) + queue = deque([ent.connected_region]) + visited = set() + while len(queue) > 0: + region = queue.pop() + visited.add(region) + if region.type == RegionType.Cave: + for location in region.locations: + if location.name not in used_locations and not location.item and location.real: + district.locations.add(location.name) + used_locations.add(location.name) + for ext in region.exits: + if ext.connected_region not in visited: + queue.appendleft(ext.connected_region) + district.sphere_one = len(check_set.intersection(district.locations)) > 0 + + +def find_reachable_locations(state, player): + check_set = set() + for region in state.reachable_regions[player]: + for location in region.locations: + if location.can_reach(state) and not location.forced_item and location.real: + check_set.add(location.name) + return check_set