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

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