diff --git a/BaseClasses.py b/BaseClasses.py index 340b08ab..ce559ac2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1243,6 +1243,7 @@ class Sector(object): self.conn_balance = None self.branch_factor = None self.entrance_sector = None + self.equations = None def region_set(self): if self.r_name_set is None: diff --git a/Doors.py b/Doors.py index 348ba4a6..b57f9a2c 100644 --- a/Doors.py +++ b/Doors.py @@ -120,7 +120,7 @@ def create_doors(world, player): create_door(player, 'Eastern Lobby Left Ledge SW', Intr).dir(So, 0xc9, Left, High).pos(2), create_door(player, 'Eastern Lobby NE', Intr).dir(No, 0xc9, Right, High).pos(3), create_door(player, 'Eastern Lobby Right Ledge SE', Intr).dir(So, 0xc9, Right, High).pos(3), - create_door(player, 'Eastern Lobby Bridge N', Nrml).dir(No, 0xc9, Mid, High).pos(1), + create_door(player, 'Eastern Lobby Bridge N', Nrml).dir(No, 0xc9, Mid, High).pos(1).trap(0x2), create_door(player, 'Eastern Cannonball S', Nrml).dir(So, 0xb9, Mid, High).pos(2), create_door(player, 'Eastern Cannonball N', Nrml).dir(No, 0xb9, Mid, High).pos(1), create_door(player, 'Eastern Cannonball Ledge WN', Nrml).dir(We, 0xb9, Top, High).pos(3), @@ -293,7 +293,7 @@ def create_doors(world, player): create_door(player, 'Tower Circle of Pots WS', Intr).dir(We, 0xb0, Bot, High).small_key().pos(2), create_door(player, 'Tower Pacifist Run ES', Intr).dir(Ea, 0xb0, Bot, High).small_key().pos(2), create_door(player, 'Tower Pacifist Run Up Stairs', Sprl).dir(Up, 0xb0, 2, LTH).ss(X, 0x33, 0x8c, True, True), - create_door(player, 'Tower Push Statue Down Stairs', Sprl).dir(Dn, 0x40, 0, HTL).ss(X, 0x12, 0xa0, True, True), + create_door(player, 'Tower Push Statue Down Stairs', Sprl).dir(Dn, 0x40, 0, HTL).ss(X, 0x12, 0xa0, True, True).kill(), create_door(player, 'Tower Push Statue WS', Intr).dir(We, 0x40, Bot, Low).pos(0), create_door(player, 'Tower Catwalk ES', Intr).dir(Ea, 0x40, Bot, Low).pos(0), create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 9685e20a..8b521be8 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1124,6 +1124,8 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): # blue barriers assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) # polarity: + if not globally_valid(dungeon_map, None, [], polarized_sectors): + 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) # the rest @@ -1174,13 +1176,7 @@ def assign_sector(sector, dungeon, candidate_sectors): dungeon.bk_required = True if sector.bk_provided: dungeon.bk_provided = True - for door in sector.outstanding_doors: - # todo: destination sectors like skull 2 west should be - if (door.blocked or door.dead or sector.adj_outflow() <= 1) and not sector.is_entrance_sector(): - dungeon.conn_needed[hook_from_door(door)] += 1 - # todo: stonewall - else: # todo: dungeons that need connections... skull, tr, hc, desert (when edges are done) - dungeon.conn_supplied[hanger_from_door(door)] += 1 + count_conn_needed_supplied(sector, dungeon.conn_needed, dungeon.conn_supplied) factor = sector.branching_factor() if factor <= 1: dungeon.dead_ends += 1 @@ -1188,6 +1184,16 @@ def assign_sector(sector, dungeon, candidate_sectors): dungeon.branches += factor - 2 +def count_conn_needed_supplied(sector, conn_needed, conn_supplied): + for door in sector.outstanding_doors: + # todo: destination sectors like skull 2 west should be + if (door.blocked or door.dead or sector.adj_outflow() <= 1) and not sector.is_entrance_sector(): + conn_needed[hook_from_door(door)] += 1 + # todo: stonewall + else: # todo: dungeons that need connections... skull, tr, hc, desert (when edges are done) + conn_supplied[hanger_from_door(door)] += 1 + + def find_sector(r_name, sectors): for s in sectors: if r_name in s.region_set(): @@ -1307,7 +1313,7 @@ def identify_polarity_issues(dungeon_map): sector_mag = sector.magnitude() check_flags(sector_mag, connection_flags) for i in PolSlot: - if sector_mag[i.value] > 0 and other_mag[i.value] == 0: + 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 @@ -1322,6 +1328,10 @@ def identify_polarity_issues(dungeon_map): return unconnected_builders +def self_connecting(sector, slot, magnitude): + return sector.polarity()[slot.value] == 0 and sum(magnitude) > magnitude[slot.value] + + def check_flags(sector_mag, connection_flags): for slot in PolSlot: for slot2 in PolSlot: @@ -1334,14 +1344,6 @@ def check_flags(sector_mag, connection_flags): connection_flags[check_slot][slot] = True -class DoorInfo: - - def __init__(self, dependents, entrance_flag): - self.dependents = dependents - self.original = dependents.copy() - self.entrance_flag = entrance_flag - - def identify_simple_branching_issues(dungeon_map): problem_builders = {} for name, builder in dungeon_map.items(): @@ -1358,181 +1360,6 @@ def identify_simple_branching_issues(dungeon_map): return problem_builders -def identify_branching_issues(dungeon_map): - unconnected_builders = {} - for name, builder in dungeon_map.items(): - unsatisfied_doors = defaultdict(dict) - satisfying_doors = defaultdict(dict) - entrance_doors = defaultdict(dict) - multi_purpose = defaultdict(dict) - impossible_doors = defaultdict(list) - for sector in builder.sectors: - is_entrance = is_entrance_sector(builder, sector) - if is_entrance: - other_doors = {} - one_way_flag = False - for door in sector.outstanding_doors: - door_info = find_dependent_doors(door, sector) - if door_info.entrance_flag: - get_dict(entrance_doors, door)[door] = door_info - elif door.blocked or door.dead or len(door_info.dependents) == 0: - get_dict(unsatisfied_doors, door)[door] = door_info - one_way_flag = True - else: - other_doors[door] = door_info - if not one_way_flag: - for door, info in other_doors.items(): - get_dict(multi_purpose, door)[door] = info - else: - for door, info in other_doors.items(): - get_dict(satisfying_doors, door)[door] = info - 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: - door_info = find_dependent_doors(door, sector) - if door.blocked or door.dead or (outflow <= 1 and len(door_info.dependents) == 0): - get_dict(unsatisfied_doors, door)[door] = door_info - one_way_flag = True - else: - other_doors[door] = door_info - if not one_way_flag and outflow >= 2: - for door, info in other_doors.items(): - get_dict(multi_purpose, door)[door] = info - elif one_way_flag or outflow <= 1: - for door, info in other_doors.items(): - get_dict(satisfying_doors, door)[door] = info - used_doors = [] - satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose]) - while not satisfied: - candidate_is_unsated = True - candidate, info = choose_candidate([unsatisfied_doors], builder) - if candidate is None: - candidate_is_unsated = False - candidate, info = choose_candidate([multi_purpose, satisfying_doors, entrance_doors], builder) - match_list = [satisfying_doors, multi_purpose, entrance_doors] - # todo: this can pick a bad one - even though the dungeon is fine - match_maker, match_info = find_candidate_match(candidate, info, candidate_is_unsated, match_list) - if match_maker is None: - impossible_doors[hook_from_door(candidate)].append(candidate) - else: - used_doors.append((candidate, match_maker)) - update_dependents(candidate, match_maker, match_list, info, match_info, unsatisfied_doors) - cull_dependents(match_info, match_list, match_maker, info, unsatisfied_doors) - cull_dependents(info, match_list, candidate, match_info, unsatisfied_doors) - - if branches_in_trouble(match_list): - impossible_doors[hook_from_door(candidate)].append(candidate) - impossible_doors[hook_from_door(match_maker)].append(match_maker) - satisfied = True # early break out - continue - satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose]) - if len(impossible_doors) > 0: - unconnected_builders[name] = builder - for hook, door_list in impossible_doors.items(): - builder.unfulfilled[hook] += len(door_list) - return unconnected_builders - - -def get_dict(m_dict, key) -> dict: - return m_dict[hook_from_door(key)] - - -def find_dependent_doors(door, sector): - dependent_doors = [] - start_region = door.entrance.parent_region - visited = {start_region} - queue = deque([start_region]) - if len(door.dependents) > 0: - for dep_door in door.dependents: - next_region = dep_door.entrance.parent_region - visited.add(next_region) - queue.append(next_region) - found_events = set() - event_doors = set() - entrance_flag = False - while len(queue) > 0: - region = queue.popleft() - for loc in region.locations: - if loc.name in dungeon_events: - found_events.add(loc.name) - for ext in region.exits: - d = ext.door - if d is not None and d is not door and d in sector.outstanding_doors: - dependent_doors.append(d) - if d is not None and len(d.dependents) > 0: - for dep_door in d.dependents: - next_region = dep_door.entrance.parent_region - if next_region not in visited: - visited.add(next_region) - queue.append(next_region) - for ent in region.entrances: - next = ent.parent_region - d = ent.door - if d is not None: - if d.req_event is not None: - event_doors.add(d) - elif next.type == RegionType.Dungeon and next not in visited: - visited.add(next) - queue.append(next) - if not door.blocked and (next.type in [RegionType.LightWorld, RegionType.DarkWorld] or next.name == 'Sewer Drop'): - entrance_flag = True - if len(queue) == 0: - for d in event_doors: - next = d.entrance.parent_region - if d.req_event not in found_events and next not in visited and next.type == RegionType.Dungeon: - visited.add(next) - queue.append(next) - return DoorInfo(dependent_doors, entrance_flag) - - -def cull_dependents(info, lists_to_cull, door_to_cull, match_info, unsatisfied_doors): - for door in info.dependents: - for list_to_cull in lists_to_cull: - door_list = list_to_cull[hook_from_door(door)] - match_dep, rev_info = find_door_in_list(door, door_list) - if match_dep is not None: - if match_info.entrance_flag: - door_list[match_dep].entrance_flag = True - # todo: this may need to propagate via used_doors - if door_to_cull in rev_info.dependents: - rev_info.dependents = [x for x in rev_info.dependents if x != door_to_cull] - if len(rev_info.dependents) < 1 and not rev_info.entrance_flag: - del door_list[match_dep] - unsatisfied_doors[hook_from_door(door)][match_dep] = rev_info - - -def update_dependents(candidate, match_maker, match_list, cand_info, match_info, candidate_is_unsated): - for match_map in match_list: - for h_type, sub_map in match_map.items(): - for door, info in sub_map.items(): - if candidate in info.dependents: - info.dependents = [x for x in info.dependents if x != candidate] - info.dependents = list(set().union(info.dependents, match_info.dependents)) - if match_info.entrance_flag: - info.entrance_flag = True - if match_maker in info.dependents: - info.dependents = [x for x in info.dependents if x != match_maker] - info.dependents = list(set().union(info.dependents, cand_info.dependents)) - if cand_info.entrance_flag: - info.entrance_flag = True - - -def propagate_flag(match_list, used_doors): - pass - - -def branches_in_trouble(match_list): - for match_map in match_list: - for h_type, sub_map in match_map.items(): - for door, info in sub_map.items(): - if info.entrance_flag: - return False - return not is_satisfied(match_list) # could find no more entrance flags - so hopefully we are done - - def is_entrance_sector(builder, sector): for entrance in builder.all_entrances: r_set = sector.region_set() @@ -1549,67 +1376,6 @@ def is_satisfied(door_dict_list): return True -def choose_candidate(door_dict_list, builder): - for door_dict in door_dict_list: - min_constraint = None - candidate_list = None - for dir, door_list in door_dict.items(): - curr_constaint = builder.conn_supplied[dir] - builder.conn_needed[dir] - if curr_constaint == 0: - curr_constaint = -builder.conn_needed[dir] - if len(door_list) > 0 and (min_constraint is None or curr_constaint < min_constraint): - candidate_list = door_list - min_constraint = curr_constaint - if min_constraint is not None: - candidate, info = next(iter(candidate_list.items())) - del candidate_list[candidate] - return candidate, info - return None, None - - -def find_candidate_match(candidate, info, 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_list = [] - for match, match_info in door_list.items(): - if not check_deps or (match not in info.dependents and candidate not in match_info.dependents): - pair_list.append((match, match_info)) - elif len(filter_match_deps(candidate, match_info.dependents)) > 0: - backup_pair = match, match_info - backup_list = door_list - if len(pair_list) > 0: - pair = pick_most_dependent(pair_list) - del door_list[pair[0]] - return pair - if backup_pair is not None: - del backup_list[backup_pair[0]] - if not backup_pair[1].entrance_flag and len(backup_pair[1].dependents) <= 1: - logging.getLogger('').debug('Matching %s to %s unsure if safe', candidate, backup_pair[0]) - return backup_pair - return None, None - - -def pick_most_dependent(pair_list): - most_dependents = None - chosen_pair = None - for pair in pair_list: - num_dependents = len(pair[1].dependents) - if most_dependents is None or num_dependents > most_dependents: - most_dependents = num_dependents - chosen_pair = pair - return chosen_pair - - -def find_door_in_list(door, door_list): - for d, info in door_list.items(): - if d == door: - return d, info - 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): @@ -1642,6 +1408,11 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): 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) + if len(candidates) == 0: + raise Exception('Cross Dungeon Builder: Cannot find a candidate for connectedness - globally invalid') + sector = random.choice(candidates) assign_sector(sector, builder, polarized_sectors) builder.mag_needed = {} unconnected_builders = identify_polarity_issues(unconnected_builders) @@ -1653,26 +1424,18 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): 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]): + if valid_connected_assignment(builder, [choice]) and globally_valid(dungeon_map, builder, [choice], polarized_sectors): assign_sector(choice, builder, polarized_sectors) builder.total_conn_lack = 0 builder.conn_balance.clear() problem_builders = identify_simple_branching_issues(problem_builders) # step 3: 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, polarized_sectors) - sectors = random.choice(candidates) - for sector in sectors: - assign_sector(sector, builder, polarized_sectors) + polarity_step_3(dungeon_map, polarized_sectors, logger) # step 4: fix dead ends again - problem_builders = identify_branching_issues(dungeon_map) neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) + problem_builders = identify_branching_issues_2(dungeon_map) while len(problem_builders) > 0: for name, builder in problem_builders.items(): candidates = find_branching_candidates(builder, neutral_choices) @@ -1685,16 +1448,72 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): for sector in choice: assign_sector(sector, builder, polarized_sectors) builder.unfulfilled.clear() - problem_builders = identify_branching_issues(problem_builders) + problem_builders = identify_branching_issues_2(problem_builders) # step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc. + tries = 0 while len(polarized_sectors) > 0: + if tries > 100: + raise Exception('No valid assignment found') choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices)) + valid = [] for i, choice in enumerate(choices): builder = dungeon_map[choice] - if valid_polarized_assignment(builder, neutral_choices[i]): + if valid_assignment(builder, neutral_choices[i]): for sector in neutral_choices[i]: assign_sector(sector, builder, polarized_sectors) + valid.append(neutral_choices[i]) + for c in valid: + neutral_choices.remove(c) + tries += 1 + + +def polarity_step_3(dungeon_map, polarized_sectors, 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) + if len(candidates) == 0: + raise NeutralizingException('Unable to find a globally valid neutralizer') + sectors = random.choice(candidates) + for sector in sectors: + assign_sector(sector, builder, polarized_sectors) + + +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: + 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 or slot == PolSlot.Stairs: + if positives[slot.value] >= -polarity[slot.value]: + positives[slot.value] += polarity[slot.value] + else: + return False + return True def find_connection_candidates(mag_needed, sector_pool): @@ -1880,6 +1699,12 @@ def neutralize_the_rest(sector_pool): return neutral_choices +def valid_assignment(builder, sector_list): + if not valid_polarized_assignment(builder, sector_list): + return False + 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: @@ -1938,6 +1763,18 @@ def split_dungeon_builder(builder, split_list): def balance_split(candidate_sectors, dungeon_map): logger = logging.getLogger('') # categorize sectors + crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors, dungeon_map) + # blue barriers + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) + # polarity: + logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') + assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + # the rest + assign_the_rest(dungeon_map, neutral_sectors) + return dungeon_map + + +def categorize_sectors(candidate_sectors, dungeon_map): crystal_switches = {} crystal_barriers = {} polarized_sectors = {} @@ -1957,14 +1794,7 @@ def balance_split(candidate_sectors, dungeon_map): neutral_sectors[sector] = None else: polarized_sectors[sector] = None - # blue barriers - assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) - # polarity: - logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') - assign_polarized_sectors(dungeon_map, polarized_sectors, logger) - # the rest - assign_the_rest(dungeon_map, neutral_sectors) - return dungeon_map + return crystal_barriers, neutral_sectors, polarized_sectors def stonewall_dungeon_builder(builder, stonewall, entrance_region_names): @@ -1981,46 +1811,69 @@ def stonewall_dungeon_builder(builder, stonewall, entrance_region_names): stonewall_connector = Sector() stonewall_start.outstanding_doors.append(stonewall) stonewall_start.regions.append(region) + stonewall_start.entrance_sector = True + stonewall_start.branch_factor = 2 stonewall_connector.outstanding_doors += [x for x in sector.outstanding_doors if x != stonewall] stonewall_connector.regions += [x for x in sector.regions if x != region] define_sector_features([stonewall_connector, stonewall_start]) candidate_sectors[stonewall_start] = None candidate_sectors[stonewall_connector] = None - stone_builder = create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors) - origin_builder = create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors) + 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) # dependent sector splits - dependency_list = [] - removal = [] - for sector in candidate_sectors.keys(): - dependency = split_sector(sector) - if dependency is not None: - removal.append(sector) - dependency_list.append(dependency) - for sector in removal: - del candidate_sectors[sector] - retry_candidates = candidate_sectors.copy() - tries = 0 - while tries < 10: - try: - # re-assign dependent regions - for parent, child in dependency_list: - candidate_sectors[parent] = None - candidate_sectors[child] = None - chosen_builder = random.choice([stone_builder, origin_builder]) - assign_sector(child, chosen_builder, candidate_sectors) - if chosen_builder == stone_builder: - assign_sector(parent, chosen_builder, candidate_sectors) - return balance_split(candidate_sectors, dungeon_map) - except NeutralizingException: - tries += 1 - candidate_sectors = retry_candidates.copy() - candidate_sectors[stonewall_start] = None - candidate_sectors[stonewall_connector] = None - stone_builder = create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors) - origin_builder = create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors) + # dependency_list = [] + # removal = [] + # for sector in candidate_sectors.keys(): + # dependency = split_sector(sector) + # if dependency is not None: + # removal.append(sector) + # parent, child = dependency + # dependency_list.append((parent, child, sector)) + # for sector in removal: + # del candidate_sectors[sector] + # retry_candidates = candidate_sectors.copy() + # tries = 0 + # while tries < 10: + # try: + # # re-assign dependent regions + # for parent, child, original in dependency_list: + # chosen_builder = random.choice([stone_builder, origin_builder, None]) + # if chosen_builder is None: + # candidate_sectors[original] = None + # elif chosen_builder == stone_builder: + # candidate_sectors[original] = None + # assign_sector(original, chosen_builder, candidate_sectors) + # else: + # candidate_sectors[parent] = None + # candidate_sectors[child] = None + # assign_sector(child, chosen_builder, candidate_sectors) + # return stonewall_split(candidate_sectors, dungeon_map) + # except NeutralizingException: + # tries += 1 + # candidate_sectors = retry_candidates.copy() + # candidate_sectors[stonewall_start] = None + # candidate_sectors[stonewall_connector] = None + # stone_builder = create_stone_builder(builder, dungeon_map, region, stonewall_start, candidate_sectors) + # origin_builder = create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors) + # + # raise NeutralizingException('Unable to find a valid combination') - raise NeutralizingException('Unable to find a valid combination') + +def stonewall_split(candidate_sectors, dungeon_map): + logger = logging.getLogger('') + # categorize sectors + crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors, dungeon_map) + # blue barriers + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) + # 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) + # the rest + assign_the_rest(dungeon_map, neutral_sectors) + return dungeon_map def create_origin_builder(builder, dungeon_map, entrance_region_names, stonewall_connector, candidate_sectors): @@ -2047,67 +1900,311 @@ def create_stone_builder(builder, dungeon_map, region, stonewall_start, candidat 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) return stone_builder -def split_sector(sector): - entrance_set = set() - exit_map = {} - visited_regions = {} - min_cardinality = None - for door in sector.outstanding_doors: - reachable_doors = {door} - start_region = door.entrance.parent_region - visited = {start_region} - queue = deque([start_region]) - while len(queue) > 0: - region = queue.popleft() - for ext in region.exits: - connect = ext.connected_region - if connect is not None and connect.type == RegionType.Dungeon and connect not in visited: - visited.add(connect) - queue.append(connect) - elif ext.door in sector.outstanding_doors: - reachable_doors.add(ext.door) - visited_regions[door] = visited - if len(reachable_doors) >= len(sector.outstanding_doors): - entrance_set.add(door) - else: - door_cardinality = len(reachable_doors) - if door_cardinality not in exit_map.keys(): - exit_map[door_cardinality] = set() - exit_map[door_cardinality].add(door) - if min_cardinality is None or door_cardinality < min_cardinality: - min_cardinality = door_cardinality - exit_set = set() - if min_cardinality is not None: - for cardinality, door_set in exit_map.items(): - if cardinality > min_cardinality: - entrance_set.update(door_set) - exit_set = exit_map[min_cardinality] - if len(entrance_set) > 0 and len(exit_set) > 0: - entrance_sector = Sector() - exit_sector = Sector() - entrance_sector.outstanding_doors.extend(entrance_set) - region_set = set() - for ent_door in entrance_set: - region_set.update(visited_regions[ent_door]) - entrance_sector.regions.extend(region_set) - exit_sector.outstanding_doors.extend(exit_set) - region_set = set() - for ext_door in exit_set: - region_set.update(visited_regions[ext_door]) - exit_sector.regions.extend(region_set) - define_sector_features([entrance_sector, exit_sector]) - return entrance_sector, exit_sector - return None +# def split_sector(sector): +# entrance_set = set() +# exit_map = {} +# visited_regions = {} +# min_cardinality = None +# for door in sector.outstanding_doors: +# reachable_doors = {door} +# start_region = door.entrance.parent_region +# visited = {start_region} +# queue = deque([start_region]) +# while len(queue) > 0: +# region = queue.popleft() +# for ext in region.exits: +# connect = ext.connected_region +# if connect is not None and connect.type == RegionType.Dungeon and connect not in visited: +# visited.add(connect) +# queue.append(connect) +# elif ext.door in sector.outstanding_doors: +# reachable_doors.add(ext.door) +# visited_regions[door] = visited +# if len(reachable_doors) >= len(sector.outstanding_doors): +# entrance_set.add(door) +# else: +# door_cardinality = len(reachable_doors) +# if door_cardinality not in exit_map.keys(): +# exit_map[door_cardinality] = set() +# exit_map[door_cardinality].add(door) +# if min_cardinality is None or door_cardinality < min_cardinality: +# min_cardinality = door_cardinality +# exit_set = set() +# if min_cardinality is not None: +# for cardinality, door_set in exit_map.items(): +# if cardinality > min_cardinality: +# entrance_set.update(door_set) +# exit_set = exit_map[min_cardinality] +# if len(entrance_set) > 0 and len(exit_set) > 0: +# entrance_sector = Sector() +# exit_sector = Sector() +# entrance_sector.outstanding_doors.extend(entrance_set) +# region_set = set() +# for ent_door in entrance_set: +# region_set.update(visited_regions[ent_door]) +# entrance_sector.regions.extend(region_set) +# exit_sector.outstanding_doors.extend(exit_set) +# region_set = set() +# for ext_door in exit_set: +# region_set.update(visited_regions[ext_door]) +# exit_sector.regions.extend(region_set) +# define_sector_features([entrance_sector, exit_sector]) +# return entrance_sector, exit_sector +# return None class NeutralizingException(Exception): pass +class DoorEquation: + + def __init__(self, door): + self.door = door + self.cost = defaultdict(list) + self.benefit = defaultdict(list) + self.required = False + + def copy(self): + eq = DoorEquation(self.door) + for key, doors in self.cost.items(): + eq.cost[key] = doors.copy() + for key, doors in self.benefit.items(): + eq.benefit[key] = doors.copy() + eq.required = self.required + return eq + + def total_cost(self): + ttl = 0 + for key, door_list in self.cost.items(): + ttl += len(door_list) + return ttl + + def profit(self): + ttl = 0 + for key, door_list in self.benefit.items(): + ttl += len(door_list) + return ttl - self.total_cost() + + def neutral(self): + for key in Hook: + if len(self.cost[key]) != len(self.benefit[key]): + return False + return True + + def can_cover_cost(self, current_access): + for key, door_list in self.cost.items(): + if len(door_list) > current_access[key]: + return False + return True + + +def identify_branching_issues_2(dungeon_map): + unconnected_builders = {} + for name, builder in dungeon_map.items(): + unreached_doors = resolve_equations(builder, []) + if len(unreached_doors) > 0: + unconnected_builders[name] = builder + for hook, door_list in unreached_doors.items(): + builder.unfulfilled[hook] += len(door_list) + return unconnected_builders + + +def resolve_equations(builder, sector_list): + unreached_doors = defaultdict(list) + equations = copy_door_equations(builder, sector_list) + current_access = defaultdict(int) + reached_doors = set() + # resolve all that provide more access + free_sector, eq_list, free_eq = find_free_equation(equations) + while free_eq is not None: + resolve_equation(free_eq, eq_list, free_sector, current_access, reached_doors, equations) + free_sector, eq_list, free_eq = find_free_equation(equations) + while len(equations) > 0: + eq, eq_list, sector = find_priority_equation(equations, current_access) + if eq is not None: + resolve_equation(eq, eq_list, sector, current_access, reached_doors, equations) + else: + for sector, eq_list in equations.items(): + for eq in eq_list: + unreached_doors[hook_from_door(eq.door)].append(eq.door) + return unreached_doors + return unreached_doors + + +# an equations with no change to access (check) +# the highest benefit equations, that can be paid for (check) +# 0-benefit required transforms +# 0-benefit transforms (how to pick between these?) +# negative benefit transforms (dead end) +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 + for sector, eq_list in equations.items(): + eq_list.sort(key=lambda eq: eq.profit(), reverse=True) + best_local_profit = None + for eq in eq_list: + profit = eq.profit() + if best_local_profit is None or profit > best_local_profit: + best_local_profit = profit + 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 + + +def calc_flex(equations, current_access): + flex_spending = defaultdict(int) + required_costs = defaultdict(int) + for sector, eq_list in equations.items(): + for eq in eq_list: + if eq.required: + for key, door_list in eq.cost.items(): + required_costs[key] += len(door_list) + for key in Hook: + flex_spending[key] = max(0, current_access[key]-required_costs[key]) + return flex_spending + + +def resolve_equation(equation, eq_list, sector, current_access, reached_doors, equations): + for key, door_list in equation.cost.items(): + if current_access[key] - len(door_list) < 0: + raise Exception('Cannot pay for this connection') + current_access[key] -= len(door_list) + for door in door_list: + reached_doors.add(door) + for key, door_list in equation.benefit.items(): + current_access[key] += len(door_list) + for door in door_list: + reached_doors.add(door) + eq_list.remove(equation) + removing = [x for x in eq_list if x.door in reached_doors] + for r_eq in removing: + all_benefits_met = True + for key in Hook: + fringe_list = [x for x in r_eq.benefit[key] if x not in reached_doors] + if len(fringe_list) > 0: + all_benefits_met = False + r_eq.benefit[key] = fringe_list + if all_benefits_met: + eq_list.remove(r_eq) + if len(eq_list) == 0: + del equations[sector] + + +def find_free_equation(equations): + for sector, eq_list in equations.items(): + for eq in eq_list: + if eq.total_cost() <= 0: + return sector, eq_list, eq + return None, None, None + + +def copy_door_equations(builder, sector_list): + equations = {} + for sector in builder.sectors + sector_list: + if sector.equations is None: + #todo: sort equations? + sector.equations = calc_sector_equations(sector, builder) + curr_list = equations[sector] = [] + for equation in sector.equations: + curr_list.append(equation.copy()) + return equations + + +def calc_sector_equations(sector, builder): + equations = [] + is_entrance = is_entrance_sector(builder, sector) + if is_entrance: + flagged_equations = [] + for door in sector.outstanding_doors: + equation, flag = calc_door_equation(door, sector, True) + if flag: + flagged_equations.append(equation) + equations.append(equation) + for flagged_equation in flagged_equations: + for equation in equations: + for key, door_list in equation.benefit.items(): + if flagged_equation.door in door_list and flagged_equation != equation: + door_list.remove(flagged_equation.door) + else: + for door in sector.outstanding_doors: + equation, flag = calc_door_equation(door, sector, False) + equations.append(equation) + return equations + + +def calc_door_equation(door, sector, look_for_entrance): + if look_for_entrance and not door.blocked: + flag = sector.is_entrance_sector() + if flag: + eq = DoorEquation(door) + eq.benefit[hook_from_door(door)].append(door) + eq.required = True + return eq, flag + eq = DoorEquation(door) + eq.required = door.blocked or door.dead + eq.cost[hanger_from_door(door)].append(door) + if not door.stonewall: + start_region = door.entrance.parent_region + visited = {start_region} + queue = deque([start_region]) + found_events = set() + event_doors = set() + while len(queue) > 0: + region = queue.popleft() + for loc in region.locations: + if loc.name in dungeon_events: + found_events.add(loc.name) + for d in event_doors: + if loc.name == d.req_event: + connect = d.entrance.connected_region + if connect is not None and connect.type == RegionType.Dungeon and connect not in visited: + visited.add(connect) + queue.append(connect) + for ext in region.exits: + d = ext.door + if d is not None: + if d.controller is not None: + d = d.controller + if d is not door and d in sector.outstanding_doors and not d.blocked: + eq_list = eq.benefit[hook_from_door(d)] + if d not in eq_list: + eq_list.append(d) + if d.req_event is not None and d.req_event not in found_events: + event_doors.add(d) + else: + connect = ext.connected_region + if connect is not None and connect.type == RegionType.Dungeon and connect not in visited: + visited.add(connect) + queue.append(connect) + if len(eq.benefit) == 0: + eq.required = True + return eq, False + + # common functions - todo: move to a common place def kth_combination(k, l, r): if r == 0: diff --git a/Main.py b/Main.py index 448ec551..9dace756 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.9-pre' +__version__ = '0.0.a-pre' def main(args, seed=None):