Cross Dungeon initial work

This commit is contained in:
aerinon
2019-12-19 14:48:09 -07:00
parent 0cd665a1fc
commit 35c3a07dc3
9 changed files with 1425 additions and 657 deletions

View File

@@ -229,11 +229,15 @@ class World(object):
if keys: if keys:
for p in range(1, self.players + 1): 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 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)', for item in ItemFactory(key_list, p):
'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):
soft_collect(item) soft_collect(item)
ret.sweep_for_events() ret.sweep_for_events()
return ret return ret
@@ -889,12 +893,30 @@ class Polarity:
def __getitem__(self, item): def __getitem__(self, item):
return self.vector[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): def is_neutral(self):
for i in range(len(self.vector)): for i in range(len(self.vector)):
if self.vector[i] != 0: if self.vector[i] != 0:
return False return False
return True 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 = { pol_idx = {
Direction.North: (0, 'Pos'), Direction.North: (0, 'Pos'),
@@ -918,6 +940,11 @@ pol_add = {
'Add': lambda x, y: x + y, 'Add': lambda x, y: x + y,
'Mod': lambda x, y: (x + y) % 2 'Mod': lambda x, y: (x + y) % 2
} }
pol_comp = {
'Add': lambda x: -x,
'Mod': lambda x: 0 if x == 0 else 1
}
@unique @unique
class CrystalBarrier(Enum): 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.req_event = None # if a dungeon event is required for this door - swamp palace mostly
self.controller = None self.controller = None
self.dependents = [] self.dependents = []
self.dead = False
def getAddress(self): def getAddress(self):
if self.type == DoorType.Normal: if self.type == DoorType.Normal:
@@ -1035,6 +1063,10 @@ class Door(object):
self.crystal = CrystalBarrier.Either self.crystal = CrystalBarrier.Either
return self return self
def kill(self):
self.dead = True
return self
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name return isinstance(other, self.__class__) and self.name == other.name
@@ -1054,6 +1086,19 @@ class Sector(object):
self.regions = [] self.regions = []
self.outstanding_doors = [] self.outstanding_doors = []
self.name = None 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): def polarity(self):
pol = Polarity() pol = Polarity()
@@ -1076,6 +1121,19 @@ class Sector(object):
outflow = outflow + 1 outflow = outflow + 1
return outflow 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): class Boss(object):
def __init__(self, name, enemizer_name, defeat_rule, player): 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)]["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)]["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 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"] = [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 Basement"] = self.world.get_dungeon('Ganons Tower', player).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 Middle"] = self.world.get_dungeon('Ganons Tower', player).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 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"] = "Agahnim 2" self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon" self.bosses[str(player)]["Ganon"] = "Ganon"

View File

