New non-recursive ER Must-Exit fill algorithm

This commit is contained in:
codemann8
2023-01-16 07:13:44 -06:00
parent 904ff5ec85
commit a3edbaf8b9

View File

@@ -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):