From 3fdf9e86248fffcc3d6d7ff7914c652311049e6f Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 11 Mar 2020 14:21:14 -0600 Subject: [PATCH] Unvalidated gen work --- DungeonGenerator.py | 281 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 217 insertions(+), 64 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 98ac7453..5b039fad 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1023,6 +1023,7 @@ class DungeonBuilder(object): self.flex = 0 self.key_door_proposal = None + self.allowance = None if name in dungeon_dead_end_allowance.keys(): self.allowance = dungeon_dead_end_allowance[name] elif 'Stonewall' in name: @@ -1124,7 +1125,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge logger.info('-Assigning Chest Locations') 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, global_pole) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) for sector in leftover: if sector.polarity().is_neutral(): neutral_sectors[sector] = None @@ -1348,7 +1349,7 @@ def minimal_locations(dungeon_name): return 5 -def assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, assign_one=False): +def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): population = [] some_c_switches_present = False for name, builder in dungeon_map.items(): @@ -1358,6 +1359,8 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, as 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 + if len(crystal_switches) == 0: + raise Exception('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys()))) valid, builder_choice, switch_choice = False, None, None switch_candidates = list(crystal_switches) switch_choice = random.choice(switch_candidates) @@ -1373,7 +1376,9 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, global_pole, as 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]) + test_set = [switch_choice] + test_set.extend(crystal_barriers) + valid = global_pole.is_valid_choice(dungeon_map, builder_choice, test_set) assign_sector(switch_choice, builder_choice, crystal_switches, global_pole) return crystal_switches sector_list = list(crystal_switches) @@ -1400,45 +1405,48 @@ def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole): def identify_polarity_issues(dungeon_map): unconnected_builders = {} for name, builder in dungeon_map.items(): - if len(builder.sectors) == 1: - continue - else: - def sector_filter(x, y): - return x != y + identify_polarity_issues_internal(name, builder, unconnected_builders) + return unconnected_builders + + +def identify_polarity_issues_internal(name, builder, unconnected_builders): + if len(builder.sectors) == 1: + return + else: + def sector_filter(x, y): + return x != y # else: # def sector_filter(x, y): # return x != y and (x.outflow() > 1 or is_entrance_sector(builder, x)) - connection_flags = {} - for slot in PolSlot: - connection_flags[slot] = {} - for slot2 in PolSlot: - connection_flags[slot][slot2] = False - 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() - check_flags(sector_mag, connection_flags) - unconnected_sector = True + connection_flags = {} + for slot in PolSlot: + connection_flags[slot] = {} + for slot2 in PolSlot: + connection_flags[slot][slot2] = False + 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() + check_flags(sector_mag, connection_flags) + unconnected_sector = True + for i in PolSlot: + if sector_mag[i.value] == 0 or other_mag[i.value] > 0 or self_connecting(sector, i, sector_mag): + unconnected_sector = False + break + if unconnected_sector: for i in PolSlot: - if sector_mag[i.value] == 0 or other_mag[i.value] > 0 or self_connecting(sector, i, sector_mag): - unconnected_sector = False - break - if unconnected_sector: - for i in PolSlot: - if sector_mag[i.value] > 0 and other_mag[i.value] == 0 and not self_connecting(sector, i, sector_mag): - builder.mag_needed[i] = [x for x in PolSlot if other_mag[x.value] > 0] - if name not in unconnected_builders.keys(): - unconnected_builders[name] = builder - ttl_mag = sum_magnitude(builder.sectors) - for slot in PolSlot: - for slot2 in PolSlot: - if ttl_mag[slot.value] > 0 and ttl_mag[slot2.value] > 0 and not connection_flags[slot][slot2]: - builder.mag_needed[slot] = [slot2] - builder.mag_needed[slot2] = [slot] + if sector_mag[i.value] > 0 and other_mag[i.value] == 0 and not self_connecting(sector, i, sector_mag): + builder.mag_needed[i] = [x for x in PolSlot if other_mag[x.value] > 0] if name not in unconnected_builders.keys(): unconnected_builders[name] = builder - return unconnected_builders - + ttl_mag = sum_magnitude(builder.sectors) + for slot in PolSlot: + for slot2 in PolSlot: + if ttl_mag[slot.value] > 0 and ttl_mag[slot2.value] > 0 and not connection_flags[slot][slot2]: + builder.mag_needed[slot] = [slot2] + builder.mag_needed[slot2] = [slot] + if name not in unconnected_builders.keys(): + unconnected_builders[name] = builder def self_connecting(sector, slot, magnitude): return sector.polarity()[slot.value] == 0 and sum(magnitude) > magnitude[slot.value] @@ -1518,7 +1526,7 @@ def filter_match_deps(candidate, match_deps): def sum_magnitude(sector_list): - result = [0, 0, 0] + result = [0] * len(PolSlot) for sector in sector_list: vector = sector.magnitude() for i in range(len(result)): @@ -1651,7 +1659,11 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): sub_candidates = odd_candidates.pop(best_charge) candidate_list = random.choice(sub_candidates) sub_candidates.remove(candidate_list) - valid = global_pole.is_valid_choice(dungeon_map, builder, candidate_list) and valid_branch_only(builder, candidate_list) + test_set = find_forced_connections(dungeon_map, candidate_list, polarized_sectors) + if ensure_test_set_connectedness(test_set, builder, polarized_sectors, dungeon_map, global_pole): + valid = global_pole.is_valid_choice(dungeon_map, builder, test_set) and valid_branch_only(builder, candidate_list) + else: + valid = False for candidate in candidate_list: assign_sector(candidate, builder, polarized_sectors, global_pole) @@ -1679,6 +1691,63 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): assign_sector(sector, builder, polarized_sectors, global_pole) +def find_forced_connections(dungeon_map, candidate_list, polarized_sectors): + test_set = list(candidate_list) + other_sectors = [x for x in polarized_sectors if x not in candidate_list] + dungeon_hooks = defaultdict(int) + for name, builder in dungeon_map.items(): + d_mag = sum_hook_magnitude(builder.sectors) + for val in Hook: + dungeon_hooks[val] += d_mag[val.value] + queue = deque(candidate_list) + while queue: + candidate = queue.pop() + c_mag = candidate.hook_magnitude() + other_candidates = [x for x in candidate_list if x != candidate] + for val in Hook: + if c_mag[val.value] > 0: + opp = opposite_h_type(val) + o_val = opp.value + if sum_hook_magnitude(other_candidates)[o_val] == 0 and dungeon_hooks[opp] == 0 and not valid_self(c_mag, val, opp): + forced_sector = [] + for sec in other_sectors: + if sec.hook_magnitude()[o_val] > 0: + forced_sector.append(sec) + if len(forced_sector) > 1: + break + if len(forced_sector) == 1: + test_set.append(forced_sector[0]) + return test_set + + +def valid_self(c_mag, val, opp): + if val == Hook.Stairs: + return c_mag[val.value] > 2 + else: + return c_mag[opp.value] > 0 and sum(c_mag) > 2 + + +def ensure_test_set_connectedness(test_set, builder, polarized_sectors, dungeon_map, global_pole): + test_copy = list(test_set) + while not valid_connected_assignment(builder, test_copy): + dummy_builder = DungeonBuilder("Dummy Builder for " + builder.name) + dummy_builder.sectors = builder.sectors + test_copy + possibles = [x for x in polarized_sectors if x not in test_copy] + candidates = find_connected_candidates(possibles) + valid, sector = False, None + while not valid: + if len(candidates) == 0: + return False + sector = random.choice(candidates) + candidates.remove(sector) + t2 = test_copy+[sector] + valid = global_pole.is_valid_choice(dungeon_map, builder, t2) and valid_branch_only(builder, t2) + test_copy.append(sector) + dummy_builder.sectors = builder.sectors + test_copy + test_set[:] = test_copy + return True + + class GlobalPolarity: def __init__(self, candidate_sectors): @@ -1805,6 +1874,7 @@ def find_simple_branching_candidates(builder, sector_pool): candidates = defaultdict(list) charges = defaultdict(list) outflow_needed = builder.dead_ends + builder.forced_loops * 2 > builder.branches + builder.allowance + total_needed = builder.dead_ends + builder.forced_loops * 2 - builder.branches + builder.allowance original_lack = builder.total_conn_lack best_lack = original_lack for sector in sector_pool: @@ -1817,7 +1887,8 @@ def find_simple_branching_candidates(builder, sector_pool): if lack < 0: ttl_lack += -lack forced_loops = calc_forced_loops(builder.sectors + [sector]) - valid_branches = builder.dead_ends + forced_loops * 2 + sector.dead_ends() <= builder.branches + builder.allowance + sector.branches() + net_outflow = builder.dead_ends + forced_loops * 2 + sector.dead_ends() - builder.branches - builder.allowance - sector.branches() + valid_branches = net_outflow < total_needed if valid_branches and (ttl_lack < original_lack or original_lack >= 0): candidates[ttl_lack].append(sector) charges[ttl_lack].append((builder.polarity()+sector.polarity()).charge()) @@ -1948,6 +2019,14 @@ def find_branching_candidates(builder, neutral_choices): return candidates +def find_connected_candidates(sector_pool): + candidates = [] + for sector in sector_pool: + if sector.adj_outflow() >= 2: + candidates.append(sector) + return candidates + + def neutralize_the_rest(sector_pool): neutral_choices = [] main_pool = list(sector_pool) @@ -2034,6 +2113,10 @@ def valid_assignment(builder, sector_list): return len(resolve_equations(builder, sector_list)) == 0 +def valid_equations(builder, sector_list): + return len(resolve_equations(builder, sector_list)) == 0 + + def valid_connected_assignment(builder, sector_list): full_list = sector_list + builder.sectors for sector in full_list: @@ -2073,13 +2156,35 @@ def valid_polarized_assignment(builder, sector_list): def assign_the_rest(dungeon_map, neutral_sectors, global_pole): + comb_w_replace = len(dungeon_map) ** len(neutral_sectors) + combinations = None + if comb_w_replace <= 1000: + combinations = list(itertools.product(dungeon_map.keys(), repeat=len(neutral_sectors))) + random.shuffle(combinations) + tries = 0 while len(neutral_sectors) > 0: - sector_list = list(neutral_sectors) - choices = random.choices(list(dungeon_map.keys()), k=len(sector_list)) + if tries > 1000 or (combinations and tries >= len(combinations)): + raise Exception('No valid assignment found for "neutral" sectors. Ref: %s' % next(iter(dungeon_map.keys()))) + # sector_list = list(neutral_sectors) + if combinations: + choices = combinations[tries] + else: + choices = random.choices(list(dungeon_map.keys()), k=len(neutral_sectors)) + neutral_sector_list = list(neutral_sectors) + chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): - builder = dungeon_map[choice] - if valid_assignment(builder, [sector_list[i]]): - assign_sector(sector_list[i], builder, neutral_sectors, global_pole) + chosen_sectors[choice].append(neutral_sector_list[i]) + all_valid = True + for name, sector_list in chosen_sectors.items(): + if not valid_assignment(dungeon_map[name], sector_list): + all_valid = False + break + if all_valid: + for name, sector_list in chosen_sectors.items(): + builder = dungeon_map[name] + for sector in sector_list: + assign_sector(sector, builder, neutral_sectors, global_pole) + tries += 1 def split_dungeon_builder(builder, split_list): @@ -2103,8 +2208,10 @@ def balance_split(candidate_sectors, dungeon_map, global_pole): # categorize sectors check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole) check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole) + check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole) 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) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, + global_pole, len(crystal_barriers) > 0) for sector in leftover: if sector.polarity().is_neutral(): neutral_sectors[sector] = None @@ -2156,6 +2263,14 @@ def check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole): builder.c_locked = True +def check_crystal(dead_end, entrance): + if dead_end.blue_barrier and not entrance.c_switch and not dead_end.c_switch: + return False + if entrance.blue_barrier and not entrance.c_switch and not dead_end.c_switch: + return False + return True + + def check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole): done = False while not done: @@ -2166,26 +2281,47 @@ def check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole): dungeon_hooks[name] = sum_hook_magnitude(builder.sectors) for val in Hook: if magnitude[val.value] == 1: - found_hooks = [] + forced_sector = None + for sec in candidate_sectors: + if sec.hook_magnitude()[val.value] > 0: + forced_sector = sec + break opp = opposite_h_type(val).value - for name, hooks in dungeon_hooks.items(): - if hooks[opp] > 0 and not dungeon_map[name].c_locked: - found_hooks.append(name) - if len(found_hooks) == 1: - done = False - forced_sector = None - for sec in candidate_sectors: - if sec.hook_magnitude()[val.value] > 0: - forced_sector = sec - break - assign_sector(forced_sector, dungeon_map[found_hooks[0]], candidate_sectors, global_pole) + other_sectors = [x for x in candidate_sectors if x != forced_sector] + if sum_hook_magnitude(other_sectors)[opp] == 0: + found_hooks = [] + for name, hooks in dungeon_hooks.items(): + if hooks[opp] > 0 and not dungeon_map[name].c_locked: + found_hooks.append(name) + if len(found_hooks) == 1: + done = False + assign_sector(forced_sector, dungeon_map[found_hooks[0]], candidate_sectors, global_pole) -def check_crystal(dead_end, entrance): - if dead_end.blue_barrier and not entrance.c_switch and not dead_end.c_switch: - return False - if entrance.blue_barrier and not entrance.c_switch and not dead_end.c_switch: - return False +def check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole): + for name, builder in dungeon_map.items(): + if check_for_forced_crystal_single(builder, candidate_sectors): + builder.c_switch_required = True + + +def check_for_forced_crystal_single(builder, candidate_sectors): + builder_doors = defaultdict(dict) + for sector in builder.sectors: + for door in sector.outstanding_doors: + builder_doors[hook_from_door(door)][door] = sector + candidate_doors = defaultdict(dict) + for sector in candidate_sectors: + for door in sector.outstanding_doors: + candidate_doors[hook_from_door(door)][door] = sector + for hook in builder_doors.keys(): + for door in builder_doors[hook].keys(): + opp = opposite_h_type(hook) + for d, sector in builder_doors[opp].items(): + if d != door and (not sector.blue_barrier or sector.c_switch): + return False + for d, sector in candidate_doors[opp].items(): + if not sector.blue_barrier or sector.c_switch: + return False return True @@ -2348,7 +2484,7 @@ def find_priority_equation(equations, access_id, current_access): if best_local_profit is None or profit > best_local_profit: best_local_profit = profit all_candidates.append((eq, eq_list, sector)) - elif best_profit is None or profit >= best_profit: + elif (best_profit is None or profit >= best_profit) and profit > 0: if best_profit is None or profit > best_profit: wanted_candidates = [eq] best_profit = profit @@ -2401,7 +2537,24 @@ def find_priority_equation(equations, access_id, current_access): leads_to_profit = [x for x in good_local_candidates if can_enable_wanted(x[0], wanted_candidates)] if len(leads_to_profit) == 0: leads_to_profit = good_local_candidates - return leads_to_profit[0] # just pick one I guess + if len(leads_to_profit) == 1: + return leads_to_profit[0] + + cost_point = {x[0]: find_cost_point(x, current_access) for x in leads_to_profit} + best_point = max(cost_point.values()) + cost_point_candidates = [x for x in leads_to_profit if cost_point[x[0]] == best_point] + if len(cost_point_candidates) == 0: + cost_point_candidates = leads_to_profit + return cost_point_candidates[0] # just pick one I guess + + +def find_cost_point(eq_triplet, access): + cost_point = 0 + for key, costs in eq_triplet[0].cost.items(): + cost_count = len(costs) + if cost_count > 0: + cost_point += access[key] - cost_count + return cost_point def find_greedy_equation(equations, access_id, current_access):