@@ -6,13 +6,16 @@ import operator as op
import time import time
from functools import reduce 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 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 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 RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon 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): def link_doors(world, player):
@@ -53,15 +56,15 @@ def link_doors(world, player):
elif world.doorShuffle == 'experimental': elif world.doorShuffle == 'experimental':
experiment(world, player) experiment(world, player)
mark_regions(world, player)
if world.doorShuffle != 'vanilla': if world.doorShuffle != 'vanilla':
create_door_spoiler(world, player) create_door_spoiler(world, player)
# todo: I think this function is not necessary
def mark_regions(world, player): def mark_regions(world, player):
# traverse dungeons and make sure dungeon property is assigned # traverse dungeons and make sure dungeon property is assigned
playerDungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player] player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
for dungeon in playerDungeons: for dungeon in player_dungeons:
queue = collections.deque(dungeon.regions) queue = collections.deque(dungeon.regions)
while len(queue) > 0: while len(queue) > 0:
region = world.get_region(queue.popleft(), player) 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) logger.debug('Door not found in queue: %s connected to %s', door_b.name, door_a.name)
else: else:
logger.warning('Door not connected: %s', door_a.name) 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): def vanilla_key_logic(world, player):
sectors = [] builders = []
for dungeon in [dungeon for dungeon in world.dungeons if dungeon.player == player]: for dungeon in [dungeon for dungeon in world.dungeons if dungeon.player == player]:
sector = Sector() sector = Sector()
sector.name = dungeon.name sector.name = dungeon.name
sector.regions.extend(convert_regions(dungeon.regions, world, player)) 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) overworld_prep(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player)
for sector in sectors: for builder in builders:
start_regions = convert_regions(entrances_map[sector.name], world, player) start_regions = convert_regions(entrances_map[builder.name], world, player)
doors = convert_key_doors(default_small_key_doors[sector.name], world, player) doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(sector, start_regions, doors, world, player) key_layout = build_key_layout(builder, start_regions, doors, world, player)
valid = validate_key_layout_ex(key_layout, world, player) valid = validate_key_layout(key_layout, world, player)
if not valid: 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(): if player not in world.key_logic.keys():
world.key_logic[player] = {} world.key_logic[player] = {}
key_layout = analyze_dungeon(key_layout, world, 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) 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): def within_dungeon(world, player):
fix_big_key_doors_with_ugly_smalls(world, player) fix_big_key_doors_with_ugly_smalls(world, player)
overworld_prep(world, player) overworld_prep(world, player)
dungeon_sectors = []
entrances_map, potentials, connections = determine_entrance_list(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player)
connections_tuple = (entrances_map, potentials, connections)
dungeon_builders = {}
for key in dungeon_regions.keys(): for key in dungeon_regions.keys():
sector_list = convert_to_sectors(dungeon_regions[key], world, player) sector_list = convert_to_sectors(dungeon_regions[key], world, player)
if key in split_region_starts.keys(): dungeon_builders[key] = simple_dungeon_builder(key, sector_list, world, player)
split_sectors = split_up_sectors(sector_list, split_region_starts[key]) dungeon_builders[key].entrance_list = list(entrances_map[key])
for idx, sub_sector_list in enumerate(split_sectors): recombinant_builders = {}
entrance_list = list(split_region_starts[key][idx]) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map)
# shuffable entrances like pinball, left pit need to be added to entrance list main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
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])
paths = determine_required_paths(world) paths = determine_required_paths(world)
check_required_paths(paths, world, player) check_required_paths(paths, world, player)
# shuffle_key_doors for dungeons # shuffle_key_doors for dungeons
start = time.process_time() start = time.process_time()
for sector, entrances in world.dungeon_layouts[player].values(): for builder in world.dungeon_layouts[player].values():
shuffle_key_doors(sector, entrances, world, player) shuffle_key_doors(builder, world, player)
logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start) 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): def determine_entrance_list(world, player):
entrance_map = {} entrance_map = {}
potential_entrances = {} potential_entrances = {}
@@ -347,6 +364,7 @@ def determine_entrance_list(world, player):
return entrance_map, potential_entrances, connections return entrance_map, potential_entrances, connections
# todo: kill drop exceptions
def drop_exception(name): def drop_exception(name):
return name in ['Skull Pot Circle', 'Skull Back Drop'] 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) entrance_list.append(region.name)
origin_reg, origin_dungeon = enabled[region.name] origin_reg, origin_dungeon = enabled[region.name]
if origin_reg != region.name and origin_dungeon != region.dungeon: 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) entrance_map[key].append(region.name)
if drop_exception(region.name): # only because they have unique regions if drop_exception(region.name): # only because they have unique regions
entrance_list.append(region.name) entrance_list.append(region.name)
@@ -593,34 +613,128 @@ def doors_fit_mandatory_pair(pair_list, a, b):
def cross_dungeon(world, player): 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 = [] all_sectors = []
for key in dungeon_regions.keys(): for key in dungeon_regions.keys():
all_sectors.extend(convert_to_sectors(dungeon_regions[key], world, player)) all_sectors.extend(convert_to_sectors(dungeon_regions[key], world, player))
dungeon_split = split_up_sectors(all_sectors, default_dungeon_sets) dungeon_builders = create_dungeon_builders(all_sectors, world, player)
dungeon_sectors = [] for builder in dungeon_builders.values():
for idx, sector_list in enumerate(dungeon_split): builder.entrance_list = list(entrances_map[builder.name])
name = dungeon_x_idx_to_name[idx] dungeon_obj = world.get_dungeon(builder.name, player)
if name in split_region_starts.keys(): for sector in builder.sectors:
split = split_up_sectors(sector_list, split_region_starts[name]) for region in sector.regions:
for sub_idx, sub_sector_list in enumerate(split): region.dungeon = dungeon_obj
dungeon_sectors.append((name, sub_sector_list, split_region_starts[name][sub_idx])) 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: else:
dungeon_sectors.append((name, sector_list, region_starts[name])) logger.info('Cross Dungeon: Increase failed for %s', name)
# todo - adjust dungeon item pools -- ? builder.flex = 0
dungeon_layouts = [] logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining)
# 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))
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: # Re-assign dungeon bosses
shuffle_key_doors(layout[0], layout[1], world, player) 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): def experiment(world, player):
within_dungeon(world, player) cross_dungeon(world, player)
def convert_to_sectors(region_names, world, player): def convert_to_sectors(region_names, world, player):
@@ -628,12 +742,12 @@ def convert_to_sectors(region_names, world, player):
sectors = [] sectors = []
while len(region_list) > 0: while len(region_list) > 0:
region = region_list.pop() region = region_list.pop()
sector = None
new_sector = True new_sector = True
region_chunk = [region] region_chunk = [region]
exits = [] exits = []
exits.extend(region.exits) exits.extend(region.exits)
outstanding_doors = [] outstanding_doors = []
matching_sectors = []
while len(exits) > 0: while len(exits) > 0:
ext = exits.pop() ext = exits.pop()
if ext.connected_region is not None: 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: if connect_region not in region_chunk:
for existing in sectors: for existing in sectors:
if connect_region in existing.regions: if connect_region in existing.regions:
sector = existing
new_sector = False new_sector = False
if existing not in matching_sectors:
matching_sectors.append(existing)
else: else:
door = world.check_for_door(ext.name, player) door = world.check_for_door(ext.name, player)
if door is not None and door.controller is None and door.dest is None: if door is not None and door.controller is None and door.dest is None:
outstanding_doors.append(door) outstanding_doors.append(door)
if new_sector:
sector = 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.regions.extend(region_chunk)
sector.outstanding_doors.extend(outstanding_doors) sector.outstanding_doors.extend(outstanding_doors)
if new_sector:
sectors.append(sector) sectors.append(sector)
return sectors return sectors
# those with split region starts like Desert/Skull combine for key layouts # those with split region starts like Desert/Skull combine for key layouts
def combine_layouts(dungeon_layouts, entrances_map): def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
combined = {} for recombine in recombinant_builders.values():
queue = collections.deque(dungeon_layouts) queue = collections.deque(dungeon_builders.values())
while len(queue) > 0: while len(queue) > 0:
sector, entrance_list = queue.pop() builder = queue.pop()
if sector.name in split_region_starts: if builder.name.startswith(recombine.name):
dungeon_layouts.remove((sector, entrance_list)) del dungeon_builders[builder.name]
# desert_entrances.extend(entrance_list) if recombine.master_sector is None:
if sector.name not in combined: recombine.master_sector = builder.master_sector
combined[sector.name] = sector recombine.master_sector.name = recombine.name
else: else:
combined[sector.name].regions.extend(sector.regions) recombine.master_sector.regions.extend(builder.master_sector.regions)
for key in combined.keys(): recombine.layout_starts = list(entrances_map[recombine.name])
dungeon_layouts.append((combined[key], list(entrances_map[key]))) dungeon_builders[recombine.name] = recombine
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
def valid_region_to_explore(region, world, player): def valid_region_to_explore(region, world, player):
return region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player] return region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player]
def shuffle_key_doors(dungeon_sector, entrances, world, player): def shuffle_key_doors(builder, world, player):
logger = logging.getLogger('') start_regions = convert_regions(builder.path_entrances, world, player)
logger.info('Shuffling Key doors for %s', dungeon_sector.name)
start_regions = convert_regions(entrances, world, player)
# count number of key doors - this could be a table? # count number of key doors - this could be a table?
num_key_doors = 0 num_key_doors = 0
current_doors = []
skips = [] skips = []
for region in dungeon_sector.regions: for region in builder.master_sector.regions:
for ext in region.exits: for ext in region.exits:
d = world.check_for_door(ext.name, player) d = world.check_for_door(ext.name, player)
if d is not None and d.smallKey: if d is not None and d.smallKey:
current_doors.append(d)
if d not in skips: if d not in skips:
if d.type == DoorType.Interior: if d.type == DoorType.Interior:
skips.append(d.dest) 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)) skips.append(world.get_door(dp.door_a, player))
break break
num_key_doors += 1 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 # traverse dungeon and find candidates
candidates = [] candidates = []
checked_doors = set() 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: if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates:
flat_candidates.append(candidate) flat_candidates.append(candidate)
# find valid combination of candidates
paired_candidates = build_pair_list(flat_candidates) paired_candidates = build_pair_list(flat_candidates)
if len(paired_candidates) < num_key_doors: builder.candidates = paired_candidates
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) 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 itr = 0
start = time.process_time() start = time.process_time()
sample_list = list(range(0, int(combinations))) sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list) 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) key_layout = build_key_layout(builder, start_regions, proposal, world, player)
while not validate_key_layout_ex(key_layout, world, player): while not validate_key_layout(key_layout, world, player):
itr += 1 itr += 1
if itr >= combinations: stop_early = False
logger.info('Lowering key door count because no valid layouts: %s', dungeon_sector.name) if itr % 1000 == 0:
num_key_doors -= 1 mark = time.process_time()-start
if num_key_doors < 0: if (mark > 10 and itr*100/combinations > 50) or (mark > 20 and itr*100/combinations > 25) or mark > 30:
raise Exception('Bad dungeon %s - 0 key doors not valid' % dungeon_sector.name) stop_early = True
combinations = ncr(len(paired_candidates), num_key_doors) 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))) sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list) random.shuffle(sample_list)
itr = 0 itr = 0
proposal = kth_combination(sample_list[itr], paired_candidates, num_key_doors) start = time.process_time() # reset time since itr reset
key_layout.reset(proposal) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
key_layout.reset(proposal, builder, world, player)
if (itr+1) % 1000 == 0: if (itr+1) % 1000 == 0:
mark = time.process_time()-start mark = time.process_time()-start
logger.info('%s time elapsed. %s iterations/s', mark, itr/mark) logger.info('%s time elapsed. %s iterations/s', mark, itr/mark)
# make changes # make changes
if player not in world.key_logic.keys(): if player not in world.key_logic.keys():
world.key_logic[player] = {} world.key_logic[player] = {}
key_layout_new = analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
reassign_key_doors(current_doors, proposal, world, player) reassign_key_doors(builder, proposal, world, player)
log_key_logic(dungeon_sector.name, key_layout_new.key_logic) log_key_logic(builder.name, key_layout.key_logic)
world.key_logic[player][dungeon_sector.name] = key_layout_new.key_logic world.key_logic[player][builder.name] = key_layout.key_logic
return True
def log_key_logic(d_name, key_logic): def log_key_logic(d_name, key_logic):
@@ -910,7 +943,7 @@ def build_pair_list(flat_list):
queue = collections.deque(flat_list) queue = collections.deque(flat_list)
while len(queue) > 0: while len(queue) > 0:
d = queue.pop() d = queue.pop()
if d.dest in queue: if d.dest in queue and d.type != DoorType.SpiralStairs:
paired_list.append((d, d.dest)) paired_list.append((d, d.dest))
queue.remove(d.dest) queue.remove(d.dest)
else: else:
@@ -989,10 +1022,10 @@ def ncr(n, r):
return numerator / denominator return numerator / denominator
def reassign_key_doors(current_doors, proposal, world, player): def reassign_key_doors(builder, proposal, world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
flat_proposal = flatten_pair_list(proposal) 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: while len(queue) > 0:
d = queue.pop() d = queue.pop()
if d.type is DoorType.SpiralStairs and d not in proposal: if d.type is DoorType.SpiralStairs and d not in proposal:
@@ -1082,7 +1115,7 @@ def determine_required_paths(world):
paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock') paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock')
# noinspection PyTypeChecker # noinspection PyTypeChecker
paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary')) 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') paths['Thieves Town'].append('Thieves Attic Window')
return paths return paths
@@ -1164,19 +1197,19 @@ def create_door(world, player, entName, region_name):
def check_required_paths(paths, world, player): def check_required_paths(paths, world, player):
for dungeon_name in paths.keys(): 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: if len(paths[dungeon_name]) > 0:
states_to_explore = defaultdict(list) states_to_explore = defaultdict(list)
for path in paths[dungeon_name]: for path in paths[dungeon_name]:
if type(path) is tuple: if type(path) is tuple:
states_to_explore[tuple([path[0]])].append(path[1]) states_to_explore[tuple([path[0]])].append(path[1])
else: else:
states_to_explore[tuple(entrances)].append(path) states_to_explore[tuple(builder.path_entrances)].append(path)
cached_initial_state = None cached_initial_state = None
for start_regs, dest_regs in states_to_explore.items(): for start_regs, dest_regs in states_to_explore.items():
check_paths = convert_regions(dest_regs, world, player) check_paths = convert_regions(dest_regs, world, player)
start_regions = convert_regions(start_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: if not initial or cached_initial_state is None:
init = determine_init_crystal(initial, cached_initial_state, start_regions) init = determine_init_crystal(initial, cached_initial_state, start_regions)
state = ExplorationState(init, dungeon_name) 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 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 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 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 Hint Tile Push Block', Lgcl),
create_door(player, 'Eastern Courtyard WN', Nrml).dir(We, 0xa9, Top, Low).pos(3), 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), 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 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 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 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 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 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(), 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 Pothole Chain', Hole),
create_door(player, 'Hera 5F Normal Holes', Hole), create_door(player, 'Hera 5F Normal Holes', Hole),
create_door(player, 'Hera Fairies\' Warp', Warp), 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 Outer Hole', Hole),
create_door(player, 'Hera Boss Inner 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 Freefall', Hole),
create_door(player, 'PoD Pit Room Bomb Hole', 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 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 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 Basement Ledge Drop Down', Lgcl),
create_door(player, 'PoD Stalfos Basement Warp', Warp), 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 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 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 NE', Nrml).dir(No, 0x2a, Right, High).no_exit().trap(0x4).pos(0),
create_door(player, 'PoD Arena Main Crystal Path', Lgcl), create_door(player, 'PoD Arena Main Crystal Path', Lgcl),
@@ -365,7 +365,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 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 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 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 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 SW', Intr).dir(So, 0x1b, Left, High).pos(0),
create_door(player, 'PoD Bow Statue Down Ladder', Lddr).no_entrance(), create_door(player, 'PoD Bow Statue Down Ladder', Lddr).no_entrance(),
@@ -443,7 +443,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 EN', Nrml).dir(Ea, 0x34, Top, High).pos(0),
create_door(player, 'Swamp Barrier - Orange', Lgcl), create_door(player, 'Swamp Barrier - Orange', Lgcl),
create_door(player, 'Swamp Barrier Ledge Hook Path', 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 Left Pit', Hole),
create_door(player, 'Swamp Attic Right 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), create_door(player, 'Swamp Push Statue S', Nrml).dir(So, 0x26, Mid, High).small_key().pos(0),
@@ -459,7 +459,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 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 WN', Intr).dir(We, 0x76, Top, Low).pos(0),
create_door(player, 'Swamp Drain Right Switch', Lgcl), 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 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 Room WS', Intr).dir(We, 0x76, Bot, Low).pos(1),
create_door(player, 'Swamp Flooded Spot Ladder', Lgcl), create_door(player, 'Swamp Flooded Spot Ladder', Lgcl),
@@ -648,7 +648,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 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 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 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 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 WS', Intr).dir(We, 0x3f, Bot, High).pos(0),
create_door(player, 'Ice Tongue Pull Up Ladder', Lddr), create_door(player, 'Ice Tongue Pull Up Ladder', Lddr),
@@ -660,7 +660,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 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 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 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 Ledge Path', Lgcl),
create_door(player, 'Ice Hookshot Balcony 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), create_door(player, 'Ice Hookshot Balcony SW', Intr).dir(So, 0x7f, Left, High).pos(1),
@@ -786,7 +786,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 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 ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0),
create_door(player, 'Mire Warping Pool Warp', Warp), 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 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 NW', Intr).dir(No, 0x97, Left, High).pos(1),
create_door(player, 'Mire Torches Bottom WS', Intr).dir(We, 0x97, Bot, High).pos(0), create_door(player, 'Mire Torches Bottom WS', Intr).dir(We, 0x97, Bot, High).pos(0),
@@ -900,10 +900,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 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 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 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 Blocked Stairs Block Path', Lgcl),
create_door(player, 'GT Big Chest SW', Nrml).dir(So, 0x8c, Left, High).pos(4), 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 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 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), create_door(player, 'GT Tile Room EN', Intr).dir(Ea, 0x8d, Top, High).small_key().pos(1),
@@ -912,7 +912,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 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 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 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 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 EN', Intr).dir(Ea, 0x9d, Top, High).pos(2),
create_door(player, 'GT Compass Room Warp', Warp), create_door(player, 'GT Compass Room Warp', Warp),
@@ -936,7 +936,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 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 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 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', Lgcl),
create_door(player, 'GT Double Switch Orange Barrier 2', Lgcl), create_door(player, 'GT Double Switch Orange Barrier 2', Lgcl),
create_door(player, 'GT Double Switch Blue Path', Lgcl), create_door(player, 'GT Double Switch Blue Path', Lgcl),
@@ -971,7 +971,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 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 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 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 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 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), create_door(player, 'GT Falling Bridge WN', Nrml).dir(We, 0x7c, Top, High).small_key().pos(2),
@@ -1008,7 +1008,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 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 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 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 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 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1), create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1),
@@ -1035,14 +1035,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 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 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 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 Left Moldorm Ledge Drop Down', Lgcl),
create_door(player, 'GT Right 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 Gap', Lgcl),
create_door(player, 'GT Moldorm Hole', Hole), create_door(player, 'GT Moldorm Hole', Hole),
create_door(player, 'GT Validation Block Path', Lgcl), 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 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 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 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), 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 collections import defaultdict
from enum import Enum, unique from enum import Enum, unique
import logging 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 Regions import key_only_locations, dungeon_events, flooded_keys_reverse
from Dungeons import dungeon_regions
@unique @unique
@@ -33,11 +37,13 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
doors_to_connect = set() doors_to_connect = set()
all_regions = set() all_regions = set()
bk_needed = False bk_needed = False
bk_special = False
for sector in available_sectors: for sector in available_sectors:
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect.add(door) doors_to_connect.add(door)
all_regions.update(sector.regions) all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) 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 = {} proposed_map = {}
choices_master = [[]] choices_master = [[]]
depth = 0 depth = 0
@@ -50,9 +56,10 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
# what are my choices? # what are my choices?
itr += 1 itr += 1
if itr > 5000: 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(): 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 dungeon_cache[depth] = dungeon, hangers, hooks
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed) valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed)
else: else:
@@ -83,7 +90,7 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
dungeon_cache.pop(depth, None) dungeon_cache.pop(depth, None)
depth -= 1 depth -= 1
if depth < 0: 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] a, b = choices_master[depth][-1]
logger.debug(' ' * depth + "%d: Rescinding %s, %s", depth, a.name, b.name) logger.debug(' ' * depth + "%d: Rescinding %s, %s", depth, a.name, b.name)
proposed_map.pop(a, None) 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() master_sector = available_sectors.pop()
for sub_sector in available_sectors: for sub_sector in available_sectors:
master_sector.regions.extend(sub_sector.regions) master_sector.regions.extend(sub_sector.regions)
master_sector.outstanding_doors.clear()
master_sector.r_name_set = None
return master_sector return master_sector
@@ -109,38 +118,46 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
return False 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> # step 1 create dungeon: Dict<DoorName|Origin, GraphPiece>
dungeon = {} 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) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
doors_to_connect = set()
hanger_set = set() hanger_set = set()
o_state_cache = {} o_state_cache = {}
for sector in available_sectors: for sector in available_sectors:
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect.add(door)
if not door.stonewall and door not in proposed_map.keys(): if not door.stonewall and door not in proposed_map.keys():
hanger_set.add(door) hanger_set.add(door)
parent = parent_region(door, world, player).parent_region 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 o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map) piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map)
dungeon[door.name] = piece dungeon[door.name] = piece
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player) check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player)
# catalog hooks: Dict<Hook, Set<Door, Crystal, Door>> # catalog hooks: Dict<Hook, List<Door, Crystal, Door>>
# and hangers: Dict<Hang, Set<Door>> # and hangers: Dict<Hang, List<Door>>
avail_hooks = defaultdict(set) avail_hooks = defaultdict(list)
hangers = defaultdict(set) hangers = defaultdict(list)
for key, piece in dungeon.items(): for key, piece in dungeon.items():
door_hang = piece.hanger_info door_hang = piece.hanger_info
if door_hang is not None: if door_hang is not None:
hanger = hanger_from_door(door_hang) hanger = hanger_from_door(door_hang)
hangers[hanger].add(door_hang) hangers[hanger].append(door_hang)
for door, crystal in piece.hooks.items(): for door, crystal in piece.hooks.items():
hook = hook_from_door(door) 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 # thin out invalid hanger
winnow_hangers(hangers, avail_hooks) 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): def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
parent = parent_region(door, world, player).parent_region parent = parent_region(door, world, player).parent_region
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon) 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) 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) 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): def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
# choose a hanger # choose a hanger
all_hooks = set() all_hooks = {}
origin = dungeon['Origin'] origin = dungeon['Origin']
for key in avail_hooks.keys(): for key in avail_hooks.keys():
for hstuff in avail_hooks[key]: for hstuff in avail_hooks[key]:
all_hooks.add(hstuff[0]) all_hooks[hstuff[0]] = None
candidate_hangers = [] candidate_hangers = []
for key in hangers.keys(): for key in hangers.keys():
candidate_hangers.extend(hangers[key]) candidate_hangers.extend(hangers[key])
candidate_hangers.sort(key=lambda x: x.name) # sorting to create predictable seeds candidate_hangers.sort(key=lambda x: x.name) # sorting to create predictable seeds
random.shuffle(candidate_hangers) # randomize if equal preference random.shuffle(candidate_hangers) # randomize if equal preference
stage_2_hangers = [] 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() hookable_hangers = collections.deque()
queue = collections.deque(candidate_hangers) queue = collections.deque(candidate_hangers)
while len(queue) > 0: while len(queue) > 0:
c_hang = queue.pop() c_hang = queue.pop()
if c_hang in all_hooks: if c_hang in all_hooks.keys():
hookable_hangers.append(c_hang) hookable_hangers.append(c_hang)
else: else:
stage_2_hangers.append(c_hang) # prefer hangers that are not hooks 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] 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): if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
return False 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 return False
for key in hangers.keys(): for key in hangers.keys():
if len(hooks[key]) > 0 and len(hangers[key]) == 0: 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 all_unattached[d] = CrystalBarrier.Null
elif all_unattached[d] == CrystalBarrier.Blue and exp_d.crystal == CrystalBarrier.Orange: elif all_unattached[d] == CrystalBarrier.Blue and exp_d.crystal == CrystalBarrier.Orange:
# the swapping case # 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: elif all_unattached[d] == CrystalBarrier.Either:
all_unattached[d] = exp_d.crystal # pessimism, and if not this, leave it alone all_unattached[d] = exp_d.crystal # pessimism, and if not this, leave it alone
else: 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): 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): def parent_region(door, world, player):
@@ -576,9 +603,9 @@ class ExplorationState(object):
if region.type == RegionType.Dungeon: if region.type == RegionType.Dungeon:
for location in region.locations: for location in region.locations:
if key_checks and location not in self.found_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 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 self.ttl_locations += 1
if location not in self.found_locations: if location not in self.found_locations:
self.found_locations.append(location) 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 location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location): if self.flooded_key_check(location):
self.perform_event(location.name, key_region) 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) 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: if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened:
self.big_key_opened = True self.big_key_opened = True
@@ -625,7 +653,8 @@ class ExplorationState(object):
if self.can_traverse(door): if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): 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) 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) self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_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 self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): 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) 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) self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_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) other = self.find_door_in_list(door, self.unattached_doors)
if self.crystal != other.crystal: if self.crystal != other.crystal:
other.crystal = CrystalBarrier.Either 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) self.append_door_to_list(door, self.event_doors, flag)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors, flag) 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): def add_all_doors_check_key_region(self, region, key_region, world, player):
for door in get_doors(world, region, player): for door in get_doors(world, region, player):
if self.can_traverse(door): 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) self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors) self.append_door_to_list(door, self.avail_doors)
@@ -724,7 +756,8 @@ class ExplorationState(object):
return cnt return cnt
def validate(self, door, region, world, player): 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): def in_door_list(self, door, door_list):
for d in 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() explorable_door = local_state.next_avail_door()
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None: 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.visit_region(connect_region)
local_state.add_all_doors_check_unattached(connect_region, world, player) local_state.add_all_doors_check_unattached(connect_region, world, player)
return local_state 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: while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door() explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey: 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 continue # we can't open this door
if explorable_door.door in proposed_map: if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else: else:
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None: 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 flag = explorable_door.flag or explorable_door.door.bigKey
local_state.visit_region(connect_region, bk_Flag=flag) 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) local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player)
return local_state 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 # cross-utility methods
def valid_region_to_explore(region, name, world, player): def valid_region_to_explore(region, name, world, player):
if region is None: if region is None:
@@ -852,3 +894,713 @@ def convert_regions(region_names, world, player):
for name in region_names: for name in region_names:
region_list.append(world.get_region(name, player)) region_list.append(world.get_region(name, player))
return region_list 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 = { split_region_starts = {
'Desert Palace': [ 'Desert Palace': {
['Desert Back Lobby'], 'Back': ['Desert Back Lobby'],
['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'] 'Main': ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby']
], },
'Skull Woods': [ 'Skull Woods': {
['Skull 1 Lobby', 'Skull Pot Circle'], '1': ['Skull 1 Lobby', 'Skull Pot Circle'],
['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'], '2': ['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'],
['Skull 3 Lobby'] '3': ['Skull 3 Lobby']
] }
} }
flexible_starts = { 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'), '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'), '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'), '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'), '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'), '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'), '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 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): class KeyLayout(object):
def __init__(self, sector, starts, proposal): def __init__(self, sector, starts, proposal):
@@ -45,20 +14,21 @@ class KeyLayout(object):
self.proposal = proposal self.proposal = proposal
self.key_logic = KeyLogic(sector.name) self.key_logic = KeyLogic(sector.name)
self.key_spheres = None
self.key_counters = None self.key_counters = None
self.flat_prop = None self.flat_prop = None
self.max_chests = None self.max_chests = None
self.max_drops = None self.max_drops = None
self.all_chest_locations = {} self.all_chest_locations = {}
self.big_key_special = False
# bk special? # bk special?
# bk required? True if big chests or big doors exists # 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.proposal = proposal
self.flat_prop = flatten_pair_list(self.proposal) self.flat_prop = flatten_pair_list(self.proposal)
self.key_logic = KeyLogic(self.sector.name) self.key_logic = KeyLogic(self.sector.name)
self.max_chests = calc_max_chests(builder, self, world, player)
class KeyLogic(object): class KeyLogic(object):
@@ -69,6 +39,8 @@ class KeyLogic(object):
self.sm_restricted = set() self.sm_restricted = set()
self.small_key_name = dungeon_keys[dungeon_name] self.small_key_name = dungeon_keys[dungeon_name]
self.bk_name = dungeon_bigs[dungeon_name] self.bk_name = dungeon_bigs[dungeon_name]
self.bk_doors = set()
self.bk_chests = set()
self.logic_min = {} self.logic_min = {}
self.logic_max = {} self.logic_max = {}
@@ -96,34 +68,7 @@ class KeyCounter(object):
self.used_keys = 0 self.used_keys = 0
self.big_key_opened = False self.big_key_opened = False
self.important_location = False self.important_location = False
self.other_locations = {}
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
def used_smalls_loc(self, reserve=0): def used_smalls_loc(self, reserve=0):
return max(self.used_keys + reserve - len(self.key_only_locations), 0) return max(self.used_keys + reserve - len(self.key_only_locations), 0)
@@ -140,29 +85,35 @@ class KeyCounter(object):
return ret return ret
def build_key_layout(sector, start_regions, proposal, world, player): def build_key_layout(builder, start_regions, proposal, world, player):
key_layout = KeyLayout(sector, start_regions, proposal) key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
key_layout.flat_prop = flatten_pair_list(key_layout.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_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 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): def analyze_dungeon(key_layout, world, player):
key_layout.key_counters = create_key_counters(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 key_logic = key_layout.key_logic
find_bk_locked_sections(key_layout, world) 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()) original_key_counter = find_counter({}, False, key_layout)
key_counter = key_layout.key_counters[counter_id({}, init_bk, key_layout.flat_prop)] queue = collections.deque([(None, original_key_counter)])
queue = collections.deque([(key_layout.key_spheres['Origin'], key_counter)])
doors_completed = set() doors_completed = set()
while len(queue) > 0: while len(queue) > 0:
queue = collections.deque(sorted(queue, key=queue_sorter)) 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) chest_keys = available_chest_small_keys(key_counter, world)
raw_avail = chest_keys + len(key_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys 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 not key_counter.big_key_opened:
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls: 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)) 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)) key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
# todo: detect forced subsequent keys - see keypuzzles # todo: detect forced subsequent keys - see keypuzzles
# try to relax the rules here? - smallest requirement that doesn't force a softlock # try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = collections.deque() child_queue = collections.deque()
for child in sorted(list(key_sphere.child_doors), key=lambda x: x.name): for child in key_counter.child_doors.keys():
next_sphere = key_layout.key_spheres[child.name] if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
# todo: empty_sphere are not always empty, Mire spike barrier is not empty if other doors open first odd_counter = create_odd_key_counter(child, key_counter, key_layout, world)
if not empty_sphere(next_sphere) and child not in doors_completed: if not empty_counter(odd_counter) and child not in doors_completed:
child_queue.append((child, next_sphere)) child_queue.append((child, odd_counter))
while len(child_queue) > 0: while len(child_queue) > 0:
child, next_sphere = child_queue.popleft() child, odd_counter = child_queue.popleft()
if not child.bigKey: 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) rule = create_rule(best_counter, key_counter, key_layout, world)
check_for_self_lock_key(rule, next_sphere, key_layout, world) check_for_self_lock_key(rule, child, best_counter, key_layout, world)
bk_restricted_rules(rule, next_sphere, key_counter, key_layout, world) bk_restricted_rules(rule, child, odd_counter, key_counter, key_layout, world)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
doors_completed.add(next_sphere.access_door) doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, next_sphere, key_layout) next_counter = find_next_counter(child, key_counter, key_layout)
queue.append((next_sphere, next_counter)) queue.append((child, next_counter))
check_rules(key_layout) check_rules(original_key_counter, key_layout)
return key_layout
def count_key_drops(sector): def count_key_drops(sector):
@@ -205,43 +155,42 @@ def count_key_drops(sector):
def queue_sorter(queue_item): def queue_sorter(queue_item):
sphere, counter = queue_item door, counter = queue_item
if sphere.access_door is None: if door is None:
return 0 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): 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 key_logic = key_layout.key_logic
bk_key_not_required = set() bk_key_not_required = set()
big_chest_allowed_big_key = world.accessibility != 'locations' big_chest_allowed_big_key = world.accessibility != 'locations'
for key in key_spheres.keys(): for counter in key_counters.values():
sphere = key_spheres[key] key_layout.all_chest_locations.update(counter.free_locations)
key_layout.all_chest_locations.update(sphere.free_locations) if counter.big_key_opened and counter.important_location:
if sphere.bk_locked and (sphere.prize_region or KeyCounter.special_region(sphere)):
big_chest_allowed_big_key = False big_chest_allowed_big_key = False
if not sphere.bk_locked: if not counter.big_key_opened:
bk_key_not_required.update(sphere.free_locations) 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))) 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: if not big_chest_allowed_big_key:
key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations))
def empty_sphere(sphere): def empty_counter(counter):
if len(sphere.key_only_locations) != 0 or len(sphere.free_locations) != 0 or len(sphere.child_doors) != 0: if len(counter.key_only_locations) != 0 or len(counter.free_locations) != 0 or len(counter.child_doors) != 0:
return False return False
return not sphere.prize_region return not counter.important_location
def relative_empty_sphere(sphere, key_counter): def relative_empty_counter(odd_counter, key_counter):
if len(set(sphere.key_only_locations).difference(key_counter.key_only_locations)) > 0: if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False 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 return False
new_child_door = 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): if unique_child_door(child, key_counter):
new_child_door = True new_child_door = True
break break
@@ -260,23 +209,15 @@ def unique_child_door(child, key_counter):
return True return True
def increment_key_counter(door, sphere, key_counter, flat_proposal): def find_best_counter(door, odd_counter, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible?
new_counter = key_counter.copy() ignored_doors = {door, door.dest} if door is not None else {}
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}
finished = False finished = False
opened_doors = dict(key_counter.open_doors) opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened bk_opened = key_counter.big_key_opened
# new_counter = key_counter # new_counter = key_counter
last_counter = key_counter last_counter = key_counter
while not finished: 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: if door_set is None or len(door_set) == 0:
finished = True finished = True
continue 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) new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened bk_open = new_counter.big_key_opened
# this means the new_door invalidates the door / leads to the same stuff # 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) ignored_doors.add(new_door)
else: else:
if not key_wasted(new_door, last_counter, new_counter, key_layout, world): 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 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 = [] small_doors = []
big_doors = [] big_doors = []
for other in key_counter.child_doors: for other in key_counter.child_doors:
if other not in ignored_doors and other.dest not in ignored_doors: if other not in ignored_doors and other.dest not in ignored_doors:
if other.bigKey: 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) big_doors.append(other)
elif other.dest not in small_doors: elif other.dest not in small_doors:
small_doors.append(other) 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 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)): if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
return None return None
@@ -338,10 +282,10 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world):
return False 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])} 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) bk_open = old_counter.big_key_opened or new_door.bigKey
return key_layout.key_counters[counter_id(proposed_doors, bk_open, key_layout.flat_prop)] return find_counter(proposed_doors, bk_open, key_layout)
def check_special_locations(locations): 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): def create_rule(key_counter, prev_counter, key_layout, world):
prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_chest_keys = available_chest_small_keys(prev_counter, world)
prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world) chest_keys = available_chest_small_keys(key_counter, world)
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations) key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
raw_avail = chest_keys + len(key_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) 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': if world.accessibility != 'locations':
counter = KeyCounter(key_layout.max_chests) counter = find_inverted_counter(door, parent_counter, key_layout, world)
counter.update(sphere)
if not self_lock_possible(counter): if not self_lock_possible(counter):
return 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: if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
rule.allow_small = True rule.allow_small = True
rule.small_location = next(iter(counter.free_locations)) 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): def self_lock_possible(counter):
return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location 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 return key_counter.max_chests
def bk_restricted_rules(rule, sphere, key_counter, key_layout, world): def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world):
if sphere.bk_locked: if key_counter.big_key_opened:
return 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 bk_number = create_rule(best_counter, key_counter, key_layout, world).small_key_num
if bk_number == rule.small_key_num: if bk_number == rule.small_key_num:
return return
post_counter = KeyCounter(key_layout.max_chests) door_open = find_next_counter(door, best_counter, key_layout)
post_counter.update(sphere) ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
other_doors_beyond_me = [x for x in post_counter.child_doors if not x.bigKey] dest_ignored = []
queue = collections.deque(other_doors_beyond_me) for door in ignored_doors.keys():
already_queued = set(other_doors_beyond_me) if door.dest not in ignored_doors:
while len(queue) > 0: dest_ignored.append(door.dest)
child = queue.popleft() ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
if child not in post_counter.open_doors: post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
post_counter = increment_key_counter(child, key_layout.key_spheres[child.name], post_counter, key_layout.flat_prop) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
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))
if len(unique_loc) > 0: if len(unique_loc) > 0:
rule.alternate_small_key = bk_number rule.alternate_small_key = bk_number
rule.alternate_big_key_loc.update(unique_loc) 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): def open_a_door(door, child_state, flat_proposal):
if door.bigKey: if door.bigKey:
child_state.big_key_opened = True child_state.big_key_opened = True
@@ -610,7 +494,7 @@ def filter_big_chest(locations):
def count_locations_exclude_big_chest(state): def count_locations_exclude_big_chest(state):
cnt = 0 cnt = 0
for loc in state.found_locations: 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 cnt += 1
return cnt return cnt
@@ -648,21 +532,33 @@ def flatten_pair_list(paired_list):
return flat_list return flat_list
def check_rules(key_layout): def check_rules(original_counter, key_layout):
all_key_only = set() all_key_only = set()
key_only_map = {} key_only_map = {}
for sphere in key_layout.key_spheres.values(): queue = collections.deque([(None, original_counter)])
for loc in sphere.key_only_locations: 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: if loc not in all_key_only:
all_key_only.add(loc) all_key_only.add(loc)
access_rules = [] access_rules = []
key_only_map[loc] = access_rules key_only_map[loc] = access_rules
else: else:
access_rules = key_only_map[loc] 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)) access_rules.append(DoorRules(0))
else: 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_bk = defaultdict(list)
min_rule_non_bk = defaultdict(list) min_rule_non_bk = defaultdict(list)
check_non_bk = False check_non_bk = False
@@ -712,30 +608,26 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
# Soft lock stuff # Soft lock stuff
def validate_key_layout_ex(key_layout, world, player): def validate_key_layout(key_layout, world, player):
return validate_key_layout_main_loop(key_layout, world, player)
def validate_key_layout_main_loop(key_layout, world, player):
flat_proposal = key_layout.flat_prop flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name) 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 state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
for region in key_layout.start_regions: for region in key_layout.start_regions:
state.visit_region(region, key_checks=True) state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player) 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) 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 num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing
if not smalls_avail and num_bigs == 0: if not smalls_avail and num_bigs == 0:
return True # I think that's the end 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) 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_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world)
available_big_locations = ttl_locations - state.used_locations if not state.big_key_special else 0 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): 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 return False
else: else:
@@ -747,7 +639,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
state_copy.used_smalls += 1 state_copy.used_smalls += 1
code = state_id(state_copy, flat_proposal) code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys(): 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 checked_states[code] = valid
else: else:
valid = checked_states[code] 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 state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal) code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys(): 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 checked_states[code] = valid
else: else:
valid = checked_states[code] valid = checked_states[code]
@@ -768,6 +660,18 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
return True 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): def create_key_counters(key_layout, world, player):
key_counters = {} key_counters = {}
flat_proposal = key_layout.flat_prop 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) state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop) 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)]) queue = collections.deque([(key_counters[code], state)])
while len(queue) > 0: while len(queue) > 0:
next_key_sphere, parent_state = queue.popleft() next_key_counter, parent_state = queue.popleft()
for door in next_key_sphere.child_doors: for door in next_key_counter.child_doors:
child_state = parent_state.copy() 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) open_a_door(door, child_state, flat_proposal)
expand_key_state(child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop) code = state_id(child_state, key_layout.flat_prop)
if code not in key_counters.keys(): 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 key_counters[code] = child_kr
queue.append((child_kr, child_state)) queue.append((child_kr, child_state))
return key_counters 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 = KeyCounter(key_layout.max_chests)
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
for loc in state.found_locations: for loc in state.found_locations:
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: if important_location(loc, world):
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']:
key_counter.important_location = True key_counter.important_location = True
key_counter.other_locations[loc] = None
elif loc.event and 'Small Key' in loc.item.name: elif loc.event and 'Small Key' in loc.item.name:
key_counter.key_only_locations[loc] = None key_counter.key_only_locations[loc] = None
elif loc.name not in dungeon_events: elif loc.name not in dungeon_events:
key_counter.free_locations[loc] = None 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.open_doors.update(dict.fromkeys(state.opened_doors))
key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
if state.big_key_special: if state.big_key_special:
@@ -823,6 +730,34 @@ def create_key_counter_x(state, key_layout, world, player):
return key_counter 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): def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0' s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal: for d in flat_proposal:
@@ -857,6 +792,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
return None 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): def counter_id(opened_doors, bk_unlocked, flat_proposal):
s_id = '1' if bk_unlocked else '0' s_id = '1' if bk_unlocked else '0'
for d in flat_proposal: for d in flat_proposal:
@@ -864,6 +803,10 @@ def counter_id(opened_doors, bk_unlocked, flat_proposal):
return s_id 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): # class SoftLockException(Exception):
# pass # pass
@@ -900,7 +843,7 @@ def val_hyrule(key_logic, world, player):
def val_eastern(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) 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 - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Eastern Palace - Boss', 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): def val_mire(key_logic, world, player):
mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'} 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 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) 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 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 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 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 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']), 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 - Map Guard Key Drop': 'Small Key (Escape)',
'Hyrule Castle - Boomerang 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 - 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 Square Pot Key': 'Small Key (Eastern Palace)',
'Eastern Palace - Dark Eyegore Key Drop': '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)', 'Desert Palace - Desert Tiles 1 Pot Key': 'Small Key (Desert Palace)',

