From a3edbaf8b981a463999e133833c8c9688d90e5b5 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 16 Jan 2023 07:13:44 -0600 Subject: [PATCH] New non-recursive ER Must-Exit fill algorithm --- EntranceShuffle.py | 194 ++++++++++++++++++++++++++++++++------------- 1 file changed, 140 insertions(+), 54 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index cc0faba9..ef1d36c6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1478,65 +1478,151 @@ def junk_fill_inaccessible(world, player): def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): - invFlag = world.mode[player] == 'inverted' + resolved_regions = list() + inaccessible_regions = list() + def find_inacessible_ow_regions(): + nonlocal inaccessible_regions + find_inaccessible_regions(world, player) + inaccessible_regions = list(world.inaccessible_regions[player]) + + # find OW regions that don't have a multi-entrance dungeon exit connected + glitch_regions = ['Central Cliffs', 'Eastern Cliff', 'Desert Northeast Cliffs', 'Hyrule Castle Water', + 'Dark Central Cliffs', 'Darkness Cliff', 'Mire Northeast Cliffs', 'Pyramid Water'] + multi_dungeon_exits = { + 'Hyrule Castle South Portal', 'Hyrule Castle West Portal', 'Hyrule Castle East Portal', 'Sanctuary Portal', + 'Desert South Portal', 'Desert West Portal', + 'Skull 2 East Portal', 'Skull 2 West Portal', + 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Eye Bridge Portal' + } + for region_name in world.inaccessible_regions[player]: + if region_name in resolved_regions \ + or (region_name == 'Pyramid Exit Ledge' and (world.shuffle[player] != 'insanity' or world.is_tile_swapped(0x1b, player))) \ + or (region_name == 'Spiral Mimic Ledge Extend' and not world.is_tile_swapped(0x05, player)) \ + or (world.logic[player] in ['noglitches', 'minorglitches'] and region_name in glitch_regions): + # removing irrelevant and resolved regions + inaccessible_regions.remove(region_name) + continue + region = world.get_region(region_name, player) + if region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: + inaccessible_regions.remove(region_name) + continue + if world.shuffle[player] != 'insanity': + for exit in region.exits: + # because dungeon regions haven't been connected yet, the inaccessibility check won't be able to know it's reachable yet + # TODO: Should also cascade to other regions and remove them as inaccessibles + if exit.connected_region and exit.connected_region.name in multi_dungeon_exits: + inaccessible_regions.remove(region_name) + break - if stack_size3a() > 500: - from DungeonGenerator import GenerationException - raise GenerationException(f'Infinite loop detected at \'connect_inaccessible_regions\'') + find_inacessible_ow_regions() - random.shuffle(lw_entrances) - random.shuffle(dw_entrances) - - find_inaccessible_regions(world, player) - - # remove regions that have a dungeon entrance - accessible_regions = list() - for region_name in world.inaccessible_regions[player]: + # keep track of neighboring regions for later consolidation + must_exit_links = OrderedDict() + for region_name in inaccessible_regions: region = world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: - accessible_regions.append(region_name) - break - for region_name in accessible_regions.copy(): - accessible_regions = list(OrderedDict.fromkeys(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) - world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] - - # split inaccessible into 2 lists for each world - inaccessible_regions = list(world.inaccessible_regions[player]) + must_exit_links[region_name] = [x.connected_region.name for x in region.exits if x.connected_region and x.connected_region.name in inaccessible_regions] + + # group neighboring regions together, separated by one-ways + def consolidate_group(region): + processed_regions.append(region) + must_exit_links_copy.pop(region) + region_group.append(region) + for dest_region in must_exit_links[region]: + if region in must_exit_links[dest_region]: + if dest_region not in processed_regions: + consolidate_group(dest_region) + else: + one_ways.append(tuple((region, dest_region))) + + processed_regions = list() + must_exit_candidates = list() + one_ways = list() + must_exit_links_copy = must_exit_links.copy() + while len(must_exit_links_copy): + region_group = list() + region_name = next(iter(must_exit_links_copy)) + consolidate_group(region_name) + must_exit_candidates.append(region_group) + + # get available entrances in each group + for regions in must_exit_candidates: + entrances = list() + for region_name in regions: + region = world.get_region(region_name, player) + entrances = entrances + [x.name for x in region.exits if x.spot_type == 'Entrance' and not x.connected_region] + entrances = [e for e in entrances if e in entrance_pool and e not in ignore_list] + must_exit_candidates[must_exit_candidates.index(regions)] = tuple((regions, entrances)) + + # necessary for circular relations between region groups, it will pick the last group + # and fill one of those entrances, and we don't want it to bias the same group + random.shuffle(must_exit_candidates) + + # remove must exit candidates that would be made accessible thru other region groups + def find_group(region): + for group in must_exit_candidates: + regions, _ = group + if region in regions: + return group + raise Exception(f'Could not find region group for {region}') + + def cascade_ignore(group): + nonlocal ignored_regions + regions, _ = group + ignored_regions = ignored_regions + regions + for from_region, to_region in one_ways: + if from_region in regions and to_region not in ignored_regions: + cascade_ignore(find_group(to_region)) + + def process_group(group): + nonlocal processed_regions, ignored_regions + regions, entrances = group + must_exit_candidates_copy.remove(group) + processed_regions = processed_regions + regions + if regions[0] not in ignored_regions: + for from_region, to_region in one_ways: + if to_region in regions and from_region not in ignored_regions + processed_regions: + process_group(find_group(from_region)) # process the parent region group + if regions[0] not in ignored_regions: + # this is the top level region + if len(entrances): + # we will fulfill must exit here and cascade access to children + must_exit_regions.append(group) + cascade_ignore(group) + else: + ignored_regions = ignored_regions + regions + + processed_regions = list() + ignored_regions = list() must_exit_regions = list() - otherworld_must_exit_regions = list() - for region_name in inaccessible_regions.copy(): - region = world.get_region(region_name, player) - if region.type not in [RegionType.LightWorld, RegionType.DarkWorld] or not any((not exit.connected_region and exit.spot_type == 'Entrance') for exit in region.exits) \ - or (region_name == 'Pyramid Exit Ledge' and (world.shuffle[player] != 'insanity' or world.is_tile_swapped(0x1b, player))) \ - or region_name in ['Hyrule Castle Water', 'Pyramid Water']: - inaccessible_regions.remove(region_name) - elif region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - must_exit_regions.append(region_name) - elif region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): - otherworld_must_exit_regions.append(region_name) - - def connect_one(region_name, pool): - inaccessible_entrances = list() - region = world.get_region(region_name, player) - for exit in region.exits: - if not exit.connected_region and exit.name in [e for e in entrance_pool if e not in ignore_list] and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): - inaccessible_entrances.append(exit.name) - if len(inaccessible_entrances): - random.shuffle(inaccessible_entrances) - connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list) - - # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - pool = [e for e in (lw_entrances if world.shuffle[player] in ['lean', 'crossed', 'insanity'] else dw_entrances) if e in entrance_pool] - if len(otherworld_must_exit_regions) > 0 and len(pool): - random.shuffle(otherworld_must_exit_regions) - connect_one(otherworld_must_exit_regions[0], pool) - elif len(must_exit_regions) > 0: + must_exit_candidates_copy = must_exit_candidates.copy() + while len(must_exit_candidates_copy): + region_group = next(iter(must_exit_candidates_copy)) + process_group(region_group) + + # connect must exits + random.shuffle(must_exit_regions) + must_exits_lw = list() + must_exits_dw = list() + for regions, entrances in must_exit_regions: + region = world.get_region(regions[0], player) + if region.type == RegionType.LightWorld: + must_exits_lw.append(random.choice(entrances)) + else: + must_exits_dw.append(random.choice(entrances)) + if world.shuffle[player] in ['lean', 'crossed', 'insanity']: # cross world + pool = [e for e in lw_entrances + dw_entrances if e in entrance_pool] + connect_mandatory_exits(world, pool, caves, must_exits_lw + must_exits_dw, player) + else: pool = [e for e in lw_entrances if e in entrance_pool] - if len(pool): - random.shuffle(must_exit_regions) - connect_one(must_exit_regions[0], pool) + connect_mandatory_exits(world, pool, caves, must_exits_lw, player) + pool = [e for e in dw_entrances if e in entrance_pool] + connect_mandatory_exits(world, pool, caves, must_exits_dw, player) + + # check accessibility afterwards + find_inacessible_ow_regions() + if len(inaccessible_regions) > 0: + logging.getLogger('').debug(f'Could not resolve inaccessible regions: [{", ".join(inaccessible_regions)}]') + logging.getLogger('').debug(f'^ This is most often a false positive because Dungeon regions aren\'t connected yet') def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits):