Revert "Revert "Cross Dungeon initial work""

This reverts commit fadc085b67.
This commit is contained in:
aerinon
2020-01-02 11:37:47 -07:00
parent 511cc62234
commit 88849e921c
9 changed files with 1425 additions and 657 deletions

View File

@@ -229,11 +229,15 @@ class World(object):
if keys:
for p in range(1, self.players + 1):
key_list = []
player_dungeons = [x for x in self.dungeons if x.player == p]
for dungeon in player_dungeons:
if dungeon.big_key is not None:
key_list += [dungeon.big_key.name]
if len(dungeon.small_keys) > 0:
key_list += [x.name for x in dungeon.small_keys]
from Items import ItemFactory
for item in ItemFactory(['Small Key (Escape)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + ['Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + ['Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + ['Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)', 'Big Key (Ganons Tower)'] + ['Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + ['Small Key (Ganons Tower)'] * 4,
p):
for item in ItemFactory(key_list, p):
soft_collect(item)
ret.sweep_for_events()
return ret
@@ -889,12 +893,30 @@ class Polarity:
def __getitem__(self, item):
return self.vector[item]
def __eq__(self, other):
for i in range(len(self.vector)):
if self.vector[i] != other.vector[i]:
return False
return True
def is_neutral(self):
for i in range(len(self.vector)):
if self.vector[i] != 0:
return False
return True
def complement(self):
result = Polarity()
for i in range(len(self.vector)):
result.vector[i] = pol_comp[pol_idx_2[i]](self.vector[i])
return result
def charge(self):
result = 0
for i in range(len(self.vector)):
result += abs(self.vector[i])
return result
pol_idx = {
Direction.North: (0, 'Pos'),
@@ -918,6 +940,11 @@ pol_add = {
'Add': lambda x, y: x + y,
'Mod': lambda x, y: (x + y) % 2
}
pol_comp = {
'Add': lambda x: -x,
'Mod': lambda x: 0 if x == 0 else 1
}
@unique
class CrystalBarrier(Enum):
@@ -961,6 +988,7 @@ class Door(object):
self.req_event = None # if a dungeon event is required for this door - swamp palace mostly
self.controller = None
self.dependents = []
self.dead = False
def getAddress(self):
if self.type == DoorType.Normal:
@@ -1035,6 +1063,10 @@ class Door(object):
self.crystal = CrystalBarrier.Either
return self
def kill(self):
self.dead = True
return self
def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name
@@ -1054,6 +1086,19 @@ class Sector(object):
self.regions = []
self.outstanding_doors = []
self.name = None
self.r_name_set = None
self.chest_locations = 0
self.key_only_locations = 0
self.c_switch = False
self.orange_barrier = False
self.blue_barrier = False
self.bk_required = False
self.bk_provided = False
def region_set(self):
if self.r_name_set is None:
self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions))
return self.r_name_set.keys()
def polarity(self):
pol = Polarity()
@@ -1076,6 +1121,19 @@ class Sector(object):
outflow = outflow + 1
return outflow
def adj_outflow(self):
outflow = 0
for door in self.outstanding_doors:
if not door.blocked and not door.dead:
outflow = outflow + 1
return outflow
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s' % next(iter(self.region_set()))
class Boss(object):
def __init__(self, name, enemizer_name, defeat_rule, player):
@@ -1335,14 +1393,9 @@ class Spoiler(object):
self.bosses[str(player)]["Ice Palace"] = self.world.get_dungeon("Ice Palace", player).boss.name
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
if self.world.mode != 'inverted':
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
else:
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
self.bosses[str(player)]["Ganons Tower Basement"] = [x for x in self.world.dungeons if x.player == player and 'bottom' in x.bosses.keys()][0].bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = [x for x in self.world.dungeons if x.player == player and 'middle' in x.bosses.keys()][0].bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = [x for x in self.world.dungeons if x.player == player and 'top' in x.bosses.keys()][0].bosses['top'].name
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"

View File

@@ -6,13 +6,16 @@ import operator as op
import time
from functools import reduce
from BaseClasses import RegionType, Door, DoorType, Direction, Sector, Polarity, CrystalBarrier
from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBarrier
from Regions import key_only_locations
from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions
from Dungeons import dungeon_regions, region_starts, split_region_starts, flexible_starts
from Dungeons import drop_entrances
from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys
from Items import ItemFactory
from RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout_ex
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
def link_doors(world, player):
@@ -53,15 +56,15 @@ def link_doors(world, player):
elif world.doorShuffle == 'experimental':
experiment(world, player)
mark_regions(world, player)
if world.doorShuffle != 'vanilla':
create_door_spoiler(world, player)
# todo: I think this function is not necessary
def mark_regions(world, player):
# traverse dungeons and make sure dungeon property is assigned
playerDungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
for dungeon in playerDungeons:
player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
for dungeon in player_dungeons:
queue = collections.deque(dungeon.regions)
while len(queue) > 0:
region = world.get_region(queue.popleft(), player)
@@ -101,34 +104,31 @@ def create_door_spoiler(world, player):
logger.debug('Door not found in queue: %s connected to %s', door_b.name, door_a.name)
else:
logger.warning('Door not connected: %s', door_a.name)
# for dp in world.paired_doors[player]:
# if dp.pair:
# logger.debug('Paired Doors: %s with %s (p%d)', dp.door_a, dp.door_b, player)
# else:
# logger.debug('Unpaired Doors: %s not paired with %s (p%d)', dp.door_a, dp.door_b, player)
def vanilla_key_logic(world, player):
sectors = []
builders = []
for dungeon in [dungeon for dungeon in world.dungeons if dungeon.player == player]:
sector = Sector()
sector.name = dungeon.name
sector.regions.extend(convert_regions(dungeon.regions, world, player))
sectors.append(sector)
builder = simple_dungeon_builder(sector.name, [sector], world, player)
builder.master_sector = sector
builders.append(builder)
overworld_prep(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player)
for sector in sectors:
start_regions = convert_regions(entrances_map[sector.name], world, player)
doors = convert_key_doors(default_small_key_doors[sector.name], world, player)
key_layout = build_key_layout(sector, start_regions, doors, world, player)
valid = validate_key_layout_ex(key_layout, world, player)
for builder in builders:
start_regions = convert_regions(entrances_map[builder.name], 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:
raise Exception('Vanilla key layout not valid %s' % sector.name)
raise Exception('Vanilla key layout not valid %s' % builder.name)
if player not in world.key_logic.keys():
world.key_logic[player] = {}
key_layout = analyze_dungeon(key_layout, world, player)
world.key_logic[player][sector.name] = key_layout.key_logic
world.key_logic[player][builder.name] = key_layout.key_logic
validate_vanilla_key_logic(world, player)
@@ -271,61 +271,78 @@ def pair_existing_key_doors(world, player, door_a, door_b):
def within_dungeon(world, player):
fix_big_key_doors_with_ugly_smalls(world, player)
overworld_prep(world, player)
dungeon_sectors = []
entrances_map, potentials, connections = determine_entrance_list(world, player)
connections_tuple = (entrances_map, potentials, connections)
dungeon_builders = {}
for key in dungeon_regions.keys():
sector_list = convert_to_sectors(dungeon_regions[key], world, player)
if key in split_region_starts.keys():
split_sectors = split_up_sectors(sector_list, split_region_starts[key])
for idx, sub_sector_list in enumerate(split_sectors):
entrance_list = list(split_region_starts[key][idx])
# shuffable entrances like pinball, left pit need to be added to entrance list
if key in flexible_starts.keys():
add_shuffled_entrances(sub_sector_list, flexible_starts[key], entrance_list)
filtered_list = [x for x in entrance_list if x in entrances_map[key]]
dungeon_sectors.append((key, sub_sector_list, filtered_list))
else:
dungeon_sectors.append((key, sector_list, list(entrances_map[key])))
enabled_entrances = {}
dungeon_layouts = []
sector_queue = collections.deque(dungeon_sectors)
last_key = None
while len(sector_queue) > 0:
key, sector_list, entrance_list = sector_queue.popleft()
split_dungeon = key in split_region_starts.keys()
origin_list = list(entrance_list)
find_enabled_origins(sector_list, enabled_entrances, origin_list, entrances_map, key)
origin_list_sans_drops = remove_drop_origins(origin_list)
if len(origin_list_sans_drops) <= 0:
if last_key == key:
raise Exception('Infinte loop detected %s' % key)
sector_queue.append((key, sector_list, entrance_list))
last_key = key
else:
ds = generate_dungeon(key, sector_list, origin_list_sans_drops, split_dungeon, world, player)
find_new_entrances(ds, connections, potentials, enabled_entrances, world, player)
ds.name = key
layout_starts = origin_list if len(entrance_list) <= 0 else entrance_list
dungeon_layouts.append((ds, layout_starts))
last_key = None
combine_layouts(dungeon_layouts, entrances_map)
world.dungeon_layouts[player] = {}
for sector, entrances in dungeon_layouts:
find_enabled_origins([sector], enabled_entrances, entrances, entrances_map, sector.name)
world.dungeon_layouts[player][sector.name] = (sector, entrances_map[sector.name])
dungeon_builders[key] = simple_dungeon_builder(key, sector_list, world, player)
dungeon_builders[key].entrance_list = list(entrances_map[key])
recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world)
check_required_paths(paths, world, player)
# shuffle_key_doors for dungeons
start = time.process_time()
for sector, entrances in world.dungeon_layouts[player].values():
shuffle_key_doors(sector, entrances, world, player)
for builder in world.dungeon_layouts[player].values():
shuffle_key_doors(builder, world, player)
logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start)
def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map):
for name, split_list in split_region_starts.items():
builder = dungeon_builders.pop(name)
recombinant_builders[name] = builder
split_builders = split_dungeon_builder(builder, split_list)
dungeon_builders.update(split_builders)
for sub_name, split_entrances in split_list.items():
sub_builder = dungeon_builders[name+' '+sub_name]
entrance_list = list(split_entrances)
if name in flexible_starts.keys():
add_shuffled_entrances(sub_builder.sectors, flexible_starts[name], entrance_list)
filtered_entrance_list = [x for x in entrance_list if x in entrances_map[name]]
sub_builder.entrance_list = filtered_entrance_list
def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player):
entrances_map, potentials, connections = connections_tuple
enabled_entrances = {}
sector_queue = collections.deque(dungeon_builders.values())
last_key = None
while len(sector_queue) > 0:
builder = sector_queue.popleft()
split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods')
name = builder.name
if split_dungeon:
name = ' '.join(builder.name.split(' ')[:-1])
origin_list = list(builder.entrance_list)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
origin_list_sans_drops = remove_drop_origins(origin_list)
if len(origin_list_sans_drops) <= 0:
if last_key == builder.name:
raise Exception('Infinte loop detected %s' % builder.name)
sector_queue.append(builder)
last_key = builder.name
else:
logging.getLogger('').info('Generating dungeon: %s', builder.name)
ds = generate_dungeon(name, builder.sectors, origin_list_sans_drops, split_dungeon, world, player)
find_new_entrances(ds, connections, potentials, enabled_entrances, world, player)
ds.name = name
builder.master_sector = ds
builder.layout_starts = origin_list if len(builder.entrance_list) <= 0 else builder.entrance_list
last_key = None
combine_layouts(recombinant_builders, dungeon_builders, entrances_map)
world.dungeon_layouts[player] = {}
for builder in dungeon_builders.values():
find_enabled_origins([builder.master_sector], enabled_entrances, builder.layout_starts, entrances_map, builder.name)
builder.path_entrances = entrances_map[builder.name]
world.dungeon_layouts[player] = dungeon_builders
def determine_entrance_list(world, player):
entrance_map = {}
potential_entrances = {}
@@ -347,6 +364,7 @@ def determine_entrance_list(world, player):
return entrance_map, potential_entrances, connections
# todo: kill drop exceptions
def drop_exception(name):
return name in ['Skull Pot Circle', 'Skull Back Drop']
@@ -365,6 +383,8 @@ def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key):
entrance_list.append(region.name)
origin_reg, origin_dungeon = enabled[region.name]
if origin_reg != region.name and origin_dungeon != region.dungeon:
if key not in entrance_map.keys():
key = ' '.join(key.split(' ')[:-1])
entrance_map[key].append(region.name)
if drop_exception(region.name): # only because they have unique regions
entrance_list.append(region.name)
@@ -593,34 +613,128 @@ def doors_fit_mandatory_pair(pair_list, a, b):
def cross_dungeon(world, player):
fix_big_key_doors_with_ugly_smalls(world, player)
overworld_prep(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player)
connections_tuple = (entrances_map, potentials, connections)
all_sectors = []
for key in dungeon_regions.keys():
all_sectors.extend(convert_to_sectors(dungeon_regions[key], world, player))
dungeon_split = split_up_sectors(all_sectors, default_dungeon_sets)
dungeon_sectors = []
for idx, sector_list in enumerate(dungeon_split):
name = dungeon_x_idx_to_name[idx]
if name in split_region_starts.keys():
split = split_up_sectors(sector_list, split_region_starts[name])
for sub_idx, sub_sector_list in enumerate(split):
dungeon_sectors.append((name, sub_sector_list, split_region_starts[name][sub_idx]))
dungeon_builders = create_dungeon_builders(all_sectors, world, player)
for builder in dungeon_builders.values():
builder.entrance_list = list(entrances_map[builder.name])
dungeon_obj = world.get_dungeon(builder.name, player)
for sector in builder.sectors:
for region in sector.regions:
region.dungeon = dungeon_obj
for loc in region.locations:
if loc.name in key_only_locations:
key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name]
loc.forced_item = loc.item = ItemFactory(key_name, player)
recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world)
check_required_paths(paths, world, player)
start = time.process_time()
total_keys = remaining = 29
total_candidates = 0
start_regions_map = {}
# Step 1: Find Small Key Door Candidates
for name, builder in dungeon_builders.items():
dungeon = world.get_dungeon(name, player)
if not builder.bk_required or builder.bk_provided:
dungeon.big_key = None
elif builder.bk_required and not builder.bk_provided:
dungeon.big_key = ItemFactory(dungeon_bigs[name], player)
start_regions = convert_regions(builder.path_entrances, world, player)
find_small_key_door_candidates(builder, start_regions, world, player)
builder.key_doors_num = max(0, len(builder.candidates) - builder.key_drop_cnt)
total_candidates += builder.key_doors_num
start_regions_map[name] = start_regions
# Step 2: Initial Key Number Assignment & Calculate Flexibility
for name, builder in dungeon_builders.items():
calculated = int(round(builder.key_doors_num*total_keys/total_candidates))
max_keys = builder.location_cnt - calc_used_dungeon_items(builder)
cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt)
limit = min(max_keys, cand_len)
suggested = min(calculated, limit)
combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt)
while combo_size > 500000 and suggested > 0:
suggested -= 1
combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt)
builder.key_doors_num = suggested + builder.key_drop_cnt
remaining -= suggested
builder.combo_size = combo_size
if suggested < limit:
builder.flex = limit - suggested
# Step 3: Initial valid combination find - reduce flex if needed
for name, builder in dungeon_builders.items():
suggested = builder.key_doors_num - builder.key_drop_cnt
find_valid_combination(builder, start_regions_map[name], world, player)
actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt
if actual_chest_keys < suggested:
remaining += suggested - actual_chest_keys
builder.flex = 0
# Step 4: Try to assign remaining keys
builder_order = [x for x in dungeon_builders.values() if x.flex > 0]
builder_order.sort(key=lambda b: b.combo_size)
queue = collections.deque(builder_order)
logger = logging.getLogger('')
while len(queue) > 0 and remaining > 0:
builder = queue.popleft()
name = builder.name
logger.info('Cross Dungeon: Increasing key count by 1 for %s', name)
builder.key_doors_num += 1
result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False)
if result:
remaining -= 1
builder.flex -= 1
if builder.flex > 0:
builder.combo_size = ncr(len(builder.candidates), builder.key_doors_num)
queue.append(builder)
queue = collections.deque(sorted(queue, key=lambda b: b.combo_size))
else:
dungeon_sectors.append((name, sector_list, region_starts[name]))
# todo - adjust dungeon item pools -- ?
dungeon_layouts = []
# for key, sector_list, entrance_list in dungeon_sectors:
# ds = shuffle_dungeon_no_repeats_new(world, player, sector_list, entrance_list)
# ds.name = key
# dungeon_layouts.append((ds, entrance_list))
logger.info('Cross Dungeon: Increase failed for %s', name)
builder.flex = 0
logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining)
combine_layouts(dungeon_layouts)
# Last Step: Adjust Small Key Dungeon Pool
for name, builder in dungeon_builders.items():
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
dungeon = world.get_dungeon(name, player)
if actual_chest_keys == 0:
dungeon.small_keys = []
else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start)
# todo: pair down paired doors - excessive rom writes ATM
for layout in dungeon_layouts:
shuffle_key_doors(layout[0], layout[1], world, player)
# Re-assign dungeon bosses
gt = world.get_dungeon('Ganons Tower', player)
for name, builder in dungeon_builders.items():
reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player)
reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player)
reassign_boss('GT Moldorm', 'top', builder, gt, world, player)
def reassign_boss(boss_region, boss_key, builder, gt, world, player):
if boss_region in builder.master_sector.region_set():
new_dungeon = world.get_dungeon(builder.name, player)
if new_dungeon != gt:
gt_boss = gt.bosses.pop(boss_key)
new_dungeon.bosses[boss_key] = gt_boss
def experiment(world, player):
within_dungeon(world, player)
cross_dungeon(world, player)
def convert_to_sectors(region_names, world, player):
@@ -628,12 +742,12 @@ def convert_to_sectors(region_names, world, player):
sectors = []
while len(region_list) > 0:
region = region_list.pop()
sector = None
new_sector = True
region_chunk = [region]
exits = []
exits.extend(region.exits)
outstanding_doors = []
matching_sectors = []
while len(exits) > 0:
ext = exits.pop()
if ext.connected_region is not None:
@@ -645,182 +759,55 @@ def convert_to_sectors(region_names, world, player):
if connect_region not in region_chunk:
for existing in sectors:
if connect_region in existing.regions:
sector = existing
new_sector = False
if existing not in matching_sectors:
matching_sectors.append(existing)
else:
door = world.check_for_door(ext.name, player)
if door is not None and door.controller is None and door.dest is None:
outstanding_doors.append(door)
if new_sector:
sector = Sector()
if not new_sector:
for match in matching_sectors:
sector.regions.extend(match.regions)
sector.outstanding_doors.extend(match.outstanding_doors)
sectors.remove(match)
sector.regions.extend(region_chunk)
sector.outstanding_doors.extend(outstanding_doors)
if new_sector:
sectors.append(sector)
return sectors
# those with split region starts like Desert/Skull combine for key layouts
def combine_layouts(dungeon_layouts, entrances_map):
combined = {}
queue = collections.deque(dungeon_layouts)
def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
for recombine in recombinant_builders.values():
queue = collections.deque(dungeon_builders.values())
while len(queue) > 0:
sector, entrance_list = queue.pop()
if sector.name in split_region_starts:
dungeon_layouts.remove((sector, entrance_list))
# desert_entrances.extend(entrance_list)
if sector.name not in combined:
combined[sector.name] = sector
builder = queue.pop()
if builder.name.startswith(recombine.name):
del dungeon_builders[builder.name]
if recombine.master_sector is None:
recombine.master_sector = builder.master_sector
recombine.master_sector.name = recombine.name
else:
combined[sector.name].regions.extend(sector.regions)
for key in combined.keys():
dungeon_layouts.append((combined[key], list(entrances_map[key])))
def split_up_sectors(sector_list, entrance_sets):
new_sector_grid = []
leftover_sectors = []
leftover_sectors.extend(sector_list)
for entrance_set in entrance_sets:
new_sector_list = []
for sector in sector_list:
s_regions = list(map(lambda r: r.name, sector.regions))
for entrance in entrance_set:
if entrance in s_regions:
new_sector_list.append(sector)
leftover_sectors.remove(sector)
break
new_sector_grid.append(new_sector_list)
shuffle_sectors(new_sector_grid, leftover_sectors)
return new_sector_grid
def sum_vector(sector_list, func):
result = [0, 0, 0]
for sector in sector_list:
vector = func(sector)
for i in range(len(result)):
result[i] = result[i] + vector[i]
return result
def is_polarity_neutral(sector_list):
pol = Polarity()
for sector in sector_list:
pol += sector.polarity()
return pol.is_neutral()
search_iterations = 0
def is_proposal_valid(proposal, buckets, candidates):
logger = logging.getLogger('')
global search_iterations
search_iterations = search_iterations + 1
if search_iterations % 100 == 0:
logger.debug('Iteration %s', search_iterations)
# check that proposal is complete
for i in range(len(proposal)):
if proposal[i] is -1:
return False # indicates an incomplete proposal
test_bucket = []
for bucket_idx in range(len(buckets)):
test_bucket.append(list(buckets[bucket_idx]))
for i in range(len(proposal)):
test_bucket[proposal[i]].append(candidates[i])
for test in test_bucket:
valid = is_polarity_neutral(test)
if not valid:
return False
for sector in test:
other_sectors = list(test)
other_sectors.remove(sector)
sector_mag = sector.magnitude()
other_mag = sum_vector(other_sectors, lambda x: x.magnitude())
for i in range(len(sector_mag)):
if sector_mag[i] > 0 and other_mag[i] == 0:
return False
return True
def shuffle_sectors(buckets, candidates):
# for a faster search - instead of random - put the most likely culprits to cause problems at the end, least likely at the front
# unless we start checking for failures earlier in the algo
random.shuffle(candidates)
proposal = [-1]*len(candidates)
solution = find_proposal_monte_carlo(proposal, buckets, candidates)
if solution is None:
raise Exception('Unable to find a proposal')
for i in range(len(solution)):
buckets[solution[i]].append(candidates[i])
# monte carlo proposal generation
def find_proposal_monte_carlo(proposal, buckets, candidates):
n = len(candidates)
k = len(buckets)
hashes = set()
collisions = 0
while collisions < 10000:
hash = ''
for i in range(n):
proposal[i] = random.randrange(k)
hash = hash + str(proposal[i])
if hash not in hashes:
collisions = 0
if is_proposal_valid(proposal, buckets, candidates):
return proposal
hashes.add(hash)
else:
collisions = collisions + 1
raise Exception('Too many collisions in a row, solutions space is sparse')
# this is a DFS search - fairly slow
def find_proposal(proposal, buckets, candidates):
size = len(candidates)
combination_grid = []
for i in range(size):
combination_grid.append(list(range(len(buckets))))
# randomize which bucket
for possible_buckets in combination_grid:
random.shuffle(possible_buckets)
idx = 0
while idx != size or not is_proposal_valid(proposal, buckets, candidates):
if idx == size:
idx = idx - 1
while len(combination_grid[idx]) == 0:
if idx == -1: # this is the failure case - we shouldn't hit it
return None
combination_grid[idx] = list(range(len(buckets)))
idx = idx - 1
proposal[idx] = combination_grid[idx].pop()
# can we detect a bad choice at this stage
idx = idx + 1
return proposal
recombine.master_sector.regions.extend(builder.master_sector.regions)
recombine.layout_starts = list(entrances_map[recombine.name])
dungeon_builders[recombine.name] = recombine
def valid_region_to_explore(region, world, player):
return region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player]
def shuffle_key_doors(dungeon_sector, entrances, world, player):
logger = logging.getLogger('')
logger.info('Shuffling Key doors for %s', dungeon_sector.name)
start_regions = convert_regions(entrances, world, player)
def shuffle_key_doors(builder, world, player):
start_regions = convert_regions(builder.path_entrances, world, player)
# count number of key doors - this could be a table?
num_key_doors = 0
current_doors = []
skips = []
for region in dungeon_sector.regions:
for region in builder.master_sector.regions:
for ext in region.exits:
d = world.check_for_door(ext.name, player)
if d is not None and d.smallKey:
current_doors.append(d)
if d not in skips:
if d.type == DoorType.Interior:
skips.append(d.dest)
@@ -833,7 +820,22 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player):
skips.append(world.get_door(dp.door_a, player))
break
num_key_doors += 1
builder.key_doors_num = num_key_doors
find_small_key_door_candidates(builder, start_regions, world, player)
find_valid_combination(builder, start_regions, world, player)
def find_current_key_doors(builder, world, player):
current_doors = []
for region in builder.master_sector.regions:
for ext in region.exits:
d = world.check_for_door(ext.name, player)
if d is not None and d.smallKey:
current_doors.append(d)
return current_doors
def find_small_key_door_candidates(builder, start_regions, world, player):
# traverse dungeon and find candidates
candidates = []
checked_doors = set()
@@ -847,42 +849,73 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player):
if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates:
flat_candidates.append(candidate)
# find valid combination of candidates
paired_candidates = build_pair_list(flat_candidates)
if len(paired_candidates) < num_key_doors:
num_key_doors = len(paired_candidates) # reduce number of key doors
logger.info('Lowering key door count because not enough candidates: %s', dungeon_sector.name)
combinations = ncr(len(paired_candidates), num_key_doors)
builder.candidates = paired_candidates
def calc_used_dungeon_items(builder):
base = 4
if builder.bk_required and not builder.bk_provided:
base += 1
if builder.name == 'Hyrule Castle':
base -= 1 # Missing compass/map
if builder.name == 'Agahnims Tower':
base -= 2 # Missing both compass/map
# gt can lose map once compasses work
return base
def find_valid_combination(builder, start_regions, world, player, drop_keys=True):
logger = logging.getLogger('')
logger.info('Shuffling Key doors for %s', builder.name)
# find valid combination of candidates
if len(builder.candidates) < builder.key_doors_num:
if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False
builder.key_doors_num = len(builder.candidates) # reduce number of key doors
logger.info('Lowering key door count because not enough candidates: %s', builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num)
itr = 0
start = time.process_time()
sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list)
proposal = kth_combination(sample_list[itr], paired_candidates, num_key_doors)
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
key_layout = build_key_layout(dungeon_sector, start_regions, proposal, world, player)
while not validate_key_layout_ex(key_layout, world, player):
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
while not validate_key_layout(key_layout, world, player):
itr += 1
if itr >= combinations:
logger.info('Lowering key door count because no valid layouts: %s', dungeon_sector.name)
num_key_doors -= 1
if num_key_doors < 0:
raise Exception('Bad dungeon %s - 0 key doors not valid' % dungeon_sector.name)
combinations = ncr(len(paired_candidates), num_key_doors)
stop_early = False
if itr % 1000 == 0:
mark = time.process_time()-start
if (mark > 10 and itr*100/combinations > 50) or (mark > 20 and itr*100/combinations > 25) or mark > 30:
stop_early = True
if itr >= combinations or stop_early:
if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False
logger.info('Lowering key door count because no valid layouts: %s', builder.name)
builder.key_doors_num -= 1
if builder.key_doors_num < 0:
raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num)
sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list)
itr = 0
proposal = kth_combination(sample_list[itr], paired_candidates, num_key_doors)
key_layout.reset(proposal)
start = time.process_time() # reset time since itr reset
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
key_layout.reset(proposal, builder, world, player)
if (itr+1) % 1000 == 0:
mark = time.process_time()-start
logger.info('%s time elapsed. %s iterations/s', mark, itr/mark)
# make changes
if player not in world.key_logic.keys():
world.key_logic[player] = {}
key_layout_new = analyze_dungeon(key_layout, world, player)
reassign_key_doors(current_doors, proposal, world, player)
log_key_logic(dungeon_sector.name, key_layout_new.key_logic)
world.key_logic[player][dungeon_sector.name] = key_layout_new.key_logic
analyze_dungeon(key_layout, world, player)
reassign_key_doors(builder, proposal, world, player)
log_key_logic(builder.name, key_layout.key_logic)
world.key_logic[player][builder.name] = key_layout.key_logic
return True
def log_key_logic(d_name, key_logic):
@@ -910,7 +943,7 @@ def build_pair_list(flat_list):
queue = collections.deque(flat_list)
while len(queue) > 0:
d = queue.pop()
if d.dest in queue:
if d.dest in queue and d.type != DoorType.SpiralStairs:
paired_list.append((d, d.dest))
queue.remove(d.dest)
else:
@@ -989,10 +1022,10 @@ def ncr(n, r):
return numerator / denominator
def reassign_key_doors(current_doors, proposal, world, player):
def reassign_key_doors(builder, proposal, world, player):
logger = logging.getLogger('')
flat_proposal = flatten_pair_list(proposal)
queue = collections.deque(current_doors)
queue = collections.deque(find_current_key_doors(builder, world, player))
while len(queue) > 0:
d = queue.pop()
if d.type is DoorType.SpiralStairs and d not in proposal:
@@ -1085,7 +1118,7 @@ def determine_required_paths(world):
paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock')
# noinspection PyTypeChecker
paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
if world.doorShuffle in ['basic', 'experimental']:
if world.doorShuffle in ['basic', 'experimental']: # todo: crossed?
paths['Thieves Town'].append('Thieves Attic Window')
return paths
@@ -1167,19 +1200,19 @@ def create_door(world, player, entName, region_name):
def check_required_paths(paths, world, player):
for dungeon_name in paths.keys():
sector, entrances = world.dungeon_layouts[player][dungeon_name]
builder = world.dungeon_layouts[player][dungeon_name]
if len(paths[dungeon_name]) > 0:
states_to_explore = defaultdict(list)
for path in paths[dungeon_name]:
if type(path) is tuple:
states_to_explore[tuple([path[0]])].append(path[1])
else:
states_to_explore[tuple(entrances)].append(path)
states_to_explore[tuple(builder.path_entrances)].append(path)
cached_initial_state = None
for start_regs, dest_regs in states_to_explore.items():
check_paths = convert_regions(dest_regs, world, player)
start_regions = convert_regions(start_regs, world, player)
initial = start_regs == tuple(entrances)
initial = start_regs == tuple(builder.path_entrances)
if not initial or cached_initial_state is None:
init = determine_init_crystal(initial, cached_initial_state, start_regions)
state = ExplorationState(init, dungeon_name)

View File

@@ -145,7 +145,7 @@ def create_doors(world, player):
create_door(player, 'Eastern Compass Room EN', Intr).dir(Ea, 0xa8, Top, High).pos(3),
create_door(player, 'Eastern Hint Tile WN', Intr).dir(We, 0xa8, Top, High).pos(3),
create_door(player, 'Eastern Hint Tile EN', Nrml).dir(Ea, 0xa8, Top, Low).pos(4),
create_door(player, 'Eastern Hint Tile Blocked Path SE', Nrml).dir(So, 0xa8, Right, High).small_key().pos(2),
create_door(player, 'Eastern Hint Tile Blocked Path SE', Nrml).dir(So, 0xa8, Right, High).small_key().pos(2).kill(),
create_door(player, 'Eastern Hint Tile Push Block', Lgcl),
create_door(player, 'Eastern Courtyard WN', Nrml).dir(We, 0xa9, Top, Low).pos(3),
create_door(player, 'Eastern Courtyard EN', Nrml).dir(Ea, 0xa9, Top, Low).pos(4),
@@ -226,7 +226,7 @@ def create_doors(world, player):
create_door(player, 'Desert Four Statues ES', Intr).dir(Ea, 0x53, Bot, High).pos(1),
create_door(player, 'Desert Beamos Hall WS', Intr).dir(We, 0x53, Bot, High).pos(1),
create_door(player, 'Desert Beamos Hall NE', Nrml).dir(No, 0x53, Right, High).small_key().pos(2),
create_door(player, 'Desert Tiles 2 SE', Nrml).dir(So, 0x43, Right, High).small_key().pos(2),
create_door(player, 'Desert Tiles 2 SE', Nrml).dir(So, 0x43, Right, High).small_key().pos(2).kill(),
create_door(player, 'Desert Tiles 2 NE', Intr).dir(No, 0x43, Right, High).small_key().pos(1),
create_door(player, 'Desert Wall Slide SE', Intr).dir(So, 0x43, Right, High).small_key().pos(1),
create_door(player, 'Desert Wall Slide NW', Nrml).dir(No, 0x43, Left, High).big_key().pos(0).no_entrance(),
@@ -263,7 +263,7 @@ def create_doors(world, player):
create_door(player, 'Hera 5F Pothole Chain', Hole),
create_door(player, 'Hera 5F Normal Holes', Hole),
create_door(player, 'Hera Fairies\' Warp', Warp),
create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0),
create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0).kill(),
create_door(player, 'Hera Boss Outer Hole', Hole),
create_door(player, 'Hera Boss Inner Hole', Hole),
@@ -322,12 +322,12 @@ def create_doors(world, player):
create_door(player, 'PoD Pit Room Freefall', Hole),
create_door(player, 'PoD Pit Room Bomb Hole', Hole),
create_door(player, 'PoD Big Key Landing Hole', Hole),
create_door(player, 'PoD Big Key Landing Down Stairs', Sprl).dir(Dn, 0x3a, 0, HTH).ss(A, 0x11, 0x00),
create_door(player, 'PoD Big Key Landing Down Stairs', Sprl).dir(Dn, 0x3a, 0, HTH).ss(A, 0x11, 0x00).kill(),
create_door(player, 'PoD Basement Ledge Up Stairs', Sprl).dir(Up, 0x0a, 0, HTH).ss(A, 0x1a, 0xec).small_key().pos(0),
create_door(player, 'PoD Basement Ledge Drop Down', Lgcl),
create_door(player, 'PoD Stalfos Basement Warp', Warp),
create_door(player, 'PoD Arena Main SW', Nrml).dir(So, 0x2a, Left, High).pos(4),
create_door(player, 'PoD Arena Bridge SE', Nrml).dir(So, 0x2a, Right, High).pos(5),
create_door(player, 'PoD Arena Bridge SE', Nrml).dir(So, 0x2a, Right, High).pos(5).kill(),
create_door(player, 'PoD Arena Main NW', Nrml).dir(No, 0x2a, Left, High).small_key().pos(1),
create_door(player, 'PoD Arena Main NE', Nrml).dir(No, 0x2a, Right, High).no_exit().trap(0x4).pos(0),
create_door(player, 'PoD Arena Main Crystal Path', Lgcl),
@@ -366,7 +366,7 @@ def create_doors(world, player):
create_door(player, 'PoD Dark Basement W Up Stairs', Sprl).dir(Up, 0x6a, 0, HTH).ss(S, 0x1b, 0x3c, True),
create_door(player, 'PoD Dark Basement E Up Stairs', Sprl).dir(Up, 0x6a, 1, HTH).ss(S, 0x1b, 0x9c, True),
create_door(player, 'PoD Dark Alley NE', Nrml).dir(No, 0x6a, Right, High).big_key().pos(0),
create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1),
create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).kill(),
create_door(player, 'PoD Mimics 2 NW', Intr).dir(No, 0x1b, Left, High).pos(0),
create_door(player, 'PoD Bow Statue SW', Intr).dir(So, 0x1b, Left, High).pos(0),
create_door(player, 'PoD Bow Statue Down Ladder', Lddr).no_entrance(),
@@ -444,7 +444,7 @@ def create_doors(world, player):
create_door(player, 'Swamp Barrier EN', Nrml).dir(Ea, 0x34, Top, High).pos(0),
create_door(player, 'Swamp Barrier - Orange', Lgcl),
create_door(player, 'Swamp Barrier Ledge Hook Path', Lgcl),
create_door(player, 'Swamp Attic Down Stairs', Sprl).dir(Dn, 0x54, 0, HTH).ss(Z, 0x12, 0x80),
create_door(player, 'Swamp Attic Down Stairs', Sprl).dir(Dn, 0x54, 0, HTH).ss(Z, 0x12, 0x80).kill(),
create_door(player, 'Swamp Attic Left Pit', Hole),
create_door(player, 'Swamp Attic Right Pit', Hole),
create_door(player, 'Swamp Push Statue S', Nrml).dir(So, 0x26, Mid, High).small_key().pos(0),
@@ -460,7 +460,7 @@ def create_doors(world, player):
create_door(player, 'Swamp Drain Left Up Stairs', Sprl).dir(Up, 0x76, 0, HTH).ss(S, 0x1b, 0x2c, True, True),
create_door(player, 'Swamp Drain WN', Intr).dir(We, 0x76, Top, Low).pos(0),
create_door(player, 'Swamp Drain Right Switch', Lgcl),
create_door(player, 'Swamp Drain Right Up Stairs', Sprl).dir(Up, 0x76, 1, HTH).ss(S, 0x1b, 0x9c, True, True),
create_door(player, 'Swamp Drain Right Up Stairs', Sprl).dir(Up, 0x76, 1, HTH).ss(S, 0x1b, 0x9c, True, True).kill(),
create_door(player, 'Swamp Flooded Room Up Stairs', Sprl).dir(Up, 0x76, 2, HTH).ss(X, 0x1a, 0xac, True, True),
create_door(player, 'Swamp Flooded Room WS', Intr).dir(We, 0x76, Bot, Low).pos(1),
create_door(player, 'Swamp Flooded Spot Ladder', Lgcl),
@@ -649,7 +649,7 @@ def create_doors(world, player):
create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0),
create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True),
create_door(player, 'Ice Spike Room Up Stairs', Sprl).dir(Up, 0x5f, 4, HTH).ss(Z, 0x1a, 0xa4, True, True),
create_door(player, 'Ice Hammer Block Down Stairs', Sprl).dir(Dn, 0x3f, 0, HTH).ss(Z, 0x11, 0xb8, True, True),
create_door(player, 'Ice Hammer Block Down Stairs', Sprl).dir(Dn, 0x3f, 0, HTH).ss(Z, 0x11, 0xb8, True, True).kill(),
create_door(player, 'Ice Hammer Block ES', Intr).dir(Ea, 0x3f, Bot, High).pos(0),
create_door(player, 'Ice Tongue Pull WS', Intr).dir(We, 0x3f, Bot, High).pos(0),
create_door(player, 'Ice Tongue Pull Up Ladder', Lddr),
@@ -661,7 +661,7 @@ def create_doors(world, player):
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1),
create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(1),
create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0),
create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0),
create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0).kill(),
create_door(player, 'Ice Hookshot Ledge Path', Lgcl),
create_door(player, 'Ice Hookshot Balcony Path', Lgcl),
create_door(player, 'Ice Hookshot Balcony SW', Intr).dir(So, 0x7f, Left, High).pos(1),
@@ -787,7 +787,7 @@ def create_doors(world, player):
create_door(player, 'Mire BK Chest Ledge WS', Intr).dir(We, 0xd1, Bot, High).pos(0),
create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0),
create_door(player, 'Mire Warping Pool Warp', Warp),
create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True),
create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True).kill(),
create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1),
create_door(player, 'Mire Torches Bottom NW', Intr).dir(No, 0x97, Left, High).pos(1),
create_door(player, 'Mire Torches Bottom WS', Intr).dir(We, 0x97, Bot, High).pos(0),
@@ -901,10 +901,10 @@ def create_doors(world, player):
create_door(player, 'GT Hope Room WN', Intr).dir(We, 0x8c, Top, High).small_key().pos(2),
create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().pos(1),
create_door(player, 'GT Big Chest NW', Intr).dir(So, 0x8c, Left, High).pos(1),
create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True),
create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True).kill(),
create_door(player, 'GT Blocked Stairs Block Path', Lgcl),
create_door(player, 'GT Big Chest SW', Nrml).dir(So, 0x8c, Left, High).pos(4),
create_door(player, 'GT Bob\'s Room SE', Nrml).dir(So, 0x8c, Right, High).pos(5),
create_door(player, 'GT Bob\'s Room SE', Nrml).dir(So, 0x8c, Right, High).pos(5).kill(),
create_door(player, 'GT Bob\'s Room Hole', Hole),
create_door(player, 'GT Tile Room WN', Nrml).dir(We, 0x8d, Top, High).pos(2),
create_door(player, 'GT Tile Room EN', Intr).dir(Ea, 0x8d, Top, High).small_key().pos(1),
@@ -913,7 +913,7 @@ def create_doors(world, player):
create_door(player, 'GT Speed Torch WS', Intr).dir(We, 0x8d, Bot, High).pos(4),
create_door(player, 'GT Pots n Blocks ES', Intr).dir(Ea, 0x8d, Bot, High).pos(4),
create_door(player, 'GT Speed Torch SE', Nrml).dir(So, 0x8d, Right, High).trap(0x4).pos(0),
create_door(player, 'GT Crystal Conveyor NE', Nrml).dir(No, 0x9d, Right, High).pos(0),
create_door(player, 'GT Crystal Conveyor NE', Nrml).dir(No, 0x9d, Right, High).pos(0).kill(),
create_door(player, 'GT Crystal Conveyor WN', Intr).dir(We, 0x9d, Top, High).pos(2),
create_door(player, 'GT Compass Room EN', Intr).dir(Ea, 0x9d, Top, High).pos(2),
create_door(player, 'GT Compass Room Warp', Warp),
@@ -937,7 +937,7 @@ def create_doors(world, player):
create_door(player, 'GT Hookshot ES', Intr).dir(Ea, 0x8b, Bot, High).small_key().pos(1),
create_door(player, 'GT Map Room WS', Intr).dir(We, 0x8b, Bot, High).small_key().pos(1),
create_door(player, 'GT Hookshot SW', Nrml).dir(So, 0x8b, Left, High).pos(3),
create_door(player, 'GT Double Switch NW', Nrml).dir(No, 0x9b, Left, High).pos(1),
create_door(player, 'GT Double Switch NW', Nrml).dir(No, 0x9b, Left, High).pos(1).kill(),
create_door(player, 'GT Double Switch Orange Barrier', Lgcl),
create_door(player, 'GT Double Switch Orange Barrier 2', Lgcl),
create_door(player, 'GT Double Switch Blue Path', Lgcl),
@@ -972,7 +972,7 @@ def create_doors(world, player):
create_door(player, 'GT Warp Maze (Rails) WS', Nrml).dir(We, 0x7d, Bot, High).pos(1),
create_door(player, 'GT Trap Room SE', Nrml).dir(So, 0x7d, Right, High).trap(0x4).pos(0),
create_door(player, 'GT Conveyor Star Pits EN', Nrml).dir(Ea, 0x7b, Top, High).small_key().pos(1),
create_door(player, 'GT Hidden Star ES', Nrml).dir(Ea, 0x7b, Bot, High).pos(2),
create_door(player, 'GT Hidden Star ES', Nrml).dir(Ea, 0x7b, Bot, High).pos(2).kill(),
create_door(player, 'GT Hidden Star Warp', Warp),
create_door(player, 'GT DMs Room SW', Nrml).dir(So, 0x7b, Left, High).trap(0x4).pos(0),
create_door(player, 'GT Falling Bridge WN', Nrml).dir(We, 0x7c, Top, High).small_key().pos(2),
@@ -1009,7 +1009,7 @@ def create_doors(world, player):
create_door(player, 'GT Gauntlet 4 SW', Intr).dir(So, 0x6d, Left, High).pos(1),
create_door(player, 'GT Gauntlet 5 NW', Intr).dir(No, 0x6d, Left, High).pos(1),
create_door(player, 'GT Gauntlet 5 WS', Nrml).dir(We, 0x6d, Bot, High).pos(2),
create_door(player, 'GT Beam Dash ES', Nrml).dir(Ea, 0x6c, Bot, High).pos(2),
create_door(player, 'GT Beam Dash ES', Nrml).dir(Ea, 0x6c, Bot, High).pos(2).kill(),
create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1),
@@ -1036,14 +1036,14 @@ def create_doors(world, player):
create_door(player, 'GT Bomb Conveyor SW', Intr).dir(So, 0x3d, Left, High).pos(3),
create_door(player, 'GT Crystal Circles NW', Intr).dir(No, 0x3d, Left, High).pos(3),
create_door(player, 'GT Crystal Circles SW', Nrml).dir(So, 0x3d, Left, High).small_key().pos(2),
create_door(player, 'GT Left Moldorm Ledge NW', Nrml).dir(No, 0x4d, Left, High).small_key().pos(0),
create_door(player, 'GT Left Moldorm Ledge NW', Nrml).dir(No, 0x4d, Left, High).small_key().pos(0).kill(),
create_door(player, 'GT Left Moldorm Ledge Drop Down', Lgcl),
create_door(player, 'GT Right Moldorm Ledge Drop Down', Lgcl),
create_door(player, 'GT Moldorm Gap', Lgcl),
create_door(player, 'GT Moldorm Hole', Hole),
create_door(player, 'GT Validation Block Path', Lgcl),
create_door(player, 'GT Validation WS', Nrml).dir(We, 0x4d, Bot, High).pos(1),
create_door(player, 'GT Right Moldorm Ledge Down Stairs', Sprl).dir(Dn, 0x4d, 0, HTH).ss(S, 0x12, 0x80),
create_door(player, 'GT Right Moldorm Ledge Down Stairs', Sprl).dir(Dn, 0x4d, 0, HTH).ss(S, 0x12, 0x80).kill(),
create_door(player, 'GT Moldorm Pit Up Stairs', Sprl).dir(Up, 0xa6, 0, HTH).ss(S, 0x1b, 0x6c),
create_door(player, 'GT Frozen Over ES', Nrml).dir(Ea, 0x4c, Bot, High).no_exit().trap(0x4).pos(0),
create_door(player, 'GT Frozen Over Up Stairs', Sprl).dir(Up, 0x4c, 0, HTH).ss(S, 0x1a, 0x6c, True),