View File

@@ -263,26 +263,19 @@ 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 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 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)) 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. # 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 - Prize', player))
set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Boss', player))
# Desert # 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_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)) set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player))
set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player))
set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player))
# Tower of Hera # 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_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 - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
@@ -291,8 +284,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 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 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 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 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 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)) set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player))
@@ -327,20 +318,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 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_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 - 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 - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', 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_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 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_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 - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', 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 # 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']: 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)) 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']: for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
@@ -353,7 +340,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_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_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_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 - 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)) set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
@@ -374,11 +360,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 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 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_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_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 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 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)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player))
@@ -395,15 +377,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 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 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_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 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 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 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 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 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 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 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 - 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)) 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))
@@ -423,7 +403,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 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)) 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 # 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 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)) 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))
@@ -431,7 +410,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 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 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 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 access to refill room
# consider can_kill_most_things to gauntlet # 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)) 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))
@@ -439,7 +417,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 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 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 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)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
add_key_logic_rules(world, player) add_key_logic_rules(world, player)
@@ -1690,6 +1667,14 @@ def add_key_logic_rules(world, player):
forbid_item(location, d_logic.bk_name, player) forbid_item(location, d_logic.bk_name, player)
for location in d_logic.sm_restricted: for location in d_logic.sm_restricted:
forbid_item(location, d_logic.small_key_name, player) 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): def create_key_rule(small_key_name, player, keys):