Merge branch 'DoorDevUnstable' into Sandbox

# Conflicts:
#	ItemList.py
#	Items.py
#	Main.py
#	Rom.py
#	data/base2current.bps
This commit is contained in:
aerinon
2023-03-10 13:43:38 -07:00
63 changed files with 4599 additions and 988 deletions

View File

@@ -6,7 +6,7 @@ from enum import unique, Flag
from typing import DefaultDict, Dict, List
from itertools import chain
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import PotFlags, LocationType, Direction
from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
@@ -15,13 +15,12 @@ from Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2
# from DungeonGenerator import generate_dungeon
from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances
from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations
from DungeonGenerator import valid_region_to_explore
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
from KeyDoorShuffle import validate_bk_layout, check_bk_special
from KeyDoorShuffle import validate_bk_layout
from Utils import ncr, kth_combination
@@ -129,7 +128,7 @@ def link_doors_prep(world, player):
vanilla_key_logic(world, player)
def link_doors_main(world, player):
def create_dungeon_pool(world, player):
pool = None
if world.doorShuffle[player] == 'basic':
pool = [([name], regions) for name, regions in dungeon_regions.items()]
@@ -143,6 +142,11 @@ def link_doors_main(world, player):
elif world.doorShuffle[player] != 'vanilla':
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
return pool
def link_doors_main(world, player):
pool = create_dungeon_pool(world, player)
if pool:
main_dungeon_pool(pool, world, player)
if world.doorShuffle[player] != 'vanilla':
@@ -226,21 +230,37 @@ def vanilla_key_logic(world, player):
add_inaccessible_doors(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player)
for builder in builders:
enabled_entrances = world.enabled_entrances[player] = {}
builder_queue = deque(builders)
last_key, loops = None, 0
while len(builder_queue) > 0:
builder = builder_queue.popleft()
origin_list = entrances_map[builder.name]
start_regions = convert_regions(origin_list, world, player)
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(builder, start_regions, doors, world, player)
valid = validate_key_layout(key_layout, world, player)
if not valid:
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
builder.key_door_proposal = doors
if player not in world.key_logic.keys():
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
log_key_logic(builder.name, key_layout.key_logic)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
if len(origin_list) <= 0:
if last_key == builder.name or loops > 1000:
origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name
if len(origin_list) > 0 else 'no origin')
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
builder_queue.append(builder)
last_key = builder.name
loops += 1
else:
find_new_entrances(builder.master_sector, entrances_map, connections, potentials,
enabled_entrances, world, player)
start_regions = convert_regions(origin_list, world, player)
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(builder, start_regions, doors, {}, world, player)
valid = validate_key_layout(key_layout, world, player)
if not valid:
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
builder.key_door_proposal = doors
if player not in world.key_logic.keys():
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
log_key_logic(builder.name, key_layout.key_logic)
# if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
# validate_vanilla_key_logic(world, player)
@@ -477,14 +497,14 @@ def choose_portals(world, player):
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
for target_region, possible_portals in info.required_passage.items():
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True,
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
portal.destination = True
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
for i in range(0, dead_end_choices):
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True,
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag)
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
if choice.deadEnd:
@@ -542,7 +562,9 @@ def customizer_portals(master_door_list, world, player):
custom_doors = world.customizer.get_doors()[player]
if custom_doors and 'lobbies' in custom_doors:
for portal, assigned_door in custom_doors['lobbies'].items():
door = next(x for x in master_door_list if x.name == assigned_door)
door = next((x for x in master_door_list if x.name == assigned_door), None)
if door is None:
raise Exception(f'{assigned_door} not found. Check for typos')
custom_portals[portal] = door
assigned_doors.add(door)
if custom_doors and 'doors' in custom_doors:
@@ -552,9 +574,27 @@ def customizer_portals(master_door_list, world, player):
if isinstance(dest, str):
door = world.get_door(dest, player)
assigned_doors.add(door)
else:
elif 'dest' in dest:
door = world.get_door(dest['dest'], player)
assigned_doors.add(door)
# restricts connected doors to the customized portals
if assigned_doors:
pool = create_dungeon_pool(world, player)
if pool:
pool_map = {}
for pool, region_list in pool:
sector_pool = convert_to_sectors(region_list, world, player)
merge_sectors(sector_pool, world, player)
for p in pool:
pool_map[p] = sector_pool
for portal, assigned_door in custom_portals.items():
portal_region = world.get_door(assigned_door, player).entrance.parent_region
portal_dungeon = world.get_region(f'{portal} Portal', player).dungeon.name
sector_pool = pool_map[portal_dungeon]
sector = next((s for s in sector_pool if portal_region in s.regions), None)
for door in sector.outstanding_doors:
if door.portalAble:
door.dungeonLink = portal_dungeon
return custom_portals, assigned_doors
@@ -798,15 +838,18 @@ def main_dungeon_pool(dungeon_pool, world, player):
hc = world.get_dungeon('Hyrule Castle', player)
hc_compass = ItemFactory('Compass (Escape)', player)
hc_compass.advancement = world.restrict_boss_items[player] != 'none'
hc.dungeon_items.append(hc_compass)
if hc.dungeon_items.count(hc_compass) < 1:
hc.dungeon_items.append(hc_compass)
if 'Agahnims Tower' in pool:
at = world.get_dungeon('Agahnims Tower', player)
at_compass = ItemFactory('Compass (Agahnims Tower)', player)
at_compass.advancement = world.restrict_boss_items[player] != 'none'
at.dungeon_items.append(at_compass)
if at.dungeon_items.count(at_compass) < 1:
at.dungeon_items.append(at_compass)
at_map = ItemFactory('Map (Agahnims Tower)', player)
at_map.advancement = world.restrict_boss_items[player] != 'none'
at.dungeon_items.append(at_map)
if at.dungeon_items.count(at_map) < 1:
at.dungeon_items.append(at_map)
sector_pool = convert_to_sectors(region_list, world, player)
merge_sectors(sector_pool, world, player)
# todo: which dungeon to create
@@ -831,7 +874,8 @@ def main_dungeon_pool(dungeon_pool, world, player):
for name in pool:
builder = world.dungeon_layouts[player][name]
region_set = builder.master_sector.region_set()
builder.bk_required = builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
builder.bk_required = (builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
or len(world.key_logic[player][name].bk_chests) > 0)
dungeon = world.get_dungeon(name, player)
if not builder.bk_required or builder.bk_provided:
dungeon.big_key = None
@@ -1018,7 +1062,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
find_standard_origins(builder, recombinant_builders, origin_list)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player)
if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player):
# todo: figure out pre-validate, ensure all needed origins are enabled?
if len(origin_list) <= 0: # or not pre_validate(builder, origin_list, split_dungeon, world, player):
if last_key == builder.name or loops > 1000:
origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin'
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
@@ -1068,14 +1113,14 @@ def determine_entrance_list(world, player):
connections = {}
for key, portal_list in dungeon_portals.items():
entrance_map[key] = []
r_names = {}
r_names = []
if key in dungeon_drops.keys():
for drop in dungeon_drops[key]:
r_names[drop] = None
r_names.append((drop, None))
for portal_name in portal_list:
portal = world.get_portal(portal_name, player)
r_names[portal.door.entrance.parent_region.name] = portal
for region_name, portal in r_names.items():
r_names.append((portal.door.entrance.parent_region.name, portal))
for region_name, portal in r_names:
if portal:
region = world.get_region(portal.name + ' Portal', player)
else:
@@ -1236,10 +1281,13 @@ def cross_dungeon(world, player):
if world.restrict_boss_items[player] != 'none':
hc_compass.advancement = at_compass.advancement = at_map.advancement = True
hc = world.get_dungeon('Hyrule Castle', player)
hc.dungeon_items.append(hc_compass)
if hc.dungeon_items.count(hc_compass) < 1:
hc.dungeon_items.append(hc_compass)
at = world.get_dungeon('Agahnims Tower', player)
at.dungeon_items.append(at_compass)
at.dungeon_items.append(at_map)
if at.dungeon_items.count(at_compass) < 1:
at.dungeon_items.append(at_compass)
if at.dungeon_items.count(at_map) < 1:
at.dungeon_items.append(at_map)
setup_custom_door_types(world, player)
assign_cross_keys(dungeon_builders, world, player)
@@ -1695,7 +1743,6 @@ def setup_custom_door_types(world, player):
custom_doors = custom_doors[player]
if 'doors' not in custom_doors:
return
# todo: dash/bomb door pool specific
customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door']
world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types}
for door, dest in custom_doors['doors'].items():
@@ -1707,8 +1754,8 @@ def setup_custom_door_types(world, player):
if d.type == DoorType.SpiralStairs:
type_map[door_kind][dungeon.name].append(d)
else:
# check if the
if d.dest.type in [DoorType.Interior, DoorType.Normal]:
# check if the dest is paired
if d.dest and d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door':
type_map[door_kind][dungeon.name].append((d, d.dest))
else:
type_map[door_kind][dungeon.name].append(d)
@@ -1737,12 +1784,12 @@ class DoorTypePool:
self.tricky += counts[6]
def chaos_shuffle(self, counts):
weights = [1, 2, 4, 3, 2, 1]
weights = [1, 2, 4, 3, 2]
return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)]
@staticmethod
def get_choices(number):
return [max(number-i, 0) for i in range(-1, 5)]
return [max(number+i, 0) for i in range(-1, 4)]
class BuilderDoorCandidates:
@@ -1756,88 +1803,103 @@ class BuilderDoorCandidates:
def shuffle_door_types(door_type_pools, paths, world, player):
start_regions_map = {}
for name, builder in world.dungeon_layouts[player].items():
start_regions = convert_regions(builder.path_entrances, world, player)
start_regions = convert_regions(find_possible_entrances(world, player, builder), world, player)
start_regions_map[name] = start_regions
builder.candidates = BuilderDoorCandidates()
all_custom = defaultdict(list)
if player in world.custom_door_types:
for custom_dict in world.custom_door_types[player].values():
for dungeon, doors in custom_dict.items():
all_custom[dungeon].extend(doors)
world.paired_doors[player].clear()
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player)
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player)
# big keys
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# small keys
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# bombable / dashable
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# handle paired list
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player):
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player):
used_doors = set()
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, trap_map, flex_map = {}, {}, {}
remaining = door_type_pool.traps
if player in world.custom_door_types:
custom_trap_doors = world.custom_door_types[player]['Trap Door']
else:
custom_trap_doors = defaultdict(list)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_trappable_candidates(builder, world, player)
if custom_trap_doors[dungeon]:
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon])
remaining -= len(custom_trap_doors[dungeon])
ttl += len(builder.candidates.trap)
if ttl == 0:
continue
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.trap)
calc = int(round(proportion * door_type_pool.traps/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], paths, world, player,
drop=True)
trap_map[dungeon] = valid_traps
if trap_number < suggestion_map[dungeon]:
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - trap_number
suggestion_map[dungeon] = trap_number
builder_order = [x for x in pool if flex_map[x] > 0]
random.shuffle(builder_order)
queue = deque(builder_order)
while len(queue) > 0 and remaining > 0:
dungeon = queue.popleft()
builder = world.dungeon_layouts[player][dungeon]
increased = suggestion_map[dungeon] + 1
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
paths, world, player)
if valid_traps:
if world.trap_door_mode[player] != 'oneway':
ttl = 0
suggestion_map, trap_map, flex_map = {}, {}, {}
remaining = door_type_pool.traps
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
custom_trap_doors = world.custom_door_types[player]['Trap Door']
else:
custom_trap_doors = defaultdict(list)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if 'Mire Warping Pool' in builder.master_sector.region_set():
custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player))
world.custom_door_types[player]['Trap Door'] = custom_trap_doors
find_trappable_candidates(builder, world, player)
if all_custom[dungeon]:
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon])
remaining -= len(custom_trap_doors[dungeon])
ttl += len(builder.candidates.trap)
if ttl == 0:
continue
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.trap)
calc = int(round(proportion * door_type_pool.traps/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], paths, world, player,
drop=True)
trap_map[dungeon] = valid_traps
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if flex_map[dungeon] > 0:
queue.append(dungeon)
# time to re-assign
if trap_number < suggestion_map[dungeon]:
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - trap_number
suggestion_map[dungeon] = trap_number
builder_order = [x for x in pool if flex_map[x] > 0]
random.shuffle(builder_order)
queue = deque(builder_order)
while len(queue) > 0 and remaining > 0:
dungeon = queue.popleft()
builder = world.dungeon_layouts[player][dungeon]
increased = suggestion_map[dungeon] + 1
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
paths, world, player)
if valid_traps:
trap_map[dungeon] = valid_traps
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if flex_map[dungeon] > 0:
queue.append(dungeon)
# time to re-assign
else:
trap_map = {dungeon: [] for dungeon in pool}
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if 'Mire Warping Pool' in builder.master_sector.region_set():
trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player))
reassign_trap_doors(trap_map, world, player)
for name, traps in trap_map.items():
used_doors.update(traps)
return used_doors
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, bk_map, flex_map = {}, {}, {}
remaining = door_type_pool.bigs
if player in world.custom_door_types:
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
custom_bk_doors = world.custom_door_types[player]['Big Key Door']
else:
custom_bk_doors = defaultdict(list)
@@ -1845,16 +1907,17 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_bk_doors[dungeon]:
builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon])
if all_custom[dungeon]:
builder.candidates.big = filter_key_door_pool(builder.candidates.big, all_custom[dungeon])
remaining -= len(custom_bk_doors[dungeon])
ttl += len(builder.candidates.big)
if ttl == 0:
continue
remaining = max(0, remaining)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.big)
calc = int(round(proportion * door_type_pool.bigs/ttl))
calc = int(round(proportion * remaining/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
@@ -1891,48 +1954,56 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
return used_doors
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, small_map, flex_map = {}, {}, {}
remaining = door_type_pool.smalls
total_keys = remaining
if player in world.custom_door_types:
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
custom_key_doors = world.custom_door_types[player]['Key Door']
else:
custom_key_doors = defaultdict(list)
total_adjustable = len(pool) > 1
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if not total_adjustable:
builder.total_keys = total_keys
find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_key_doors[dungeon]:
builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon])
remaining -= len(custom_key_doors[dungeon])
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
custom_doors = 0
if all_custom[dungeon]:
builder.candidates.small = filter_key_door_pool(builder.candidates.small, all_custom[dungeon])
custom_doors = len(custom_key_doors[dungeon])
remaining -= custom_doors
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + custom_doors
total_keys -= builder.key_drop_cnt
ttl += builder.key_doors_num
remaining = max(0, remaining)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
calculated = int(round(builder.key_doors_num*total_keys/ttl))
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
limit = min(max_keys, cand_len)
limit = min(max_keys, cand_len, max_computation)
suggested = min(calculated, limit)
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
while combo_size > 500000 and suggested > 0:
suggested -= 1
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt
remaining -= suggested + builder.key_drop_cnt
key_door_num = min(suggested + builder.key_drop_cnt, max_computation)
combo_size = ncr(len(builder.candidates.small), key_door_num)
suggestion_map[dungeon] = builder.key_doors_num = key_door_num
remaining -= key_door_num + builder.key_drop_cnt
builder.combo_size = combo_size
flex_map[dungeon] = (limit - suggested) if suggested < limit else 0
flex_map[dungeon] = (limit - key_door_num) if key_door_num < limit else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if total_adjustable:
builder.total_keys = max(suggestion_map[dungeon], builder.key_drop_cnt)
valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], world, player)
small_map[dungeon] = valid_doors
actual_chest_keys = small_number - builder.key_drop_cnt
if actual_chest_keys < suggestion_map[dungeon]:
if total_adjustable:
builder.total_keys = actual_chest_keys + builder.key_drop_cnt
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - actual_chest_keys
suggestion_map[dungeon] = small_number
@@ -1943,6 +2014,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
builder = queue.popleft()
dungeon = builder.name
increased = suggestion_map[dungeon] + 1
if increased > max_computation:
continue
builder.key_doors_num = increased
valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon],
world, player)
@@ -1951,6 +2024,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if total_adjustable:
builder.total_keys = max(increased, builder.key_drop_cnt)
if flex_map[dungeon] > 0:
builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num)
queue.append(builder)
@@ -1976,14 +2051,14 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
return used_doors
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, bd_map = {}, {}
remaining_bomb = door_type_pool.bombable
remaining_dash = door_type_pool.dashable
if player in world.custom_door_types:
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
custom_bomb_doors = world.custom_door_types[player]['Bomb Door']
custom_dash_doors = world.custom_door_types[player]['Dash Door']
else:
@@ -1993,11 +2068,9 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_bomb_doors[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon])
if all_custom[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, all_custom[dungeon])
remaining_bomb -= len(custom_bomb_doors[dungeon])
if custom_dash_doors[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon])
remaining_dash -= len(custom_dash_doors[dungeon])
ttl += len(builder.candidates.bomb_dash)
if ttl == 0:
@@ -2094,7 +2167,7 @@ def find_trappable_candidates(builder, world, player):
if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]]
for d in filtered_doors:
# I only support the first 3 due to the trapFlag right now
if 0 <= d.doorListPos < 3 and not d.entranceFlag:
if 0 <= d.doorListPos < 3 and not d.entranceFlag and d.name != 'Skull Small Hall WS':
room = world.get_room(d.roomIndex, player)
kind = room.kind(d)
if d.type == DoorType.Interior:
@@ -2115,20 +2188,21 @@ def find_trappable_candidates(builder, world, player):
for ext in world.get_region(r, player).exits:
if ext.door:
d = ext.door
if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name:
if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d):
builder.candidates.trap.append(d)
def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True):
trap_door_pool = builder.candidates.trap
trap_doors_needed = suggested
if player in world.custom_door_types:
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name]
else:
custom_trap_doors = []
if custom_trap_doors:
trap_door_pool = filter_key_door_pool(trap_door_pool, custom_trap_doors)
trap_doors_needed -= len(custom_trap_doors)
trap_doors_needed = max(0, trap_doors_needed)
if len(trap_door_pool) < trap_doors_needed:
if not drop:
return None, 0
@@ -2139,7 +2213,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
proposal.extend(custom_trap_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
while not validate_trap_layout(proposal, builder, start_regions, paths, world, player):
itr += 1
if itr >= len(sample_list):
@@ -2160,14 +2234,32 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
# eliminate start region if portal marked as destination
def filter_start_regions(builder, start_regions, world, player):
std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle'
excluded = {}
excluded = {} # todo: drop lobbies, might be better to white list instead (two entrances per region)
event_doors = {}
for region in start_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal and portal.destination:
excluded[region] = None
# make sure that a drop is not accessible for this "destination"
drop_region = next((x.parent_region for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop'), None)
if not drop_region:
excluded[region] = None
if portal and not portal.destination:
portal_entrance_region = portal.door.entrance.parent_region.name
if portal_entrance_region not in builder.path_entrances:
excluded[region] = None
if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'):
excluded[region] = None
return [x for x in start_regions if x not in excluded.keys()]
if portal is None:
entrance = next((x for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop'), None)
event_doors[entrance] = None
else:
event_doors[portal.find_portal_entrance()] = None
return [x for x in start_regions if x not in excluded.keys()], event_doors
def validate_trap_layout(proposal, builder, start_regions, paths, world, player):
@@ -2193,6 +2285,7 @@ def find_bk_special_location(builder, world, player):
return loc
return None
def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player):
cached_initial_state = None
if len(paths[dungeon_name]) > 0:
@@ -2238,7 +2331,7 @@ def reassign_trap_doors(trap_map, world, player):
logger = logging.getLogger('')
for name, traps in trap_map.items():
builder = world.dungeon_layouts[player][name]
queue = deque(find_current_trap_doors(builder))
queue = deque(find_current_trap_doors(builder, world, player))
while len(queue) > 0:
d = queue.pop()
if d.type is DoorType.Interior and d not in traps:
@@ -2257,16 +2350,21 @@ def reassign_trap_doors(trap_map, world, player):
d.blocked = False
for d in traps:
change_door_to_trap(d, world, player)
world.spoiler.set_door_type(d.name, 'Trap Door', player)
logger.debug('Trap Door: %s', d.name)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})')
def find_current_trap_doors(builder):
def exclude_boss_traps(d):
return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW']
def find_current_trap_doors(builder, world, player):
checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True)
current_doors = []
for region in builder.master_sector.regions:
for ext in region.exits:
d = ext.door
if d and d.blocked and d.trapFlag != 0:
if d and d.blocked and d.trapFlag != 0 and checker(d):
current_doors.append(d)
return current_doors
@@ -2276,7 +2374,9 @@ def change_door_to_trap(d, world, player):
if d.type is DoorType.Interior:
kind = room.kind(d)
new_kind = None
if kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
if kind == DoorKind.Trap:
new_kind = DoorKind.Trap
elif kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
new_kind = DoorKind.Trap
elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]:
new_kind = DoorKind.Trap
@@ -2356,21 +2456,16 @@ def find_big_key_door_candidates(region, checked, used, world, player):
if valid and d.dest not in candidates: # interior doors are not separable yet
candidates.append(d.dest)
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2384,13 +2479,14 @@ def find_big_key_door_candidates(region, checked, used, world, player):
def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True):
bk_door_pool = builder.candidates.big
bk_doors_needed = suggested
if player in world.custom_door_types:
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name]
else:
custom_bk_doors = []
if custom_bk_doors:
bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors)
bk_doors_needed -= len(custom_bk_doors)
bk_doors_needed = max(0, bk_doors_needed)
if len(bk_door_pool) < bk_doors_needed:
if not drop:
return None, 0
@@ -2401,7 +2497,7 @@ def find_valid_bk_combination(builder, suggested, start_regions, world, player,
proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed)
proposal.extend(custom_bk_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
while not validate_bk_layout(proposal, builder, start_regions, world, player):
itr += 1
if itr >= len(sample_list):
@@ -2452,17 +2548,20 @@ def reassign_big_key_doors(bk_map, world, player):
if d1.type is DoorType.Interior:
change_door_to_big_key(d1, world, player)
d2.bigKey = True # ensure flag is set
if d2.smallKey:
d2.smallKey = False
else:
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
change_door_to_big_key(d1, world, player)
change_door_to_big_key(d2, world, player)
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player)
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}')
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player)
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
else:
d = obj
if d.type is DoorType.Interior:
change_door_to_big_key(d, world, player)
d.dest.bigKey = True # ensure flag is set
if world.door_type_mode[player] != 'original':
d.dest.bigKey = True # ensure flag is set when bk doors are double sided
elif d.type is DoorType.SpiralStairs:
pass # we don't have spiral stairs candidates yet that aren't already key doors
elif d.type is DoorType.Normal:
@@ -2473,12 +2572,14 @@ def reassign_big_key_doors(bk_map, world, player):
if stateful_door(d.dest, dest_room.kind(d.dest)):
change_door_to_big_key(d.dest, world, player)
add_pair(d, d.dest, world, player)
world.spoiler.set_door_type(d.name, 'Big Key Door', player)
logger.debug(f'Big Key Door: {d.name}')
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player)
logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})')
def change_door_to_big_key(d, world, player):
d.bigKey = True
if d.smallKey:
d.smallKey = False
room = world.get_room(d.roomIndex, player)
if room.doorList[d.doorListPos][1] != DoorKind.BigKey:
verify_door_list_pos(d, room, world, player)
@@ -2522,14 +2623,14 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
logger = logging.getLogger('')
key_door_pool = list(builder.candidates.small)
key_doors_needed = target
if player in world.custom_door_types:
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name]
else:
custom_key_doors = []
if custom_key_doors: # could validate that each custom item is in the candidates
key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors)
key_doors_needed -= len(custom_key_doors)
key_doors_needed = max(0, key_doors_needed)
# find valid combination of candidates
if len(key_door_pool) < key_doors_needed:
if not drop_keys:
@@ -2544,9 +2645,10 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
sample_list = build_sample_list(combinations)
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
proposal.extend(custom_key_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
builder.key_doors_num = len(proposal)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
key_layout = build_key_layout(builder, start_regions, proposal, event_starts, world, player)
determine_prize_lock(key_layout, world, player)
while not validate_key_layout(key_layout, world, player):
itr += 1
@@ -2572,12 +2674,11 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
# make changes
if player not in world.key_logic.keys():
world.key_logic[player] = {}
builder.total_keys = builder.key_doors_num
analyze_dungeon(key_layout, world, player)
builder.key_door_proposal = proposal
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
return builder.key_door_proposal, key_doors_needed
return builder.key_door_proposal, key_doors_needed + len(custom_key_doors)
def find_bd_candidates(builder, start_regions, used, world, player):
@@ -2618,25 +2719,21 @@ def find_bd_door_candidates(region, checked, used, world, player):
room = world.get_room(d.roomIndex, player)
position, kind = room.doorList[d.doorListPos]
if d.type == DoorType.Interior:
valid = kind in okay_interiors
if valid and d.dest not in candidates: # interior doors are not separable yet
# interior doors are not separable yet
valid = kind in okay_interiors and d.dest not in used
if valid and d.dest not in candidates:
candidates.append(d.dest)
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2654,7 +2751,7 @@ def find_valid_bd_combination(builder, suggested, world, player):
bd_door_pool = builder.candidates.bomb_dash
bomb_doors_needed, dash_doors_needed = suggested
ttl_needed = bomb_doors_needed + dash_doors_needed
if player in world.custom_door_types:
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name]
custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name]
else:
@@ -2667,8 +2764,15 @@ def find_valid_bd_combination(builder, suggested, world, player):
bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors)
dash_doors_needed -= len(custom_dash_doors)
while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed:
bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed)
dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed)
test = random.choice([True, False])
if test:
bomb_doors_needed -= 1
if bomb_doors_needed < 0:
bomb_doors_needed = 0
else:
dash_doors_needed -= 1
if dash_doors_needed < 0:
dash_doors_needed = 0
bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed)
bomb_proposal.extend(custom_bomb_doors)
dash_pool = [x for x in bd_door_pool if x not in bomb_proposal]
@@ -2696,8 +2800,8 @@ def reassign_bd_doors(bd_map, world, player):
elif d.type is DoorType.Normal and not_in_proposal(d):
if not d.entranceFlag:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player)
do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player)
do_bombable_dashable(pair[0], DoorKind.Bombable, world, player)
do_bombable_dashable(pair[1], DoorKind.Dashable, world, player)
def do_bombable_dashable(proposal, kind, world, player):
@@ -2723,7 +2827,7 @@ def do_bombable_dashable(proposal, kind, world, player):
change_door_to_kind(d1, kind, world, player)
change_door_to_kind(d2, kind, world, player)
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player)
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player)
else:
d = obj
if d.type is DoorType.Interior:
@@ -2737,7 +2841,7 @@ def do_bombable_dashable(proposal, kind, world, player):
change_door_to_kind(d.dest, kind, world, player)
add_pair(d, d.dest, world, player)
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(d.name, spoiler_type, player)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player)
def find_current_bd_doors(builder, world):
@@ -2852,27 +2956,23 @@ def find_key_door_candidates(region, checked, used, world, player):
room = world.get_room(d.roomIndex, player)
position, kind = room.doorList[d.doorListPos]
if d.type == DoorType.Interior:
valid = kind in okay_interiors
if valid and d.dest not in candidates: # interior doors are not separable yet
valid = kind in okay_interiors and d.dest not in used
# interior doors are not separable yet
if valid and d.dest not in candidates:
candidates.append(d.dest)
elif d.type == DoorType.SpiralStairs:
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2944,8 +3044,8 @@ def reassign_key_doors(small_map, world, player):
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
change_door_to_small_key(d1, world, player)
change_door_to_small_key(d2, world, player)
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player)
logger.debug('Key Door: %s', d1.name+' <-> '+d2.name)
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player)
logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
else:
d = obj
if d.type is DoorType.Interior:
@@ -2961,8 +3061,8 @@ def reassign_key_doors(small_map, world, player):
if stateful_door(d.dest, dest_room.kind(d.dest)):
change_door_to_small_key(d.dest, world, player)
add_pair(d, d.dest, world, player)
world.spoiler.set_door_type(d.name, 'Key Door', player)
logger.debug('Key Door: %s', d.name)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player)
logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})')
def change_door_to_small_key(d, world, player):
@@ -3152,7 +3252,7 @@ def change_pair_type(door, new_type, world, player):
room_b.change(door.dest.doorListPos, new_type)
add_pair(door, door.dest, world, player)
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player)
world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player)
def remove_pair_type_if_present(door, world, player):
@@ -3245,6 +3345,14 @@ def find_accessible_entrances(world, player, builder):
return visited_entrances
def find_possible_entrances(world, player, builder):
entrances = [region.name for region in
(portal.door.entrance.parent_region for portal in world.dungeon_portals[player])
if region.dungeon.name == builder.name]
entrances.extend(drop_entrances[builder.name])
return entrances
def valid_inaccessible_region(r):
return r.type is not RegionType.Cave or (len(r.exits) > 0 and r.name not in ['Links House', 'Chris Houlihan Room'])
@@ -4216,7 +4324,7 @@ default_door_connections = [
('Eastern Map Valley SW', 'Eastern Dark Square NW'),
('Eastern Attic Start WS', 'Eastern False Switches ES'),
('Eastern Cannonball Hell WS', 'Eastern Single Eyegore ES'),
('Desert Compass NW', 'Desert Cannonball S'),
('Desert Compass NE', 'Desert Cannonball S'),
('Desert Beamos Hall NE', 'Desert Tiles 2 SE'),
('PoD Middle Cage N', 'PoD Pit Room S'),
('PoD Pit Room NW', 'PoD Arena Main SW'),
@@ -4503,7 +4611,7 @@ door_type_counts = {
'Swamp Palace': (6, 0, 0, 2, 0, 0, 0),
'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0),
'Misery Mire': (6, 3, 5, 2, 0, 0, 0),
'Skull Woods': (5, 0, 2, 2, 0, 1, 0),
'Skull Woods': (5, 0, 1, 2, 0, 1, 0),
'Ice Palace': (6, 1, 3, 0, 0, 0, 0),
'Tower of Hera': (1, 1, 0, 0, 0, 0, 0),
'Thieves Town': (3, 1, 2, 1, 1, 0, 0),