View File

@@ -3,9 +3,13 @@ import collections
from collections import defaultdict
from enum import Enum, unique
import logging
from functools import reduce
import operator as op
from typing import List
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, flooded_keys
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, flooded_keys
from Regions import key_only_locations, dungeon_events, flooded_keys_reverse
from Dungeons import dungeon_regions
@unique
@@ -33,11 +37,13 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
doors_to_connect = set()
all_regions = set()
bk_needed = False
bk_special = False
for sector in available_sectors:
for door in sector.outstanding_doors:
doors_to_connect.add(door)
all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
bk_special = bk_special or check_for_special(sector)
proposed_map = {}
choices_master = [[]]
depth = 0
@@ -50,9 +56,10 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
# what are my choices?
itr += 1
if itr > 5000:
raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0])
raise Exception('Generation taking too long. Ref %s' % name)
if depth not in dungeon_cache.keys():
dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, world, player)
dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map,
doors_to_connect, bk_needed, bk_special, world, player)
dungeon_cache[depth] = dungeon, hangers, hooks
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed)
else:
@@ -83,7 +90,7 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
dungeon_cache.pop(depth, None)
depth -= 1
if depth < 0:
raise Exception('Invalid dungeon. Ref %s' % entrance_region_names[0])
raise Exception('Invalid dungeon. Ref %s' % name)
a, b = choices_master[depth][-1]
logger.debug(' ' * depth + "%d: Rescinding %s, %s", depth, a.name, b.name)
proposed_map.pop(a, None)
@@ -96,6 +103,8 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
master_sector = available_sectors.pop()
for sub_sector in available_sectors:
master_sector.regions.extend(sub_sector.regions)
master_sector.outstanding_doors.clear()
master_sector.r_name_set = None
return master_sector
@@ -109,38 +118,46 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
return False
def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player):
def check_for_special(sector):
return 'Hyrule Dungeon Cellblock' in sector.region_set()
def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, bk_special, world, player):
# step 1 create dungeon: Dict<DoorName|Origin, GraphPiece>
dungeon = {}
original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(dungeon=name), proposed_map, valid_doors, bk_needed, world, player)
start = ExplorationState(dungeon=name)
start.big_key_special = bk_special
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map,
valid_doors, bk_needed, world, player)
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
doors_to_connect = set()
hanger_set = set()
o_state_cache = {}
for sector in available_sectors:
for door in sector.outstanding_doors:
doors_to_connect.add(door)
if not door.stonewall and door not in proposed_map.keys():
hanger_set.add(door)
parent = parent_region(door, world, player).parent_region
o_state = extend_reachable_state_improved([parent], ExplorationState(dungeon=name), proposed_map, valid_doors, False, world, player)
init_state = ExplorationState(dungeon=name)
init_state.big_key_special = start.big_key_special
o_state = extend_reachable_state_improved([parent], init_state, proposed_map,
valid_doors, False, world, player)
o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map)
dungeon[door.name] = piece
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player)
# catalog hooks: Dict<Hook, Set<Door, Crystal, Door>>
# and hangers: Dict<Hang, Set<Door>>
avail_hooks = defaultdict(set)
hangers = defaultdict(set)
# catalog hooks: Dict<Hook, List<Door, Crystal, Door>>
# and hangers: Dict<Hang, List<Door>>
avail_hooks = defaultdict(list)
hangers = defaultdict(list)
for key, piece in dungeon.items():
door_hang = piece.hanger_info
if door_hang is not None:
hanger = hanger_from_door(door_hang)
hangers[hanger].add(door_hang)
hangers[hanger].append(door_hang)
for door, crystal in piece.hooks.items():
hook = hook_from_door(door)
avail_hooks[hook].add((door, crystal, door_hang))
avail_hooks[hook].append((door, crystal, door_hang))
# thin out invalid hanger
winnow_hangers(hangers, avail_hooks)
@@ -183,28 +200,34 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
parent = parent_region(door, world, player).parent_region
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon)
blue_start.big_key_special = o_state.big_key_special
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player)
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map)
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
# choose a hanger
all_hooks = set()
all_hooks = {}
origin = dungeon['Origin']
for key in avail_hooks.keys():
for hstuff in avail_hooks[key]:
all_hooks.add(hstuff[0])
all_hooks[hstuff[0]] = None
candidate_hangers = []
for key in hangers.keys():
candidate_hangers.extend(hangers[key])
candidate_hangers.sort(key=lambda x: x.name) # sorting to create predictable seeds
random.shuffle(candidate_hangers) # randomize if equal preference
stage_2_hangers = []
if len(prev_choices) > 0:
prev_hanger = prev_choices[0][0]
if prev_hanger in candidate_hangers:
stage_2_hangers.append(prev_hanger)
candidate_hangers.remove(prev_hanger)
hookable_hangers = collections.deque()
queue = collections.deque(candidate_hangers)
while len(queue) > 0:
c_hang = queue.pop()
if c_hang in all_hooks:
if c_hang in all_hooks.keys():
hookable_hangers.append(c_hang)
else:
stage_2_hangers.append(c_hang) # prefer hangers that are not hooks
@@ -257,7 +280,8 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
return False
if len(true_origin_hooks) == 0 and bk_needed and possible_bks == 0 and len(proposed_map.keys()) == len(doors_to_connect):
if len(true_origin_hooks) == 0 and bk_needed and possible_bks == 0 and len(proposed_map.keys()) == len(
doors_to_connect):
return False
for key in hangers.keys():
if len(hooks[key]) > 0 and len(hangers[key]) == 0:
@@ -353,7 +377,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
all_unattached[d] = CrystalBarrier.Null
elif all_unattached[d] == CrystalBarrier.Blue and exp_d.crystal == CrystalBarrier.Orange:
# the swapping case
logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d], exp_d.crystal)
logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d],
exp_d.crystal)
elif all_unattached[d] == CrystalBarrier.Either:
all_unattached[d] = exp_d.crystal # pessimism, and if not this, leave it alone
else:
@@ -376,7 +401,9 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
def filter_for_potential_bk_locations(locations):
return [x for x in locations if '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']]
return [x for x in locations if
'- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events
and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']]
def parent_region(door, world, player):
@@ -576,9 +603,9 @@ class ExplorationState(object):
if region.type == RegionType.Dungeon:
for location in region.locations:
if key_checks and location not in self.found_locations:
if location.name in key_only_locations:
if location.name in key_only_locations and 'Small Key' in location.item.name:
self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name:
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
self.ttl_locations += 1
if location not in self.found_locations:
self.found_locations.append(location)
@@ -587,7 +614,8 @@ class ExplorationState(object):
if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location):
self.perform_event(location.name, key_region)
if location.name in flooded_keys_reverse.keys() and self.location_found(flooded_keys_reverse[location.name]):
if location.name in flooded_keys_reverse.keys() and self.location_found(
flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region)
if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened:
self.big_key_opened = True
@@ -625,7 +653,8 @@ class ExplorationState(object):
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
@@ -639,7 +668,8 @@ class ExplorationState(object):
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
@@ -656,7 +686,8 @@ class ExplorationState(object):
other = self.find_door_in_list(door, self.unattached_doors)
if self.crystal != other.crystal:
other.crystal = CrystalBarrier.Either
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
self.event_doors):
self.append_door_to_list(door, self.event_doors, flag)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors, flag)
@@ -664,7 +695,8 @@ class ExplorationState(object):
def add_all_doors_check_key_region(self, region, key_region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
@@ -724,7 +756,8 @@ class ExplorationState(object):
return cnt
def validate(self, door, region, world, player):
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, world, player)
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
world, player)
def in_door_list(self, door, door_list):
for d in door_list:
@@ -785,7 +818,8 @@ def extend_reachable_state(search_regions, state, world, player):
explorable_door = local_state.next_avail_door()
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(connect_region):
if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(
connect_region):
local_state.visit_region(connect_region)
local_state.add_all_doors_check_unattached(connect_region, world, player)
return local_state
@@ -799,20 +833,28 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey:
if isOrigin and local_state.count_locations_exclude_specials() == 0:
if isOrigin:
big_not_found = not special_big_key_found(local_state, world, player) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0
if big_not_found:
continue # we can't open this door
if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else:
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(connect_region):
if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(
connect_region):
flag = explorable_door.flag or explorable_door.door.bigKey
local_state.visit_region(connect_region, bk_Flag=flag)
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player)
return local_state
def special_big_key_found(state, world, player):
cellblock = world.get_region('Hyrule Dungeon Cellblock', player)
return state.visited(cellblock)
# cross-utility methods
def valid_region_to_explore(region, name, world, player):
if region is None:
@@ -852,3 +894,713 @@ def convert_regions(region_names, world, player):
for name in region_names:
region_list.append(world.get_region(name, player))
return region_list
# Begin crossed mode sector shuffle
class DungeonBuilder(object):
def __init__(self, name):
self.name = name
self.sectors = []
self.location_cnt = 0
self.key_drop_cnt = 0
self.bk_required = False
self.bk_provided = False
self.c_switch_required = False
self.c_switch_present = False
self.dead_ends = 0
self.branches = 0
self.mag_needed = [False, False, False]
self.unfulfilled = defaultdict(int)
self.all_entrances = None # used for sector segration/branching
self.entrance_list = None # used for overworld accessibility
self.layout_starts = None # used for overworld accessibility
self.master_sector = None
self.path_entrances = None # used for pathing/key doors, I think
self.candidates = None
self.key_doors_num = None
self.combo_size = None
self.flex = 0
def polarity_complement(self):
pol = Polarity()
for sector in self.sectors:
pol += sector.polarity()
return pol.complement()
def polarity(self):
pol = Polarity()
for sector in self.sectors:
pol += sector.polarity()
return pol
def simple_dungeon_builder(name, sector_list, world, player):
define_sector_features(sector_list, world, player)
builder = DungeonBuilder(name)
dummy_pool = dict.fromkeys(sector_list)
for sector in sector_list:
assign_sector(sector, builder, dummy_pool)
return builder
def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None):
logger = logging.getLogger('')
logger.info('Shuffling Dungeon Sectors')
if dungeon_entrances is None:
dungeon_entrances = default_dungeon_entrances
define_sector_features(all_sectors, world, player)
candidate_sectors = dict.fromkeys(all_sectors)
dungeon_map = {}
for key in dungeon_regions.keys():
dungeon_map[key] = DungeonBuilder(key)
for key in dungeon_boss_sectors.keys():
current_dungeon = dungeon_map[key]
for r_name in dungeon_boss_sectors[key]:
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
if key == 'Hyrule Castle' and world.mode == 'standard':
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
for key in dungeon_entrances.keys():
current_dungeon = dungeon_map[key]
current_dungeon.all_entrances = dungeon_entrances[key]
for r_name in current_dungeon.all_entrances:
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
# categorize sectors
free_location_sectors = {}
crystal_switches = {}
crystal_barriers = {}
polarized_sectors = {}
neutral_sectors = {}
for sector in candidate_sectors:
if sector.chest_locations > 0:
free_location_sectors[sector] = None
elif sector.c_switch:
crystal_switches[sector] = None
elif sector.blue_barrier:
crystal_barriers[sector] = None
elif sector.polarity().is_neutral():
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
logger.info('-Assigning Chest Locations')
assign_location_sectors(dungeon_map, free_location_sectors)
logger.info('-Assigning Crystal Switches and Barriers')
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches)
for sector in leftover:
if sector.polarity().is_neutral():
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
# blue barriers
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers)
# polarity:
logger.info('-Balancing Doors')
assign_polarized_sectors(dungeon_map, polarized_sectors, logger)
# the rest
assign_the_rest(dungeon_map, neutral_sectors)
return dungeon_map
def define_sector_features(sectors, world, player):
for sector in sectors:
if 'Hyrule Dungeon Cellblock' in sector.region_set():
sector.bk_provided = True
if 'Thieves Blind\'s Cell' in sector.region_set():
sector.bk_required = True
for region in sector.regions:
for loc in region.locations:
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2', 'Hyrule Castle - Big Key Drop']:
pass
elif loc.event and 'Small Key' in loc.item.name:
sector.key_only_locations += 1
elif loc.name not in dungeon_events:
sector.chest_locations += 1
if '- Big Chest' in loc.name:
sector.bk_required = True
for ext in region.exits:
door = world.check_for_door(ext.name, player)
if door is not None:
if door.crystal == CrystalBarrier.Either:
sector.c_switch = True
elif door.crystal == CrystalBarrier.Orange:
sector.orange_barrier = True
elif door.crystal == CrystalBarrier.Blue:
sector.blue_barrier = True
if door.bigKey:
sector.bk_required = True
def assign_sector(sector, dungeon, candidate_sectors):
if sector is not None:
del candidate_sectors[sector]
dungeon.sectors.append(sector)
dungeon.location_cnt += sector.chest_locations
dungeon.key_drop_cnt += sector.key_only_locations
if sector.c_switch:
dungeon.c_switch_present = True
if sector.blue_barrier:
dungeon.c_switch_required = True
if sector.bk_required:
dungeon.bk_required = True
if sector.bk_provided:
dungeon.bk_provided = True
if sector.outflow() == 1:
dungeon.dead_ends += 1
if sector.outflow() > 2:
dungeon.branches += sector.outflow() - 2
def find_sector(r_name, sectors):
for s in sectors:
if r_name in s.region_set():
return s
return None
def assign_location_sectors(dungeon_map, free_location_sectors):
valid = False
choices = None
sector_list = list(free_location_sectors)
random.shuffle(sector_list)
while not valid:
choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list)
for i, sector in enumerate(sector_list):
choice = d_idx[choices[i].name]
totals[choice] += sector.chest_locations
valid = True
for d_name, idx in d_idx.items():
if totals[idx] < minimal_locations(d_name):
valid = False
break
for i, choice in enumerate(choices):
builder = dungeon_map[choice.name]
assign_sector(sector_list[i], builder, free_location_sectors)
def weighted_random_locations(dungeon_map, free_location_sectors):
population = []
ttl_assigned = 0
weights = []
totals = []
d_idx = {}
for i, dungeon_builder in enumerate(dungeon_map.values()):
population.append(dungeon_builder)
totals.append(dungeon_builder.location_cnt)
ttl_assigned += dungeon_builder.location_cnt
weights.append(6.375)
d_idx[dungeon_builder.name] = i
average = ttl_assigned / 13
for i, db in enumerate(population):
if db.location_cnt < average:
weights[i] += average - db.location_cnt
if db.location_cnt > average:
weights[i] = max(0, weights[i] - db.location_cnt + average)
choices = random.choices(population, weights, k=len(free_location_sectors))
return choices, d_idx, totals
def minimal_locations(dungeon_name):
if dungeon_name == 'Hyrule Castle':
return 3
if dungeon_name == 'Agahnims Tower':
return 2 # bump to 3 once compasses work
# reduce gt to 4 once compasses work
return 5
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, assign_one=False):
population = []
some_c_switches_present = False
for name, builder in dungeon_map.items():
if builder.c_switch_required and not builder.c_switch_present:
population.append(name)
if builder.c_switch_present:
some_c_switches_present = True
if len(population) == 0: # nothing needs a switch
if assign_one and not some_c_switches_present: # something should have one
choice = random.choice(list(dungeon_map.keys()))
builder = dungeon_map[choice]
assign_sector(random.choice(list(crystal_switches)), builder, crystal_switches)
return crystal_switches
sector_list = list(crystal_switches)
choices = random.choices(sector_list, k=len(population))
for i, choice in enumerate(choices):
builder = dungeon_map[population[i]]
assign_sector(choice, builder, crystal_switches)
return crystal_switches
def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers):
population = []
for name, builder in dungeon_map.items():
if builder.c_switch_present:
population.append(name)
sector_list = list(crystal_barriers)
random.shuffle(sector_list)
choices = random.choices(population, k=len(sector_list))
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
assign_sector(sector_list[i], builder, crystal_barriers)
def identify_polarity_issues(dungeon_map):
unconnected_builders = {}
for name, builder in dungeon_map.items():
if len(builder.sectors) == 1:
continue
if len(builder.sectors) == 2:
def sector_filter(x, y):
return x != y
else:
def sector_filter(x, y):
return x != y and x.outflow() > 1
for sector in builder.sectors:
others = [x for x in builder.sectors if sector_filter(x, sector)]
other_mag = sum_magnitude(others)
sector_mag = sector.magnitude()
for i in range(len(sector_mag)):
if sector_mag[i] > 0 and other_mag[i] == 0:
builder.mag_needed[i] = True
if name not in unconnected_builders.keys():
unconnected_builders[name] = builder
return unconnected_builders
def identify_branching_issues(dungeon_map):
unconnected_builders = {}
for name, builder in dungeon_map.items():
unsatisfied_doors = defaultdict(list)
satisfying_doors = defaultdict(list)
entrance_doors = defaultdict(list)
multi_purpose = defaultdict(list)
for sector in builder.sectors:
is_entrance = is_entrance_sector(builder, sector)
if is_entrance:
for door in sector.outstanding_doors:
dependent_doors = [x for x in sector.outstanding_doors if x != door]
if not door.blocked:
entrance_doors[hook_from_door(door)].append((door, dependent_doors))
else:
unsatisfied_doors[hook_from_door(door)].append((door, dependent_doors))
else:
outflow = sector.outflow()
outflow -= len([x for x in sector.outstanding_doors if x.dead])
other_doors = []
one_way_flag = False
for door in sector.outstanding_doors:
dependent_doors = [x for x in sector.outstanding_doors if x != door]
if door.blocked or door.dead or (outflow <= 1 and len(dependent_doors) == 0):
unsatisfied_doors[hook_from_door(door)].append((door, dependent_doors))
one_way_flag = True
else:
other_doors.append((door, dependent_doors))
if not one_way_flag and outflow >= 2:
for door, deps in other_doors:
multi_purpose[hook_from_door(door)].append((door, deps))
elif one_way_flag or outflow <= 1:
for door, deps in other_doors:
satisfying_doors[hook_from_door(door)].append((door, deps))
used_doors = set()
satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose])
while not satisfied:
candidate_is_unsated = True
candidate, dep_list = choose_candidate([unsatisfied_doors])
if candidate is None:
candidate_is_unsated = False
candidate, dep_list = choose_candidate([multi_purpose, satisfying_doors, entrance_doors]) # consider satifying doors here?
match_list = [satisfying_doors, multi_purpose, entrance_doors]
match_maker, match_deps = find_candidate_match(candidate, dep_list, candidate_is_unsated, match_list)
if match_maker is None:
unconnected_builders[name] = builder
builder.unfulfilled[hook_from_door(candidate)] += 1
for hook, door_list in unsatisfied_doors.items():
builder.unfulfilled[hook] += len(door_list)
satisfied = True
continue
used_doors.add(candidate)
used_doors.add(match_maker)
if candidate_is_unsated and len(match_deps) == 1:
for door in match_deps:
door_list = multi_purpose[hook_from_door(door)]
pair = find_door_in_list(door, door_list)
if pair[0] is not None:
door_list.remove(pair)
unsatisfied_doors[hook_from_door(door)].append((pair))
satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose])
return unconnected_builders
def is_entrance_sector(builder, sector):
for entrance in builder.all_entrances:
r_set = sector.region_set()
if entrance in r_set:
return True
return False
def is_satisfied(door_dict_list):
for door_dict in door_dict_list:
for door_list in door_dict.values():
if len(door_list) > 0:
return False
return True
def choose_candidate(door_dict_list):
for door_dict in door_dict_list:
min_len = None
candidate_list = None
for dir, door_list in door_dict.items():
curr_len = len(door_list)
if curr_len > 0 and (min_len is None or curr_len < min_len):
candidate_list = door_list
min_len = curr_len
if min_len is not None:
candidate, dep_list = candidate_list.pop()
return candidate, dep_list
return None, None
def find_candidate_match(candidate, dep_list, check_deps, door_dict_list):
dir = hanger_from_door(candidate)
backup_pair = None
backup_list = None
for door_dict in door_dict_list:
door_list = door_dict[dir]
pair = None
for match, match_deps in door_list:
if not check_deps or match not in dep_list:
pair = match, match_deps
break
elif len(filter_match_deps(candidate, match_deps)) > 0:
backup_pair = match, match_deps
backup_list = door_list
if pair is not None:
door_list.remove(pair)
return pair
if backup_pair is not None:
backup_list.remove(backup_pair)
logging.getLogger('').debug('Matching %s to %s unsure if safe', candidate, backup_pair[0])
return backup_pair
return None, None
def find_door_in_list(door, door_list):
for d, deps in door_list:
if d == door:
return d, deps
return None, None
# todo: maybe filter by used doors too
# todo: I want the number of door that match is accessible by still
def filter_match_deps(candidate, match_deps):
return [x for x in match_deps if x != candidate]
def sum_magnitude(sector_list):
result = [0, 0, 0]
for sector in sector_list:
vector = sector.magnitude()
for i in range(len(result)):
result[i] = result[i] + vector[i]
return result
def sum_polarity(sector_list):
pol = Polarity()
for sector in sector_list:
pol += sector.polarity()
return pol
def assign_polarized_sectors(dungeon_map, polarized_sectors, logger):
# step 1: fix polarity connection issues
logger.info('--Basic Traversal')
unconnected_builders = identify_polarity_issues(dungeon_map)
while len(unconnected_builders) > 0:
for name, builder in unconnected_builders.items():
candidates = find_connection_candidates(builder.mag_needed, polarized_sectors)
if len(candidates) == 0:
raise Exception('Cross Dungeon Builder: Cannot find a candidate for connectedness - restart?')
sector = random.choice(candidates)
assign_sector(sector, builder, polarized_sectors)
builder.mag_needed = [False, False, False]
unconnected_builders = identify_polarity_issues(dungeon_map)
# step 2: fix neutrality issues
builder_order = list(dungeon_map.values())
random.shuffle(builder_order)
for builder in builder_order:
logger.info('--Balancing %s', builder.name)
while not builder.polarity().is_neutral():
candidates = find_neutralizing_candidates(builder.polarity(), polarized_sectors)
sectors = random.choice(candidates)
for sector in sectors:
assign_sector(sector, builder, polarized_sectors)
# step 3: fix dead ends
problem_builders = identify_branching_issues(dungeon_map)
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
while len(problem_builders) > 0:
for name, builder in problem_builders.items():
candidates = find_branching_candidates(builder, neutral_choices)
choice = random.choice(candidates)
if valid_polarized_assignment(builder, choice):
neutral_choices.remove(choice)
for sector in choice:
assign_sector(sector, builder, polarized_sectors)
builder.unfulfilled.clear()
problem_builders = identify_branching_issues(dungeon_map)
# step 4: assign randomly until gone - must maintain connectedness, neutral polarity
while len(polarized_sectors) > 0:
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
if valid_polarized_assignment(builder, neutral_choices[i]):
for sector in neutral_choices[i]:
assign_sector(sector, builder, polarized_sectors)
def find_connection_candidates(mag_needed, sector_pool):
candidates = []
for sector in sector_pool:
if sector.outflow() < 2:
continue
mag = sector.magnitude()
matches = False
for i, need in enumerate(mag_needed):
if need and mag[i] > 0:
matches = True
break
if matches:
candidates.append(sector)
return candidates
def find_neutralizing_candidates(polarity, sector_pool):
candidates = defaultdict(list)
original_charge = polarity.charge()
best_charge = original_charge
main_pool = list(sector_pool)
last_r = 0
while len(candidates) == 0:
r_range = range(last_r + 1, last_r + 3)
for r in r_range:
if r > len(main_pool):
if len(candidates) == 0:
raise Exception('Cross Dungeon Builder: No possible neutralizers left')
else:
continue
last_r = r
combinations = ncr(len(main_pool), r)
for i in range(0, combinations):
choice = kth_combination(i, main_pool, r)
p_charge = (polarity + sum_polarity(choice)).charge()
if p_charge < original_charge and p_charge <= best_charge:
candidates[p_charge].append(choice)
if p_charge < best_charge:
best_charge = p_charge
candidate_list = candidates[best_charge]
best_len = 10
official_cand = []
for cand in candidate_list:
size = len(cand)
if size < best_len:
best_len = size
official_cand = [cand]
elif size == best_len:
official_cand.append(cand)
return official_cand
def find_branching_candidates(builder, neutral_choices):
candidates = []
for choice in neutral_choices:
door_match = False
flow_match = False
for sector in choice:
if sector.adj_outflow() >= 2:
flow_match = True
for door in sector.outstanding_doors:
if builder.unfulfilled[hanger_from_door(door)] > 0:
door_match = True
if door_match and flow_match:
candidates.append(choice)
if len(candidates) == 0:
raise Exception('Cross Dungeon Builder: No more branching candidates!')
return candidates
def neutralize_the_rest(sector_pool):
neutral_choices = []
main_pool = list(sector_pool)
failed_pool = []
r_size = 1
while len(main_pool) > 0 or len(failed_pool) > 0:
if len(main_pool) <= r_size:
main_pool.extend(failed_pool)
failed_pool.clear()
r_size += 1
candidate = random.choice(main_pool)
main_pool.remove(candidate)
if r_size > len(main_pool):
raise Exception("Cross Dungeon Builder: no more neutral pairings possible")
combinations = ncr(len(main_pool), r_size)
itr = 0
done = False
while not done:
ttl_polarity = candidate.polarity()
choice_set = kth_combination(itr, main_pool, r_size)
for choice in choice_set:
ttl_polarity += choice.polarity()
if ttl_polarity.is_neutral():
choice_set.append(candidate)
neutral_choices.append(choice_set)
main_pool = [x for x in main_pool if x not in choice_set]
failed_pool = [x for x in failed_pool if x not in choice_set]
done = True
else:
itr += 1
if itr >= combinations:
failed_pool.append(candidate)
done = True
return neutral_choices
def valid_polarized_assignment(builder, sector_list):
full_list = sector_list + builder.sectors
for sector in full_list:
others = [x for x in full_list if x != sector]
other_mag = sum_magnitude(others)
sector_mag = sector.magnitude()
for i in range(len(sector_mag)):
if sector_mag[i] > 0 and other_mag[i] == 0:
return True
# dead_ends = 0
# branches = 0
# for sector in sector_list:
# if sector.outflow == 1:
# dead_ends += 1
# if sector.outflow() > 2:
# branches += sector.outflow() - 2
# if builder.dead_ends + dead_ends > builder.branches + branches:
# return False
return (sum_polarity(sector_list) + sum_polarity(builder.sectors)).is_neutral()
def assign_the_rest(dungeon_map, neutral_sectors):
while len(neutral_sectors) > 0:
sector_list = list(neutral_sectors)
choices = random.choices(list(dungeon_map.keys()), k=len(sector_list))
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
if valid_polarized_assignment(builder, [sector_list[i]]):
assign_sector(sector_list[i], builder, neutral_sectors)
def split_dungeon_builder(builder, split_list):
logger = logging.getLogger('')
logger.info('Splitting Up Desert/Skull')
candidate_sectors = dict.fromkeys(builder.sectors)
dungeon_map = {}
for name, split_entrances in split_list.items():
key = builder.name + ' ' + name
dungeon_map[key] = sub_builder = DungeonBuilder(key)
sub_builder.all_entrances = split_entrances
for r_name in split_entrances:
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors)
# categorize sectors
crystal_switches = {}
crystal_barriers = {}
polarized_sectors = {}
neutral_sectors = {}
for sector in candidate_sectors:
if sector.c_switch:
crystal_switches[sector] = None
elif sector.blue_barrier:
crystal_barriers[sector] = None
elif sector.polarity().is_neutral():
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, len(crystal_barriers) > 0)
for sector in leftover:
if sector.polarity().is_neutral():
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
# blue barriers
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers)
# polarity:
logger.info('-Re-balancing Desert/Skull')
assign_polarized_sectors(dungeon_map, polarized_sectors, logger)
# the rest
assign_the_rest(dungeon_map, neutral_sectors)
return dungeon_map
# common functions - todo: move to a common place
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l) - 1, r - 1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r - 1)
else:
return kth_combination(k - i, l[1:], r)
def ncr(n, r):
if r == 0:
return 1
r = min(r, n - r)
numerator = reduce(op.mul, range(n, n - r, -1), 1)
denominator = reduce(op.mul, range(1, r + 1), 1)
return int(numerator / denominator)
dungeon_boss_sectors = {
'Hyrule Castle': [],
'Eastern Palace': ['Eastern Boss'],
'Desert Palace': ['Desert Boss'],
'Tower of Hera': ['Hera Boss'],
'Agahnims Tower': ['Tower Agahnim 1'],
'Palace of Darkness': ['PoD Boss'],
'Swamp Palace': ['Swamp Boss'],
'Skull Woods': ['Skull Boss'],
'Thieves Town': ['Thieves Attic Window', 'Thieves Blind\'s Cell', 'Thieves Boss'],
'Ice Palace': ['Ice Boss'],
'Misery Mire': ['Mire Boss'],
'Turtle Rock': ['TR Boss'],
'Ganons Tower': ['GT Agahnim 2']
}
default_dungeon_entrances = {
'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Rat Path',
'Sanctuary'],
'Eastern Palace': ['Eastern Lobby'],
'Desert Palace': ['Desert Back Lobby', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'],
'Tower of Hera': ['Hera Lobby'],
'Agahnims Tower': ['Tower Lobby'],
'Palace of Darkness': ['PoD Lobby'],
'Swamp Palace': ['Swamp Lobby'],
'Skull Woods': ['Skull 1 Lobby', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull 2 East Lobby',
'Skull 2 West Lobby', 'Skull Back Drop', 'Skull 3 Lobby'],
'Thieves Town': ['Thieves Lobby'],
'Ice Palace': ['Ice Lobby'],
'Misery Mire': ['Mire Lobby'],
'Turtle Rock': ['TR Main Lobby', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'],
'Ganons Tower': ['GT Lobby']
}

View File

@@ -333,15 +333,15 @@ region_starts = {
}
split_region_starts = {
'Desert Palace': [
['Desert Back Lobby'],
['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby']
],
'Skull Woods': [
['Skull 1 Lobby', 'Skull Pot Circle'],
['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'],
['Skull 3 Lobby']
]
'Desert Palace': {
'Back': ['Desert Back Lobby'],
'Main': ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby']
},
'Skull Woods': {
'1': ['Skull 1 Lobby', 'Skull Pot Circle'],
'2': ['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'],
'3': ['Skull 3 Lobby']
}
}
flexible_starts = {

View File

@@ -127,6 +127,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),

View File

@@ -6,37 +6,6 @@ from Dungeons import dungeon_keys, dungeon_bigs
from DungeonGenerator import ExplorationState
class KeySphere(object):
def __init__(self):
self.access_door = None
self.free_locations = {}
self.prize_region = False
self.key_only_locations = {}
self.child_doors = {}
self.bk_locked = False
self.parent_sphere = None
self.other_locations = {}
def __eq__(self, other):
if self.prize_region != other.prize_region:
return False
# already have merge function for this
# if self.bk_locked != other.bk_locked:
# return False
if len(self.free_locations) != len(other.free_locations):
return False
if len(self.key_only_locations) != len(other.key_only_locations):
return False
if len(set(self.free_locations).symmetric_difference(set(other.free_locations))) > 0:
return False
if len(set(self.key_only_locations).symmetric_difference(set(other.key_only_locations))) > 0:
return False
if len(set(self.child_doors).symmetric_difference(set(other.child_doors))) > 0:
return False
return True
class KeyLayout(object):
def __init__(self, sector, starts, proposal):
@@ -45,20 +14,21 @@ class KeyLayout(object):
self.proposal = proposal
self.key_logic = KeyLogic(sector.name)
self.key_spheres = None
self.key_counters = None
self.flat_prop = None
self.max_chests = None
self.max_drops = None
self.all_chest_locations = {}
self.big_key_special = False
# bk special?
# bk required? True if big chests or big doors exists
def reset(self, proposal):
def reset(self, proposal, builder, world, player):
self.proposal = proposal
self.flat_prop = flatten_pair_list(self.proposal)
self.key_logic = KeyLogic(self.sector.name)
self.max_chests = calc_max_chests(builder, self, world, player)
class KeyLogic(object):
@@ -69,6 +39,8 @@ class KeyLogic(object):
self.sm_restricted = set()
self.small_key_name = dungeon_keys[dungeon_name]
self.bk_name = dungeon_bigs[dungeon_name]
self.bk_doors = set()
self.bk_chests = set()
self.logic_min = {}
self.logic_max = {}
@@ -96,34 +68,7 @@ class KeyCounter(object):
self.used_keys = 0
self.big_key_opened = False
self.important_location = False
def update(self, key_sphere):
self.free_locations.update(key_sphere.free_locations)
self.key_only_locations.update(key_sphere.key_only_locations)
self.child_doors.update(dict.fromkeys([x for x in key_sphere.child_doors if x not in self.open_doors and x.dest not in self.open_doors]))
self.important_location = self.important_location or key_sphere.prize_region or self.special_region(key_sphere)
@staticmethod
def special_region(key_sphere):
for other in key_sphere.other_locations:
# todo: zelda's cell is special in standard, and probably crossed too
if other.name in ['Attic Cracked Floor', 'Suspicious Maiden']:
return True
return False
def open_door(self, door, flat_proposal):
if door in flat_proposal:
self.used_keys += 1
del self.child_doors[door]
self.open_doors[door] = None
if door.dest in flat_proposal:
self.open_doors[door.dest] = None
if door.dest in self.child_doors:
del self.child_doors[door.dest]
elif door.bigKey:
self.big_key_opened = True
del self.child_doors[door]
self.open_doors[door] = None
self.other_locations = {}
def used_smalls_loc(self, reserve=0):
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
@@ -140,29 +85,35 @@ class KeyCounter(object):
return ret
def build_key_layout(sector, start_regions, proposal, world, player):
key_layout = KeyLayout(sector, start_regions, proposal)
def build_key_layout(builder, start_regions, proposal, world, player):
key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
key_layout.max_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
key_layout.max_drops = count_key_drops(key_layout.sector)
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set()
return key_layout
def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle != 'crossed':
return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
return builder.key_doors_num - key_layout.max_drops
def analyze_dungeon(key_layout, world, player):
key_layout.key_counters = create_key_counters(key_layout, world, player)
key_layout.key_spheres = create_key_spheres(key_layout, world, player)
key_logic = key_layout.key_logic
find_bk_locked_sections(key_layout, world)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
init_bk = check_special_locations(key_layout.key_spheres['Origin'].free_locations.keys())
key_counter = key_layout.key_counters[counter_id({}, init_bk, key_layout.flat_prop)]
queue = collections.deque([(key_layout.key_spheres['Origin'], key_counter)])
original_key_counter = find_counter({}, False, key_layout)
queue = collections.deque([(None, original_key_counter)])
doors_completed = set()
while len(queue) > 0:
queue = collections.deque(sorted(queue, key=queue_sorter))
key_sphere, key_counter = queue.popleft()
parent_door, key_counter = queue.popleft()
chest_keys = available_chest_small_keys(key_counter, world)
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
@@ -170,29 +121,28 @@ def analyze_dungeon(key_layout, world, player):
if not key_counter.big_key_opened:
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_sphere.bk_locked and big_chest_in_locations(key_counter.free_locations):
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
# todo: detect forced subsequent keys - see keypuzzles
# try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = collections.deque()
for child in sorted(list(key_sphere.child_doors), key=lambda x: x.name):
next_sphere = key_layout.key_spheres[child.name]
# todo: empty_sphere are not always empty, Mire spike barrier is not empty if other doors open first
if not empty_sphere(next_sphere) and child not in doors_completed:
child_queue.append((child, next_sphere))
for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world)
if not empty_counter(odd_counter) and child not in doors_completed:
child_queue.append((child, odd_counter))
while len(child_queue) > 0:
child, next_sphere = child_queue.popleft()
child, odd_counter = child_queue.popleft()
if not child.bigKey:
best_counter = find_best_counter(child, key_counter, key_layout, world, False)
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, False)
rule = create_rule(best_counter, key_counter, key_layout, world)
check_for_self_lock_key(rule, next_sphere, key_layout, world)
bk_restricted_rules(rule, next_sphere, key_counter, key_layout, world)
check_for_self_lock_key(rule, child, best_counter, key_layout, world)
bk_restricted_rules(rule, child, odd_counter, key_counter, key_layout, world)
key_logic.door_rules[child.name] = rule
doors_completed.add(next_sphere.access_door)
next_counter = find_next_counter(child, key_counter, next_sphere, key_layout)
queue.append((next_sphere, next_counter))
check_rules(key_layout)
return key_layout
doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout)
queue.append((child, next_counter))
check_rules(original_key_counter, key_layout)
def count_key_drops(sector):
@@ -205,43 +155,42 @@ def count_key_drops(sector):
def queue_sorter(queue_item):
sphere, counter = queue_item
if sphere.access_door is None:
door, counter = queue_item
if door is None:
return 0
return 1 if sphere.access_door.bigKey else 0
return 1 if door.bigKey else 0
def find_bk_locked_sections(key_layout, world):
key_spheres = key_layout.key_spheres
key_counters = key_layout.key_counters
key_logic = key_layout.key_logic
bk_key_not_required = set()
big_chest_allowed_big_key = world.accessibility != 'locations'
for key in key_spheres.keys():
sphere = key_spheres[key]
key_layout.all_chest_locations.update(sphere.free_locations)
if sphere.bk_locked and (sphere.prize_region or KeyCounter.special_region(sphere)):
for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations)
if counter.big_key_opened and counter.important_location:
big_chest_allowed_big_key = False
if not sphere.bk_locked:
bk_key_not_required.update(sphere.free_locations)
if not counter.big_key_opened:
bk_key_not_required.update(counter.free_locations)
key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_key_not_required)))
if not big_chest_allowed_big_key:
key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations))
def empty_sphere(sphere):
if len(sphere.key_only_locations) != 0 or len(sphere.free_locations) != 0 or len(sphere.child_doors) != 0:
def empty_counter(counter):
if len(counter.key_only_locations) != 0 or len(counter.free_locations) != 0 or len(counter.child_doors) != 0:
return False
return not sphere.prize_region
return not counter.important_location
def relative_empty_sphere(sphere, key_counter):
if len(set(sphere.key_only_locations).difference(key_counter.key_only_locations)) > 0:
def relative_empty_counter(odd_counter, key_counter):
if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False
if len(set(sphere.free_locations).difference(key_counter.free_locations)) > 0:
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
new_child_door = False
for child in sphere.child_doors:
for child in odd_counter.child_doors:
if unique_child_door(child, key_counter):
new_child_door = True
break
@@ -260,23 +209,15 @@ def unique_child_door(child, key_counter):
return True
def increment_key_counter(door, sphere, key_counter, flat_proposal):
new_counter = key_counter.copy()
new_counter.open_door(door, flat_proposal)
new_counter.update(sphere)
return new_counter
def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible?
door_sphere = key_layout.key_spheres[door.name]
ignored_doors = {door, door.dest}
def find_best_counter(door, odd_counter, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened
# new_counter = key_counter
last_counter = key_counter
while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, skip_bk)
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
if door_set is None or len(door_set) == 0:
finished = True
continue
@@ -286,7 +227,7 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to
new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened
# this means the new_door invalidates the door / leads to the same stuff
if relative_empty_sphere(door_sphere, new_counter):
if relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door)
else:
if not key_wasted(new_door, last_counter, new_counter, key_layout, world):
@@ -298,16 +239,19 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to
return last_counter
def find_potential_open_doors(key_counter, ignored_doors, skip_bk):
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
small_doors = []
big_doors = []
for other in key_counter.child_doors:
if other not in ignored_doors and other.dest not in ignored_doors:
if other.bigKey:
if not skip_bk:
if not skip_bk and (not key_layout.big_key_special or key_counter.big_key_opened):
big_doors.append(other)
elif other.dest not in small_doors:
small_doors.append(other)
if key_layout.big_key_special:
big_key_available = key_counter.big_key_opened
else:
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0
if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
return None
@@ -338,10 +282,10 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world):
return False
def find_next_counter(new_door, old_counter, next_sphere, key_layout):
def find_next_counter(new_door, old_counter, key_layout):
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = old_counter.big_key_opened or new_door.bigKey or check_special_locations(next_sphere.free_locations)
return key_layout.key_counters[counter_id(proposed_doors, bk_open, key_layout.flat_prop)]
bk_open = old_counter.big_key_opened or new_door.bigKey
return find_counter(proposed_doors, bk_open, key_layout)
def check_special_locations(locations):
@@ -358,8 +302,8 @@ def calc_avail_keys(key_counter, world):
def create_rule(key_counter, prev_counter, key_layout, world):
prev_chest_keys = available_chest_small_keys(prev_counter, world)
prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world)
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
raw_avail = chest_keys + len(key_counter.key_only_locations)
@@ -377,29 +321,78 @@ def create_rule(key_counter, prev_counter, key_layout, world):
return DoorRules(rule_num)
def check_for_self_lock_key(rule, sphere, key_layout, world):
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world):
if world.accessibility != 'locations':
counter = KeyCounter(key_layout.max_chests)
counter.update(sphere)
counter = find_inverted_counter(door, parent_counter, key_layout, world)
if not self_lock_possible(counter):
return
queue = collections.deque(counter.child_doors)
already_queued = set(counter.child_doors)
while len(queue) > 0:
child = queue.popleft()
if child not in counter.open_doors:
counter = increment_key_counter(child, key_layout.key_spheres[child.name], counter, key_layout.flat_prop)
if not self_lock_possible(counter):
return
for new_door in counter.child_doors:
if new_door not in already_queued:
queue.append(new_door)
already_queued.add(new_door)
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
rule.allow_small = True
rule.small_location = next(iter(counter.free_locations))
def find_inverted_counter(door, parent_counter, key_layout, world):
# open all doors in counter
counter = open_all_counter(parent_counter, key_layout, door=door)
max_counter = find_max_counter(key_layout)
# find the difference
inverted_counter = KeyCounter(key_layout.max_chests)
inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations)
inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations)
# child doors? used_keys?
inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
for loc in inverted_counter.other_locations:
if important_location(loc, world):
inverted_counter.important_location = True
return inverted_counter
def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
changed = True
counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
while changed:
changed = False
doors_to_open = {}
for child in counter.child_doors:
if door is None or (child != door and child != door.dest):
if skipBk:
if not child.bigKey:
doors_to_open[child] = None
elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
doors_to_open[child] = None
if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
changed = True
return counter
def open_some_counter(parent_counter, key_layout, ignored_doors):
changed = True
counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
while changed:
changed = False
doors_to_open = {}
for child in counter.child_doors:
if child not in ignored_doors:
if not child.bigKey:
doors_to_open[child] = None
if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
changed = True
return counter
def self_lock_possible(counter):
return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location
@@ -415,136 +408,27 @@ def available_chest_small_keys(key_counter, world):
return key_counter.max_chests
def bk_restricted_rules(rule, sphere, key_counter, key_layout, world):
if sphere.bk_locked:
def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world):
if key_counter.big_key_opened:
return
best_counter = find_best_counter(sphere.access_door, key_counter, key_layout, world, True)
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, True)
bk_number = create_rule(best_counter, key_counter, key_layout, world).small_key_num
if bk_number == rule.small_key_num:
return
post_counter = KeyCounter(key_layout.max_chests)
post_counter.update(sphere)
other_doors_beyond_me = [x for x in post_counter.child_doors if not x.bigKey]
queue = collections.deque(other_doors_beyond_me)
already_queued = set(other_doors_beyond_me)
while len(queue) > 0:
child = queue.popleft()
if child not in post_counter.open_doors:
post_counter = increment_key_counter(child, key_layout.key_spheres[child.name], post_counter, key_layout.flat_prop)
for new_door in post_counter.child_doors:
if not new_door.bigKey and new_door not in already_queued and new_door.dest not in already_queued:
queue.append(new_door)
already_queued.add(new_door)
unique_loc = set(post_counter.free_locations).difference(set(best_counter.free_locations))
door_open = find_next_counter(door, best_counter, key_layout)
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
dest_ignored = []
for door in ignored_doors.keys():
if door.dest not in ignored_doors:
dest_ignored.append(door.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
if len(unique_loc) > 0:
rule.alternate_small_key = bk_number
rule.alternate_big_key_loc.update(unique_loc)
def expand_counter_no_big_doors(door, key_counter, key_layout, ignored_doors):
door_sphere = key_layout.key_spheres[door.name]
small_doors = set()
for other in key_counter.child_doors:
if other != door and other not in ignored_doors:
if other.dest not in small_doors and not other.bigKey:
small_doors.add(other)
if len(small_doors) == 0:
return key_counter
new_counter = key_counter
last_counter = key_counter
new_ignored = set(ignored_doors)
for new_door in small_doors:
new_sphere = key_layout.key_spheres[new_door.name]
new_counter = increment_key_counter(new_door, new_sphere, new_counter, key_layout.flat_prop)
# this means the new_door invalidates the door / leads to the same stuff
if relative_empty_sphere(door_sphere, new_counter):
new_counter = last_counter
new_ignored.add(new_door)
else:
last_counter = new_counter
old_counter = None
while old_counter != new_counter:
old_counter = new_counter
new_counter = expand_counter_no_big_doors(door, old_counter, key_layout, new_ignored)
return new_counter
def create_key_spheres(key_layout, world, player):
key_spheres = {}
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
key_spheres['Origin'] = create_key_sphere(state, None, None)
queue = collections.deque([(key_spheres['Origin'], state)])
while len(queue) > 0:
next_key_sphere, parent_state = queue.popleft()
for door in next_key_sphere.child_doors:
child_state = parent_state.copy()
# open the door
open_a_door(door, child_state, flat_proposal)
expand_key_state(child_state, flat_proposal, world, player)
child_kr = create_key_sphere(child_state, next_key_sphere, door)
if door.name not in key_spheres.keys():
key_spheres[door.name] = child_kr
queue.append((child_kr, child_state))
else:
merge_sphere = old_sphere = key_spheres[door.name]
if empty_sphere(old_sphere) and not empty_sphere(child_kr):
key_spheres[door.name] = merge_sphere = child_kr
queue.append((child_kr, child_state))
if not empty_sphere(old_sphere) and not empty_sphere(child_kr) and not old_sphere == child_kr:
# ugly sphere merge function - just union locations - ugh
if old_sphere.bk_locked != child_kr.bk_locked:
if old_sphere.bk_locked:
merge_sphere.child_doors = child_kr.child_doors
merge_sphere.free_locations = child_kr.free_locations
merge_sphere.key_only_locations = child_kr.key_only_locations
else:
merge_sphere.child_doors = {**old_sphere.child_doors, **child_kr.child_doors}
merge_sphere.free_locations = {**old_sphere.free_locations, **child_kr.free_locations}
merge_sphere.key_only_locations = {**old_sphere.key_only_locations, **child_kr.key_only_locations}
merge_sphere.bk_locked = old_sphere.bk_locked and child_kr.bk_locked
# this feels so ugly, key counters are much smarter than this - would love to get rid of spheres
return key_spheres
def create_key_sphere(state, parent_sphere, door):
key_sphere = KeySphere()
key_sphere.parent_sphere = parent_sphere
p_region = parent_sphere
parent_doors = set()
parent_locations = set()
while p_region is not None:
parent_doors.update(p_region.child_doors)
parent_locations.update(p_region.free_locations)
parent_locations.update(p_region.key_only_locations)
parent_locations.update(p_region.other_locations)
p_region = p_region.parent_sphere
u_doors = [x for x in unique_doors(state.small_doors+state.big_doors) if x not in parent_doors]
key_sphere.child_doors.update(dict.fromkeys(u_doors))
region_locations = [x for x in state.found_locations if x not in parent_locations]
for loc in region_locations:
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
key_sphere.prize_region = True
key_sphere.other_locations[loc] = None
elif loc.event and 'Small Key' in loc.item.name:
key_sphere.key_only_locations[loc] = None
elif loc.name not in dungeon_events:
key_sphere.free_locations[loc] = None
else:
key_sphere.other_locations[loc] = None
# todo: Cellblock in a dungeon with a big_key door or chest - Crossed Mode
key_sphere.bk_locked = state.big_key_opened if not state.big_key_special else False
if door is not None:
key_sphere.access_door = door
return key_sphere
def open_a_door(door, child_state, flat_proposal):
if door.bigKey:
child_state.big_key_opened = True
@@ -610,7 +494,7 @@ def filter_big_chest(locations):
def count_locations_exclude_big_chest(state):
cnt = 0
for loc in state.found_locations:
if '- Big Chest' not in loc.name and '- Prize' not in loc.name:
if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and loc.name not in ['Agahnim 1', 'Agahnim 2', 'Hyrule Castle - Big Key Drop']:
cnt += 1
return cnt
@@ -648,21 +532,33 @@ def flatten_pair_list(paired_list):
return flat_list
def check_rules(key_layout):
def check_rules(original_counter, key_layout):
all_key_only = set()
key_only_map = {}
for sphere in key_layout.key_spheres.values():
for loc in sphere.key_only_locations:
queue = collections.deque([(None, original_counter)])
completed = set()
completed.add(cid(original_counter, key_layout))
while len(queue) > 0:
queue = collections.deque(sorted(queue, key=queue_sorter))
access_door, counter = queue.popleft()
for loc in counter.key_only_locations:
if loc not in all_key_only:
all_key_only.add(loc)
access_rules = []
key_only_map[loc] = access_rules
else:
access_rules = key_only_map[loc]
if sphere.access_door is None or sphere.access_door.name not in key_layout.key_logic.door_rules.keys():
if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
access_rules.append(DoorRules(0))
else:
access_rules.append(key_layout.key_logic.door_rules[sphere.access_door.name])
access_rules.append(key_layout.key_logic.door_rules[access_door.name])
for child in counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
next_counter = find_next_counter(child, counter, key_layout)
c_id = cid(next_counter, key_layout)
if c_id not in completed:
completed.add(c_id)
queue.append((child, next_counter))
min_rule_bk = defaultdict(list)
min_rule_non_bk = defaultdict(list)
check_non_bk = False
@@ -712,30 +608,26 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
# Soft lock stuff
def validate_key_layout_ex(key_layout, world, player):
return validate_key_layout_main_loop(key_layout, world, player)
def validate_key_layout_main_loop(key_layout, world, player):
def validate_key_layout(key_layout, world, player):
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
state.key_locations = key_layout.max_chests
state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
return validate_key_layout_sub_loop(state, {}, flat_proposal, world, player)
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, world, player)
def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, player):
def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, world, player):
expand_key_state(state, flat_proposal, world, player)
smalls_avail = len(state.small_doors) > 0
smalls_avail = len(state.small_doors) > 0 # de-dup crystal repeats
num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing
if not smalls_avail and num_bigs == 0:
return True # I think that's the end
ttl_locations = state.ttl_locations if state.big_key_opened else count_locations_exclude_big_chest(state)
available_small_locations = min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
available_big_locations = ttl_locations - state.used_locations if not state.big_key_special else 0
available_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world)
if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
return False
else:
@@ -747,7 +639,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
state_copy.used_smalls += 1
code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(state_copy, checked_states, flat_proposal, world, player)
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player)
checked_states[code] = valid
else:
valid = checked_states[code]
@@ -759,7 +651,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(state_copy, checked_states, flat_proposal, world, player)
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player)
checked_states[code] = valid
else:
valid = checked_states[code]
@@ -768,6 +660,18 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
return True
def cnt_avail_small_locations(key_layout, ttl_locations, state, world):
if not world.keysanity and world.mode != 'retro':
return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
return key_layout.max_chests + state.key_locations - state.used_smalls
def cnt_avail_big_locations(ttl_locations, state, world):
if not world.keysanity:
return ttl_locations - state.used_locations if not state.big_key_special else 0
return 1 if not state.big_key_special else 0
def create_key_counters(key_layout, world, player):
key_counters = {}
flat_proposal = key_layout.flat_prop
@@ -779,36 +683,39 @@ def create_key_counters(key_layout, world, player):
state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop)
key_counters[code] = create_key_counter_x(state, key_layout, world, player)
key_counters[code] = create_key_counter(state, key_layout, world, player)
queue = collections.deque([(key_counters[code], state)])
while len(queue) > 0:
next_key_sphere, parent_state = queue.popleft()
for door in next_key_sphere.child_doors:
next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors:
child_state = parent_state.copy()
# open the door
if door.bigKey:
key_layout.key_logic.bk_doors.add(door)
# open the door, if possible
if not door.bigKey or not child_state.big_key_special or child_state.big_key_opened:
open_a_door(door, child_state, flat_proposal)
expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop)
if code not in key_counters.keys():
child_kr = create_key_counter_x(child_state, key_layout, world, player)
child_kr = create_key_counter(child_state, key_layout, world, player)
key_counters[code] = child_kr
queue.append((child_kr, child_state))
return key_counters
def create_key_counter_x(state, key_layout, world, player):
def create_key_counter(state, key_layout, world, player):
key_counter = KeyCounter(key_layout.max_chests)
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
for loc in state.found_locations:
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
key_counter.important_location = True
# todo: zelda's cell is special in standard, and probably crossed too
elif loc.name in ['Attic Cracked Floor', 'Suspicious Maiden']:
if important_location(loc, world):
key_counter.important_location = True
key_counter.other_locations[loc] = None
elif loc.event and 'Small Key' in loc.item.name:
key_counter.key_only_locations[loc] = None
elif loc.name not in dungeon_events:
key_counter.free_locations[loc] = None
else:
key_counter.other_locations[loc] = None
key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
if state.big_key_special:
@@ -823,6 +730,34 @@ def create_key_counter_x(state, key_layout, world, player):
return key_counter
def important_location(loc, world):
important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
if world.mode == 'standard' or world.doorShuffle == 'crossed':
important_locations.append('Hyrule Dungeon Cellblock')
return '- Prize' in loc.name or loc.name in important_locations
def create_odd_key_counter(door, parent_counter, key_layout, world):
odd_counter = KeyCounter(key_layout.max_chests)
next_counter = find_next_counter(door, parent_counter, key_layout)
odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations)
odd_counter.key_only_locations = dict_difference(next_counter.key_only_locations, parent_counter.key_only_locations)
odd_counter.child_doors = dict_difference(next_counter.child_doors, parent_counter.child_doors)
odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations)
for loc in odd_counter.other_locations:
if important_location(loc, world):
odd_counter.important_location = True
return odd_counter
def dict_difference(dict_a, dict_b):
return dict.fromkeys([x for x in dict_a.keys() if x not in dict_b.keys()])
def dict_intersection(dict_a, dict_b):
return dict.fromkeys([x for x in dict_a.keys() if x in dict_b.keys()])
def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal:
@@ -857,6 +792,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
return None
def find_max_counter(key_layout):
return find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
def counter_id(opened_doors, bk_unlocked, flat_proposal):
s_id = '1' if bk_unlocked else '0'
for d in flat_proposal:
@@ -864,6 +803,10 @@ def counter_id(opened_doors, bk_unlocked, flat_proposal):
return s_id
def cid(counter, key_layout):
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop)
# class SoftLockException(Exception):
# pass
@@ -900,7 +843,7 @@ def val_hyrule(key_logic, world, player):
def val_eastern(key_logic, world, player):
val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, False, None, 1, {'Eastern Palace - Big Key Chest'})
val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, True, 'Eastern Palace - Big Key Chest', 1, {'Eastern Palace - Big Key Chest'})
val_rule(key_logic.door_rules['Eastern Darkness Up Stairs'], 2)
assert world.get_location('Eastern Palace - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Eastern Palace - Boss', player) in key_logic.bk_restricted
@@ -981,7 +924,7 @@ def val_ice(key_logic, world, player):
def val_mire(key_logic, world, player):
mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'}
# val_rule(key_logic.door_rules['Mire Spikes NW'], 3) # todo: is sometimes 3 or 5? best_counter order matters
val_rule(key_logic.door_rules['Mire Spikes NW'], 5) # todo: is sometimes 3 or 5? best_counter order matters
val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing)
val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing)
assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted

