diff --git a/BaseClasses.py b/BaseClasses.py index ce559ac2..e51a4cca 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1234,6 +1234,7 @@ class Sector(object): self.name = None self.r_name_set = None self.chest_locations = 0 + self.big_chest_present = False self.key_only_locations = 0 self.c_switch = False self.orange_barrier = False @@ -1242,6 +1243,7 @@ class Sector(object): self.bk_provided = False self.conn_balance = None self.branch_factor = None + self.dead_end_cnt = None self.entrance_sector = None self.equations = None @@ -1281,6 +1283,9 @@ class Sector(object): def branching_factor(self): if self.branch_factor is None: self.branch_factor = len(self.outstanding_doors) + cnt_dead = len([x for x in self.outstanding_doors if x.dead]) + if cnt_dead > 1: + self.branch_factor -= cnt_dead - 1 for region in self.regions: for ent in region.entrances: if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: @@ -1289,6 +1294,18 @@ class Sector(object): self.branch_factor += 1 return self.branch_factor + def branches(self): + return max(0, self.branching_factor() - 2) + + def dead_ends(self): + if self.dead_end_cnt is None: + if self.branching_factor() <= 1: + self.dead_end_cnt = 1 + else: + dead_cnt = len([x for x in self.outstanding_doors if x.dead]) + self.dead_end_cnt = dead_cnt - 1 if dead_cnt > 2 else 0 + return self.dead_end_cnt + def is_entrance_sector(self): if self.entrance_sector is None: self.entrance_sector = False diff --git a/DoorShuffle.py b/DoorShuffle.py index 45083d72..2262c5a5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -115,7 +115,7 @@ def vanilla_key_logic(world, player): sector = Sector() sector.name = dungeon.name sector.regions.extend(convert_regions(dungeon.regions, world, player)) - builder = simple_dungeon_builder(sector.name, [sector], world, player) + builder = simple_dungeon_builder(sector.name, [sector]) builder.master_sector = sector builders.append(builder) @@ -298,7 +298,7 @@ def within_dungeon(world, player): dungeon_builders = {} for key in dungeon_regions.keys(): sector_list = convert_to_sectors(dungeon_regions[key], world, player) - dungeon_builders[key] = simple_dungeon_builder(key, sector_list, world, player) + dungeon_builders[key] = simple_dungeon_builder(key, sector_list) dungeon_builders[key].entrance_list = list(entrances_map[key]) recombinant_builders = {} handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) @@ -322,6 +322,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) dungeon_builders.update(split_builders) for sub_name, split_entrances in split_list.items(): sub_builder = dungeon_builders[name+' '+sub_name] + sub_builder.split_flag = True entrance_list = list(split_entrances) if name in flexible_starts.keys(): add_shuffled_entrances(sub_builder.sectors, flexible_starts[name], entrance_list) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 8b521be8..4e429826 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -58,7 +58,9 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe builder = queue.popleft() stonewall = check_for_stonewall(builder, finished_stonewalls) if stonewall is not None: - dungeon_map = stonewall_dungeon_builder(builder, stonewall, entrance_region_names) + # todo: kill drop exceptions + entrances = [x for x in entrance_region_names if x not in ['Skull Back Drop']] + dungeon_map = stonewall_dungeon_builder(builder, stonewall, entrances, world, player) builder_list.remove(builder) for sub_builder in dungeon_map.values(): builder_list.append(sub_builder) @@ -984,8 +986,8 @@ def valid_region_to_explore(region, name, world, player): def get_doors(world, region, player): res = [] - for exit in region.exits: - door = world.check_for_door(exit.name, player) + for ext in region.exits: + door = world.check_for_door(ext.name, player) if door is not None: res.append(door) return res @@ -1002,8 +1004,8 @@ def get_dungeon_doors(region, world, player): def get_entrance_doors(world, region, player): res = [] - for exit in region.entrances: - door = world.check_for_door(exit.name, player) + for ext in region.entrances: + door = world.check_for_door(ext.name, player) if door is not None: res.append(door) return res @@ -1037,11 +1039,12 @@ class DungeonBuilder(object): self.conn_balance = defaultdict(int) self.mag_needed = {} self.unfulfilled = defaultdict(int) - self.all_entrances = None # used for sector segration/branching + self.all_entrances = None # used for sector segregation/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.split_flag = False self.stonewall_entrances = [] # used by stonewall system @@ -1050,6 +1053,17 @@ class DungeonBuilder(object): self.combo_size = None self.flex = 0 + if name in dungeon_dead_end_allowance.keys(): + self.allowance = dungeon_dead_end_allowance[name] + elif 'Stonewall' in name: + self.allowance = 1 + elif 'Prewall' in name: + orig_name = name[:-8] + if orig_name in dungeon_dead_end_allowance.keys(): + self.allowance = dungeon_dead_end_allowance[orig_name] + if self.allowance is None: + self.allowance = 1 + def polarity_complement(self): pol = Polarity() for sector in self.sectors: @@ -1063,12 +1077,13 @@ class DungeonBuilder(object): return pol -def simple_dungeon_builder(name, sector_list, world, player): +def simple_dungeon_builder(name, sector_list): define_sector_features(sector_list) builder = DungeonBuilder(name) dummy_pool = dict.fromkeys(sector_list) + global_pole = GlobalPolarity(dummy_pool) for sector in sector_list: - assign_sector(sector, builder, dummy_pool) + assign_sector(sector, builder, dummy_pool, global_pole) return builder @@ -1079,6 +1094,7 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): dungeon_entrances = default_dungeon_entrances define_sector_features(all_sectors) candidate_sectors = dict.fromkeys(all_sectors) + global_pole = GlobalPolarity(candidate_sectors) dungeon_map = {} for key in dungeon_regions.keys(): @@ -1086,16 +1102,17 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): 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) + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == '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) + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) 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) + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) # categorize sectors + free_location_sectors = {} crystal_switches = {} crystal_barriers = {} @@ -1113,23 +1130,23 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): else: polarized_sectors[sector] = None logger.info('-Assigning Chest Locations') - assign_location_sectors(dungeon_map, free_location_sectors) + assign_location_sectors(dungeon_map, free_location_sectors, global_pole) logger.info('-Assigning Crystal Switches and Barriers') - leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole) 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) + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) # polarity: - if not globally_valid(dungeon_map, None, [], polarized_sectors): + if not global_pole.is_valid(dungeon_map): raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!') logger.info('-Balancing Doors') - assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) # the rest - assign_the_rest(dungeon_map, neutral_sectors) + assign_the_rest(dungeon_map, neutral_sectors, global_pole) return dungeon_map @@ -1149,6 +1166,7 @@ def define_sector_features(sectors): sector.chest_locations += 1 if '- Big Chest' in loc.name: sector.bk_required = True + sector.big_chest_present = True for ext in region.exits: door = ext.door if door is not None: @@ -1162,10 +1180,11 @@ def define_sector_features(sectors): sector.bk_required = True -def assign_sector(sector, dungeon, candidate_sectors): +def assign_sector(sector, dungeon, candidate_sectors, global_pole): if sector is not None: del candidate_sectors[sector] dungeon.sectors.append(sector) + global_pole.consume(sector) dungeon.location_cnt += sector.chest_locations dungeon.key_drop_cnt += sector.key_only_locations if sector.c_switch: @@ -1177,11 +1196,8 @@ def assign_sector(sector, dungeon, candidate_sectors): if sector.bk_provided: dungeon.bk_provided = True count_conn_needed_supplied(sector, dungeon.conn_needed, dungeon.conn_supplied) - factor = sector.branching_factor() - if factor <= 1: - dungeon.dead_ends += 1 - if factor > 2: - dungeon.branches += factor - 2 + dungeon.dead_ends += sector.dead_ends() + dungeon.branches += sector.branches() def count_conn_needed_supplied(sector, conn_needed, conn_supplied): @@ -1201,7 +1217,7 @@ def find_sector(r_name, sectors): return None -def assign_location_sectors(dungeon_map, free_location_sectors): +def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): valid = False choices = None sector_list = list(free_location_sectors) @@ -1218,7 +1234,7 @@ def assign_location_sectors(dungeon_map, free_location_sectors): break for i, choice in enumerate(choices): builder = dungeon_map[choice.name] - assign_sector(sector_list[i], builder, free_location_sectors) + assign_sector(sector_list[i], builder, free_location_sectors, global_pole) def weighted_random_locations(dungeon_map, free_location_sectors): @@ -1256,7 +1272,7 @@ def minimal_locations(dungeon_name): return 5 -def assign_crystal_switch_sectors(dungeon_map, crystal_switches, assign_one=False): +def assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, assign_one=False): population = [] some_c_switches_present = False for name, builder in dungeon_map.items(): @@ -1266,19 +1282,33 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, assign_one=Fals 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) + valid, builder_choice, switch_choice = False, None, None + switch_candidates = list(crystal_switches) + switch_choice = random.choice(switch_candidates) + switch_candidates.remove(switch_choice) + builder_candidates = list(dungeon_map.keys()) + while not valid: + if len(builder_candidates) == 0: + if len(switch_candidates) == 0: + raise Exception('No where to assign crystal switch. Ref %s' % next(iter(dungeon_map.keys()))) + switch_choice = random.choice(switch_candidates) + switch_candidates.remove(switch_choice) + builder_candidates = list(dungeon_map.keys()) + choice = random.choice(builder_candidates) + builder_candidates.remove(choice) + builder_choice = dungeon_map[choice] + valid = global_pole.is_valid_choice(dungeon_map, builder_choice, [switch_choice]) + assign_sector(switch_choice, builder_choice, crystal_switches, global_pole) return crystal_switches sector_list = list(crystal_switches) choices = random.sample(sector_list, k=len(population)) for i, choice in enumerate(choices): builder = dungeon_map[population[i]] - assign_sector(choice, builder, crystal_switches) + assign_sector(choice, builder, crystal_switches, global_pole) return crystal_switches -def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers): +def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole): population = [] for name, builder in dungeon_map.items(): if builder.c_switch_present: @@ -1288,7 +1318,7 @@ def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers): 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) + assign_sector(sector_list[i], builder, crystal_barriers, global_pole) def identify_polarity_issues(dungeon_map): @@ -1347,10 +1377,10 @@ def check_flags(sector_mag, connection_flags): def identify_simple_branching_issues(dungeon_map): problem_builders = {} for name, builder in dungeon_map.items(): - if name == 'Skull Woods 2': # i dislike this special case + if name == 'Skull Woods 2': # i dislike this special case todo: identify destination entrances builder.conn_supplied[Hook.West] += 1 builder.conn_needed[Hook.East] -= 1 - if builder.dead_ends > builder.branches + 1: # todo: if entrances need to link like skull 2 then this is reduced for each linkage necessary + if builder.dead_ends > builder.branches + builder.allowance: problem_builders[name] = builder for h_type in Hook: lack = builder.conn_balance[h_type] = builder.conn_supplied[h_type] - builder.conn_needed[h_type] @@ -1398,22 +1428,21 @@ def sum_polarity(sector_list): return pol -def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): +def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, 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) - while not globally_valid(dungeon_map, builder, [sector], polarized_sectors): - candidates.remove(sector) + valid, sector = False, None + while not valid: if len(candidates) == 0: - raise Exception('Cross Dungeon Builder: Cannot find a candidate for connectedness - globally invalid') + raise Exception('Cross Dungeon Builder: Cannot find a candidate for connectedness. %s' % name) sector = random.choice(candidates) - assign_sector(sector, builder, polarized_sectors) + candidates.remove(sector) + valid = global_pole.is_valid_choice(dungeon_map, builder, [sector]) + assign_sector(sector, builder, polarized_sectors, global_pole) builder.mag_needed = {} unconnected_builders = identify_polarity_issues(unconnected_builders) @@ -1422,16 +1451,24 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): while len(problem_builders) > 0: for name, builder in problem_builders.items(): candidates, charges = find_simple_branching_candidates(builder, polarized_sectors) - # todo: could use the smaller charges as weights to help pre-balance - choice = random.choice(candidates) - if valid_connected_assignment(builder, [choice]) and globally_valid(dungeon_map, builder, [choice], polarized_sectors): - assign_sector(choice, builder, polarized_sectors) + biggest = max(charges) + 1 + weights = [biggest-x for x in charges] + valid, choice = False, None + while not valid: + if len(candidates) == 0: + raise Exception('Cross Dungeon Builder: Simple branch problems: %s' % name) + choice = random.choices(candidates, weights)[0] + i = candidates.index(choice) + candidates.pop(i) + weights.pop(i) + valid = global_pole.is_valid_choice(dungeon_map, builder, [choice]) and valid_connected_assignment(builder, [choice]) + assign_sector(choice, builder, polarized_sectors, global_pole) builder.total_conn_lack = 0 builder.conn_balance.clear() problem_builders = identify_simple_branching_issues(problem_builders) # step 3: fix neutrality issues - polarity_step_3(dungeon_map, polarized_sectors, logger) + polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger) # step 4: fix dead ends again neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) @@ -1446,7 +1483,7 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): if valid_polarized_assignment(builder, choice): neutral_choices.remove(choice) for sector in choice: - assign_sector(sector, builder, polarized_sectors) + assign_sector(sector, builder, polarized_sectors, global_pole) builder.unfulfilled.clear() problem_builders = identify_branching_issues_2(problem_builders) @@ -1454,72 +1491,116 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): tries = 0 while len(polarized_sectors) > 0: if tries > 100: - raise Exception('No valid assignment found') + raise Exception('No valid assignment found. Ref: %s' % next(iter(dungeon_map.keys()))) choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices)) valid = [] for i, choice in enumerate(choices): builder = dungeon_map[choice] if valid_assignment(builder, neutral_choices[i]): for sector in neutral_choices[i]: - assign_sector(sector, builder, polarized_sectors) + assign_sector(sector, builder, polarized_sectors, global_pole) valid.append(neutral_choices[i]) for c in valid: neutral_choices.remove(c) tries += 1 -def polarity_step_3(dungeon_map, polarized_sectors, logger): +def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): builder_order = list(dungeon_map.values()) random.shuffle(builder_order) for builder in builder_order: logger.info('--Balancing %s', builder.name) - globally_valid(dungeon_map, builder, [], polarized_sectors) while not builder.polarity().is_neutral(): candidates = find_neutralizing_candidates(builder, polarized_sectors) - sectors = random.choice(candidates) - while not globally_valid(dungeon_map, builder, sectors, polarized_sectors): - candidates.remove(sectors) + valid, sectors = False, None + while not valid: if len(candidates) == 0: - raise NeutralizingException('Unable to find a globally valid neutralizer') + raise NeutralizingException('Unable to find a globally valid neutralizer: %s' % builder.name) sectors = random.choice(candidates) + candidates.remove(sectors) + valid = global_pole.is_valid_choice(dungeon_map, builder, sectors) for sector in sectors: - assign_sector(sector, builder, polarized_sectors) + assign_sector(sector, builder, polarized_sectors, global_pole) -def globally_valid(dungeon_map, builder, sectors, polarized_sectors): - non_neutral_polarities = [x.polarity() for x in dungeon_map.values() if not x.polarity().is_neutral() and x != builder] - remaining = [x for x in polarized_sectors if x not in sectors] - positives = [0, 0, 0] - negatives = [0, 0, 0] - for sector in remaining: - pol = sector.polarity() - for slot in PolSlot: - if pol.vector[slot.value] < 0: - negatives[slot.value] += 1 - elif pol.vector[slot.value] > 0: - positives[slot.value] += 1 - if builder is not None: - current_polarity = builder.polarity() + sum_polarity(sectors) - non_neutral_polarities.append(current_polarity) - for polarity in non_neutral_polarities: +class GlobalPolarity: + + def __init__(self, candidate_sectors): + self.positives = [0, 0, 0] + self.negatives = [0, 0, 0] + for sector in candidate_sectors: + pol = sector.polarity() + for slot in PolSlot: + if pol.vector[slot.value] < 0: + self.negatives[slot.value] += -pol.vector[slot.value] + elif pol.vector[slot.value] > 0: + self.positives[slot.value] += pol.vector[slot.value] + + def copy(self): + gp = GlobalPolarity([]) + gp.positives = self.positives.copy() + gp.negatives = self.negatives.copy() + return gp + + def is_valid(self, dungeon_map): + polarities = [x.polarity() for x in dungeon_map.values()] + return self._is_valid_polarities(polarities) + + def _is_valid_polarities(self, polarities): + positives = self.positives.copy() + negatives = self.negatives.copy() + for polarity in polarities: + for slot in PolSlot: + if polarity[slot.value] > 0 and slot != PolSlot.Stairs: + if negatives[slot.value] >= polarity[slot.value]: + negatives[slot.value] -= polarity[slot.value] + else: + return False + elif polarity[slot.value] < 0 and slot != PolSlot.Stairs: + if positives[slot.value] >= -polarity[slot.value]: + positives[slot.value] += polarity[slot.value] + else: + return False + elif slot == PolSlot.Stairs: + if positives[slot.value] >= polarity[slot.value]: + positives[slot.value] -= polarity[slot.value] + else: + return False + return True + + def consume(self, sector): + polarity = sector.polarity() for slot in PolSlot: if polarity[slot.value] > 0 and slot != PolSlot.Stairs: - if negatives[slot.value] >= polarity[slot.value]: - negatives[slot.value] -= polarity[slot.value] + if self.positives[slot.value] >= polarity[slot.value]: + self.positives[slot.value] -= polarity[slot.value] else: - return False - elif polarity[slot.value] < 0 or slot == PolSlot.Stairs: - if positives[slot.value] >= -polarity[slot.value]: - positives[slot.value] += polarity[slot.value] + raise Exception('Invalid assignment of %s' % sector.name) + elif polarity[slot.value] < 0 and slot != PolSlot.Stairs: + if self.negatives[slot.value] >= -polarity[slot.value]: + self.negatives[slot.value] += polarity[slot.value] else: - return False - return True + raise Exception('Invalid assignment of %s' % sector.name) + elif slot == PolSlot.Stairs: + if self.positives[slot.value] >= polarity[slot.value]: + self.positives[slot.value] -= polarity[slot.value] + else: + raise Exception('Invalid assignment of %s' % sector.name) + + def is_valid_choice(self, dungeon_map, builder, sectors): + proposal = self.copy() + non_neutral_polarities = [x.polarity() for x in dungeon_map.values() if not x.polarity().is_neutral() and x != builder] + current_polarity = builder.polarity() + sum_polarity(sectors) + non_neutral_polarities.append(current_polarity) + for sector in sectors: + proposal.consume(sector) + return proposal._is_valid_polarities(non_neutral_polarities) def find_connection_candidates(mag_needed, sector_pool): candidates = [] for sector in sector_pool: - if sector.outflow() < 2: + if sector.branching_factor() < 2: continue mag = sector.magnitude() matches = False @@ -1537,7 +1618,7 @@ def find_connection_candidates(mag_needed, sector_pool): def find_simple_branching_candidates(builder, sector_pool): candidates = defaultdict(list) charges = defaultdict(list) - outflow_needed = builder.dead_ends > builder.branches + 1 + outflow_needed = builder.dead_ends > builder.branches + builder.allowance original_lack = builder.total_conn_lack best_lack = original_lack for sector in sector_pool: @@ -1561,13 +1642,12 @@ def find_simple_branching_candidates(builder, sector_pool): return candidates[best_lack], charges[best_lack] -def calc_sector_balance(sector): # move to base class? +def calc_sector_balance(sector): # todo: move to base class? if sector.conn_balance is None: sector.conn_balance = defaultdict(int) for door in sector.outstanding_doors: - if door.blocked or door.dead or sector.adj_outflow() <= 1: + if door.blocked or door.dead or sector.branching_factor() <= 1: sector.conn_balance[hook_from_door(door)] -= 1 - # todo: stonewall - not a great candidate anyway yet else: sector.conn_balance[hanger_from_door(door)] += 1 @@ -1584,7 +1664,7 @@ def find_neutralizing_candidates(builder, sector_pool): for r in r_range: if r > len(main_pool): if len(candidates) == 0: - raise NeutralizingException('Cross Dungeon Builder: No possible neutralizers left') + raise NeutralizingException('Cross Dungeon Builder: No possible neutralizers left %s' % builder.name) else: continue last_r = r @@ -1600,7 +1680,7 @@ def find_neutralizing_candidates(builder, sector_pool): official_cand = [] while len(official_cand) == 0: if len(candidates.keys()) == 0: - raise NeutralizingException('Cross Dungeon Builder: Weeded out all candidates') + raise NeutralizingException('Cross Dungeon Builder: Weeded out all candidates %s' % builder.name) while best_charge not in candidates.keys(): best_charge += 1 candidate_list = candidates.pop(best_charge) @@ -1610,11 +1690,8 @@ def find_neutralizing_candidates(builder, sector_pool): ttl_branches = 0 for sector in cand: calc_sector_balance(sector) - factor = sector.branching_factor() - if factor <= 1: - ttl_deads += 1 - elif factor > 2: - ttl_branches += factor - 2 + ttl_deads += sector.dead_ends() + ttl_branches += sector.branches() ttl_lack = 0 ttl_balance = 0 for hook in Hook: @@ -1625,7 +1702,7 @@ def find_neutralizing_candidates(builder, sector_pool): ttl_balance += lack if lack < 0: ttl_lack += -lack - if ttl_balance >= 0 and builder.dead_ends + ttl_deads <= builder.branches + ttl_branches + 1: # todo: calc for different entrances + if ttl_balance >= 0 and builder.dead_ends + ttl_deads <= builder.branches + ttl_branches + builder.allowance: if best_lack is None or ttl_lack < best_lack: best_lack = ttl_lack official_cand = [cand] @@ -1659,7 +1736,7 @@ def find_branching_candidates(builder, neutral_choices): if door_match and flow_match: candidates.append(choice) if len(candidates) == 0: - raise Exception('Cross Dungeon Builder: No more branching candidates!') + raise Exception('Cross Dungeon Builder: No more branching candidates! %s' % builder.name) return candidates @@ -1723,32 +1800,24 @@ def valid_connected_assignment(builder, sector_list): def valid_polarized_assignment(builder, sector_list): if not valid_connected_assignment(builder, sector_list): return False - # 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): +def assign_the_rest(dungeon_map, neutral_sectors, global_pole): 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) + assign_sector(sector_list[i], builder, neutral_sectors, global_pole) def split_dungeon_builder(builder, split_list): logger = logging.getLogger('') logger.info('Splitting Up Desert/Skull') candidate_sectors = dict.fromkeys(builder.sectors) + global_pole = GlobalPolarity(candidate_sectors) dungeon_map = {} for name, split_entrances in split_list.items(): @@ -1756,25 +1825,31 @@ def split_dungeon_builder(builder, split_list): 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) - return balance_split(candidate_sectors, dungeon_map) + assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) + return balance_split(candidate_sectors, dungeon_map, global_pole) -def balance_split(candidate_sectors, dungeon_map): +def balance_split(candidate_sectors, dungeon_map, global_pole): logger = logging.getLogger('') # categorize sectors - crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors, dungeon_map) + crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, 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) + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) # polarity: logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') - assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) # the rest - assign_the_rest(dungeon_map, neutral_sectors) + assign_the_rest(dungeon_map, neutral_sectors, global_pole) return dungeon_map -def categorize_sectors(candidate_sectors, dungeon_map): +def categorize_sectors(candidate_sectors): crystal_switches = {} crystal_barriers = {} polarized_sectors = {} @@ -1788,16 +1863,10 @@ def categorize_sectors(candidate_sectors, dungeon_map): 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 - return crystal_barriers, neutral_sectors, polarized_sectors + return crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors -def stonewall_dungeon_builder(builder, stonewall, entrance_region_names): +def stonewall_dungeon_builder(builder, stonewall, entrance_region_names, world, player): logger = logging.getLogger('') logger.info('Stonewall treatment') candidate_sectors = dict.fromkeys(builder.sectors) @@ -1818,9 +1887,29 @@ def stonewall_dungeon_builder(builder, stonewall, entrance_region_names): define_sector_features([stonewall_connector, stonewall_start]) candidate_sectors[stonewall_start] = None candidate_sectors[stonewall_connector] = None - create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors) - create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors) - return balance_split(candidate_sectors, dungeon_map) + global_pole = GlobalPolarity(candidate_sectors) + create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors, global_pole) + origin_builder = create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors, global_pole) + + if stonewall.name == 'Desert Wall Slide NW': + # not true if big shuffled or split + location_needed = not builder.split_flag and not world.bigkeyshuffle[player] + for sector in origin_builder.sectors: + location_needed &= sector.chest_locations == 0 or (sector.chest_locations == 1 and sector.big_chest_present) + if location_needed: + free_location_sectors = [] + for sector in candidate_sectors: + if sector.chest_locations > 1 or (sector.chest_locations == 1 and not sector.big_chest_present): + free_location_sectors.append(sector) + valid = False + while not valid: + if len(free_location_sectors) == 0: + raise Exception('Cannot place a big key sector before the wall slide, ouch') + sector = random.choice(free_location_sectors) + free_location_sectors.remove(sector) + valid = global_pole.is_valid_choice(dungeon_map, origin_builder, [sector]) + assign_sector(sector, origin_builder, candidate_sectors, global_pole) + return balance_split(candidate_sectors, dungeon_map, global_pole) # dependent sector splits # dependency_list = [] @@ -1861,47 +1950,53 @@ def stonewall_dungeon_builder(builder, stonewall, entrance_region_names): # raise NeutralizingException('Unable to find a valid combination') -def stonewall_split(candidate_sectors, dungeon_map): +def stonewall_split(candidate_sectors, dungeon_map, global_pole): logger = logging.getLogger('') # categorize sectors - crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors, dungeon_map) + crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, 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) + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) # polarity: logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') - polarity_step_3(dungeon_map, polarized_sectors, logger) - assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger) + assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) # the rest - assign_the_rest(dungeon_map, neutral_sectors) + assign_the_rest(dungeon_map, neutral_sectors, global_pole) return dungeon_map -def create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors): +def create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors, global_pole): key = builder.name + ' Prewall' dungeon_map[key] = origin_builder = DungeonBuilder(key) origin_builder.stonewall_entrances += entrance_region_names origin_builder.all_entrances = [] - for ent in builder.all_entrances: + for ent in entrance_region_names: sector = find_sector(ent, candidate_sectors) if sector is not None: for door in sector.outstanding_doors: if not door.blocked: origin_builder.all_entrances.append(ent) - assign_sector(sector, origin_builder, candidate_sectors) + assign_sector(sector, origin_builder, candidate_sectors, global_pole) break else: # already got assigned origin_builder.all_entrances.append(ent) - assign_sector(stonewall_connector, origin_builder, candidate_sectors) + assign_sector(stonewall_connector, origin_builder, candidate_sectors, global_pole) return origin_builder -def create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors): +def create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors, global_pole): key = builder.name + ' Stonewall' dungeon_map[key] = stone_builder = DungeonBuilder(key) stone_builder.stonewall_entrances += [region.name] stone_builder.all_entrances = [region.name] stone_builder.branch_factor = 2 - assign_sector(stonewall_start, stone_builder, candidate_sectors) + assign_sector(stonewall_start, stone_builder, candidate_sectors, global_pole) return stone_builder @@ -2045,9 +2140,8 @@ def resolve_equations(builder, sector_list): def find_priority_equation(equations, current_access): flex = calc_flex(equations, current_access) best_profit = None - best_flex = False - best_local = False - selected_triplet = None, None, None + triplet_candidates = [] + local_profit_map = {} for sector, eq_list in equations.items(): eq_list.sort(key=lambda eq: eq.profit(), reverse=True) best_local_profit = None @@ -2058,21 +2152,34 @@ def find_priority_equation(equations, current_access): if eq.can_cover_cost(current_access): if eq.neutral(): return eq, eq_list, sector # don't need to compare - flexible = eq.can_cover_cost(flex) - good_local = profit == best_local_profit - if best_profit is None or profit > best_profit: - best_profit = profit - best_flex = flexible - best_local = good_local - selected_triplet = eq, eq_list, sector - elif profit == best_profit and (flexible and not best_flex): - best_flex = flexible - best_local = good_local - selected_triplet = eq, eq_list, sector - elif profit == best_profit and flexible == best_flex and (good_local and not best_local): - best_local = good_local - selected_triplet = eq, eq_list, sector - return selected_triplet + if best_profit is None or profit >= best_profit: + if best_profit is None or profit > best_profit: + triplet_candidates = [(eq, eq_list, sector)] + best_profit = profit + else: + triplet_candidates.append((eq, eq_list, sector)) + local_profit_map[sector] = best_local_profit + if len(triplet_candidates) == 0: + return None, None, None # can't pay for anything + if len(triplet_candidates) == 1: + return triplet_candidates[0] + + required_candidates = [x for x in triplet_candidates if x[0].required] + if len(required_candidates) == 0: + required_candidates = triplet_candidates + if len(required_candidates) == 1: + return required_candidates[0] + + flexible_candidates = [x for x in required_candidates if x[0].can_cover_cost(flex)] + if len(flexible_candidates) == 0: + flexible_candidates = required_candidates + if len(flexible_candidates) == 1: + return flexible_candidates[0] + + good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit()] + if len(good_local_candidates) == 0: + good_local_candidates = flexible_candidates + return good_local_candidates[0] # just pick one I guess def calc_flex(equations, current_access): @@ -2261,3 +2368,26 @@ default_dungeon_entrances = { 'Turtle Rock': ['TR Main Lobby', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'], 'Ganons Tower': ['GT Lobby'] } + + +# todo: calculate these for ER - the multi entrance dungeons anyway +dungeon_dead_end_allowance = { + 'Hyrule Castle': 6, + 'Eastern Palace': 1, + 'Desert Palace': 2, + 'Tower of Hera': 1, + 'Agahnims Tower': 1, + 'Palace of Darkness': 1, + 'Swamp Palace': 1, + 'Skull Woods': 3, # two allowed in skull 1, 1 in skull 3, 0 in skull 2 + 'Thieves Town': 1, + 'Ice Palace': 1, + 'Misery Mire': 1, + 'Turtle Rock': 2, # this assumes one overworld connection + 'Ganons Tower': 1, + 'Desert Palace Back': 1, + 'Desert Palace Main': 1, + 'Skull Woods 1': 2, + 'Skull Woods 2': 0, + 'Skull Woods 3': 1, +} diff --git a/Main.py b/Main.py index 9dace756..294dc770 100644 --- a/Main.py +++ b/Main.py @@ -23,7 +23,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names -__version__ = '0.0.a-pre' +__version__ = '0.0.b-pre' def main(args, seed=None):