View File

@@ -219,7 +219,7 @@ def create_regions(world, player):
create_dungeon_region(player, 'Hyrule Dungeon Armory Boomerang', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Boomerang Guard Key Drop'], ['Hyrule Dungeon Armory Boomerang WS']),
create_dungeon_region(player, 'Hyrule Dungeon Armory North Branch', 'Hyrule Castle', None, ['Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Down Stairs']),
create_dungeon_region(player, 'Hyrule Dungeon Staircase', 'Hyrule Castle', None, ['Hyrule Dungeon Staircase Up Stairs', 'Hyrule Dungeon Staircase Down Stairs']),
create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle', ['Hyrule Castle - Zelda\'s Chest'], ['Hyrule Dungeon Cellblock Up Stairs']),
create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle', ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest'], ['Hyrule Dungeon Cellblock Up Stairs']),
create_dungeon_region(player, 'Sewers Behind Tapestry', 'Hyrule Castle', None, ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs']),
@@ -817,6 +817,7 @@ key_only_locations = {
'Hyrule Castle - Map Guard Key Drop': 'Small Key (Escape)',
'Hyrule Castle - Boomerang Guard Key Drop': 'Small Key (Escape)',
'Hyrule Castle - Key Rat Key Drop': 'Small Key (Escape)',
'Hyrule Castle - Big Key Drop': 'Big Key (Escape)',
'Eastern Palace - Dark Square Pot Key': 'Small Key (Eastern Palace)',
'Eastern Palace - Dark Eyegore Key Drop': 'Small Key (Eastern Palace)',
'Desert Palace - Desert Tiles 1 Pot Key': 'Small Key (Desert Palace)',

View File

@@ -263,17 +263,12 @@ def global_rules(world, player):
set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('Eastern Single Eyegore NE', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player))
# Big key rules
set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player))
set_rule(world.get_entrance('Eastern Big Key NE', player), lambda state: state.has('Big Key (Eastern Palace)', player))
set_rule(world.get_entrance('Eastern Courtyard N', player), lambda state: state.has('Big Key (Eastern Palace)', player))
# Boss rules. Same as below but no BK or arrow requirement.
set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player))
set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Boss', player))
# Desert
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player))
add_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has('Big Key (Desert Palace)', player))
@@ -281,9 +276,7 @@ def global_rules(world, player):
set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player))
# Tower of Hera
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Hera Startile Corner NW', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
@@ -293,8 +286,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('PoD Bow Statue Down Ladder', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('PoD Dark Alley NE', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
set_rule(world.get_entrance('PoD Map Balcony Drop Down', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('PoD Dark Pegs WN', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player))
@@ -329,20 +320,16 @@ def global_rules(world, player):
set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player))
set_rule(world.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: state.has('Flippers', player))
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player))
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Torch Room EN', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player))
set_rule(world.get_entrance('Thieves BK Corner NE', player), lambda state: state.has('Big Key (Thieves Town)', player))
# blind can't have the small key? - not necessarily true anymore - but likely still
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has('Big Key (Thieves Town)', player) and state.has('Hammer', player)))
for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']:
set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player))
for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
@@ -355,7 +342,6 @@ def global_rules(world, player):
set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player))
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
@@ -376,11 +362,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player) and (state.has_Boots(player) or state.has('Hookshot', player)))
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Mire BK Door Room N', player), lambda state: state.has('Big Key (Misery Mire)', player))
set_rule(world.get_entrance('Mire Square Rail NW', player), lambda state: state.has('Big Key (Misery Mire)', player))
set_rule(world.get_entrance('Mire Antechamber NW', player), lambda state: state.has('Big Key (Misery Mire)', player))
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player))
@@ -397,15 +379,13 @@ def global_rules(world, player):
set_rule(world.get_entrance('TR Hub NW', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub NE', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player))
set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('TR Dodgers NE', player), lambda state: state.has('Big Key (Turtle Rock)', player))
set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Crystal Maze Cane Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss South Stairs', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Big Key (Turtle Rock)', player))
set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
@@ -426,7 +406,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Hookshot North-South Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
set_rule(world.get_entrance('GT Firesnake Room Hook Path', player), lambda state: state.has('Hookshot', player))
# I am tempted to stick an invincibility rule for getting across falling bridge
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
set_rule(world.get_entrance('GT Ice Armos NE', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state))
set_rule(world.get_entrance('GT Ice Armos WS', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state))
@@ -434,7 +413,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('GT Dash Hall NE', player), lambda state: state.has('Big Key (Ganons Tower)', player))
# consider access to refill room
# consider can_kill_most_things to gauntlet
set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
@@ -442,7 +420,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('GT Moldorm Gap', player), lambda state: state.has('Hookshot', player) and world.get_region('GT Moldorm', player).dungeon.bosses['top'].can_defeat(state))
set_rule(world.get_entrance('GT Brightly Lit Hall NW', player), lambda state: state.has('Big Key (Ganons Tower)', player))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
add_key_logic_rules(world, player)
@@ -1693,6 +1670,14 @@ def add_key_logic_rules(world, player):
forbid_item(location, d_logic.bk_name, player)
for location in d_logic.sm_restricted:
forbid_item(location, d_logic.small_key_name, player)
for door in d_logic.bk_doors:
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
for chest in d_logic.bk_chests:
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
def create_rule(item_name, player):
return lambda state: state.has(item_name, player)
def create_key_rule(small_key_name, player, keys):