From 2eb6c1ebc0b67e64ed741df85a5bc612a2dc3e01 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 13 Aug 2020 16:23:40 -0600 Subject: [PATCH] Crossed Dungeon generation work -Added a bunch of crystal switch logic -Added a check for maiden backtracking -OrderedFrozenSet to prevent randomization leaks with the same seed -A few key improvements from an outstanding PR --- BaseClasses.py | 1 + DoorShuffle.py | 2 +- DungeonGenerator.py | 790 +++++++++++++++++++-------------- KeyDoorShuffle.py | 20 +- resources/app/cli/lang/en.json | 2 +- 5 files changed, 464 insertions(+), 351 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0d66f339..a711189e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1120,6 +1120,7 @@ pol_comp = { 'Mod': lambda x: 0 if x == 0 else 1 } + @unique class PolSlot(Enum): NorthSouth = 0 diff --git a/DoorShuffle.py b/DoorShuffle.py index f096667a..ccbbefc8 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -764,7 +764,7 @@ def find_small_key_door_candidates(builder, start_regions, world, player): checked_doors = set() for region in start_regions: possible, checked = find_key_door_candidates(region, checked_doors, world, player) - candidates.extend(possible) + candidates.extend([x for x in possible if x not in candidates]) checked_doors.update(checked) flat_candidates = [] for candidate in candidates: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 8adb87f3..fcb6a083 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -43,7 +43,7 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): bk_special = bk_special or check_for_special(sector) dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) - return check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, False) + return check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, False, False) def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player): @@ -69,7 +69,7 @@ def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player) queue = collections.deque(proposed_map.items()) while len(queue) > 0: - a, b = queue.pop() + a, b = queue.popleft() connect_doors(a, b) queue.remove((b, a)) available_sectors = list(builder.sectors) @@ -105,6 +105,7 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon finished = False # flag if standard and this is hyrule castle std_flag = world.mode[player] == 'standard' and bk_special + maiden_flag = name == 'Thieves Town' while not finished: # what are my choices? itr += 1 @@ -123,7 +124,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) dungeon_cache[depth] = dungeon, hangers, hooks - valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, std_flag) + valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, + bk_needed, std_flag, maiden_flag) else: dungeon, hangers, hooks = dungeon_cache[depth] valid = True @@ -167,7 +169,6 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon return proposed_map - def determine_if_bk_needed(sector, split_dungeon, world, player): if not split_dungeon: for region in sector.regions: @@ -338,7 +339,7 @@ def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name): hookable_hangers = collections.deque() queue = collections.deque(candidate_hangers) while len(queue) > 0: - c_hang = queue.pop() + c_hang = queue.popleft() if c_hang in all_hooks.keys(): hookable_hangers.append(c_hang) else: @@ -383,7 +384,8 @@ def filter_choices(next_hanger, door, orig_hang, prev_choices, hook_candidates): return next_hanger != door and orig_hang != next_hanger and door not in hook_candidates -def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, std_flag): +def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, + bk_needed, std_flag, maiden_flag): # evaluate if everything is still plausible # only origin is left in the dungeon and not everything is connected @@ -431,6 +433,8 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg return False if not bk_possible: return False + if maiden_flag and not maiden_valid(doors_to_connect, all_regions, proposed_map): + return False if std_flag and not cellblock_valid(doors_to_connect, all_regions, proposed_map): return False new_hangers_found = True @@ -460,6 +464,39 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg return len(all_hangers.difference(hanger_matching)) == 0 +# todo: combine these two search methods +def maiden_valid(valid_doors, all_regions, proposed_map): + cellblock = None + for region in all_regions: + if "Thieves Blind's Cell" == region.name: + cellblock = region + break + queue = deque([cellblock]) + visited = {cellblock} + while len(queue) > 0: + region = queue.popleft() + if region.name == 'Thieves Boss': + return True + for ext in region.exits: + connect = ext.connected_region + if connect is None and ext.name in valid_doors: + door = valid_doors[ext.name] + if not door.blocked: + if door in proposed_map: + new_region = proposed_map[door].entrance.parent_region + if new_region not in visited: + visited.add(new_region) + queue.append(new_region) + else: + return True # outstanding connection possible + elif connect is not None: + door = ext.door + if door is not None and not door.blocked and connect not in visited: + visited.add(connect) + queue.append(connect) + return False # couldn't find an outstanding door or Blind + + def cellblock_valid(valid_doors, all_regions, proposed_map): cellblock = None for region in all_regions: @@ -798,7 +835,7 @@ class ExplorationState(object): self.events.add(location_name) queue = collections.deque(self.event_doors) while len(queue) > 0: - exp_door = queue.pop() + exp_door = queue.popleft() if exp_door.door.req_event == location_name: self.avail_doors.append(exp_door) self.event_doors.remove(exp_door) @@ -1102,6 +1139,7 @@ class DungeonBuilder(object): self.valid_proposal = None self.split_dungeon_map = None + self.exception_list = [] def polarity_complement(self): pol = Polarity() @@ -1358,10 +1396,13 @@ def assign_sector_helper(sector, builder): count_conn_needed_supplied(sector, builder.conn_needed, builder.conn_supplied) builder.dead_ends += sector.dead_ends() builder.branches += sector.branches() - if builder.split_dungeon_map: - builder.split_dungeon_map = None - if builder.valid_proposal: - builder.valid_proposal = None + if sector in builder.exception_list: + builder.exception_list.remove(sector) + else: + if builder.split_dungeon_map: + builder.split_dungeon_map = None + if builder.valid_proposal: + builder.valid_proposal = None def count_conn_needed_supplied(sector, conn_needed, conn_supplied): @@ -1532,7 +1573,7 @@ def is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors): if eq.total_cost() <= 0: for key, door_list in eq.benefit.items(): for door in door_list: - if door not in eq.blue_blocked: + if door not in eq.crystal_blocked.keys() or eq.crystal_blocked[door] != CrystalBarrier.Blue: current_access[key] = True break for key, flag in current_access.items(): @@ -1543,11 +1584,13 @@ def is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors): changed = False for sector in other_sectors: for eq in sector.equations: - for key, door_list in eq.cost.items(): - if key in current_access.keys() and current_access[key]: - for bene_key, door_list in eq.benefit.items(): - for door in door_list: - if door not in eq.blue_blocked and bene_key not in current_access.keys(): + key, cost_door = eq.cost + if key in current_access.keys() and current_access[key]: + for bene_key, door_list in eq.benefit.items(): + for door in door_list: + block_dict = eq.crystal_blocked + if door not in block_dict.keys() or block_dict[door] != CrystalBarrier.Blue: + if bene_key not in current_access.keys(): current_access[bene_key] = True changed = True break @@ -1569,11 +1612,12 @@ def pol_cand_matches_access_reach(sector, access, reachable_crystals): if sector.equations is None: sector.equations = calc_sector_equations(sector, None) for eq in sector.equations: - for key, door_list in eq.cost.items(): - if key in access.keys() and access[key]: - for bene_key, door_list in eq.benefit.items(): - for door in door_list: - if door not in eq.blue_blocked and opposite_h_type(bene_key) in reachable_crystals.keys(): + key, cost_door = eq.cost + if key in access.keys() and access[key]: + for bene_key, door_list in eq.benefit.items(): + for door in door_list: + if door not in eq.crystal_blocked.keys() or eq.crystal_blocked[door] != CrystalBarrier.Blue: + if opposite_h_type(bene_key) in reachable_crystals.keys(): return True return False @@ -1590,9 +1634,9 @@ def crystal_cand_matches_access(sector, access): if sector.equations is None: sector.equations = calc_sector_equations(sector, None) for eq in sector.equations: - for key, door_list in eq.cost.items(): - if key in access.keys() and access[key] and eq.c_switch: - return True + key, cost_door = eq.cost + if key in access.keys() and access[key] and eq.c_switch and len(sector.outstanding_doors) > 1: + return True return False @@ -1904,62 +1948,6 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole): parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole) -def neutralization_old_method(dungeon_map, polarized_sectors, global_pole): - builder_order = list(dungeon_map.values()) - random.shuffle(builder_order) - - def max_deviation_polarity(bldr): - max = 0 - for x in bldr.polarity().vector: - if abs(x) > max: - max = abs(x) - return max - builder_order.sort(key=max_deviation_polarity) - - finished = all([x.polarity().is_neutral() for x in builder_order]) - solution_list = defaultdict(list) - unchosen_sectors = list(polarized_sectors) - tries = 0 - while not finished: - if tries > 10: - raise NeutralizingException('Unable to find globally valid neutralizers.') - try: - for builder in builder_order: - chosen_sectors = solution_list[builder] - while not (builder.polarity()+sum_polarity(chosen_sectors)).is_neutral(): - rejects = [] - start = time.process_time() - candidates = find_neutralizing_candidates(builder, chosen_sectors, unchosen_sectors, rejects) - logging.getLogger('').debug(f'{builder.name} took {time.process_time()-start}') - valid, sectors = False, None - while not valid: - if len(candidates) == 0: - candidates = find_neutralizing_candidates(builder, chosen_sectors, unchosen_sectors, rejects) - if len(candidates) == 0: - raise NeutralizingException('Unable to find a globally valid neutralizer: %s' % builder.name) - sectors = random.choice(candidates) - candidates.remove(sectors) - proposal = solution_list.copy() - proposal[builder] = list(proposal[builder]) - proposal[builder].extend(sectors) - valid = global_pole.is_valid_multi_choice_2(dungeon_map, builder_order, proposal) - if not valid: - rejects.append(sectors) - solution_list[builder].extend(sectors) - for sector in sectors: - unchosen_sectors.remove(sector) - finished = all([(x.polarity()+sum_polarity(solution_list[x])).is_neutral() for x in builder_order]) - except NeutralizingException: - finished = False - if not finished: - tries += 1 - solution_list = defaultdict(list) - unchosen_sectors = list(polarized_sectors) - # for builder, sectors in solution_list.items(): - # for sector in sectors: - # assign_sector(sector, builder, polarized_sectors, global_pole) - - def parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole): start = time.process_time() builders = list(dungeon_map.values()) @@ -2018,7 +2006,7 @@ def find_forced_connections(dungeon_map, candidate_list, polarized_sectors): dungeon_hooks[val] += d_mag[val.value] queue = deque(candidate_list) while queue: - candidate = queue.pop() + candidate = queue.popleft() c_mag = candidate.hook_magnitude() other_candidates = [x for x in candidate_list if x != candidate] for val in Hook: @@ -2264,87 +2252,14 @@ def find_odd_sectors(grouped_candidates): return [x for x in grouped_candidates if sum_polarity(x).charge() % 2 != 0] -# this is related to the perfect sum problem in CS -# there are dynamic programming solution but they involve an increasing order -# maybe order them by (polarity+sum_p(sector)).charge? -# currently this is an iterative deepening solution of the naive 2^n (with n = len(sector_pool)) -def find_exact_neutralizing_candidates(builder, chosen_sectors, sector_pool, rejects): - polarity = builder.polarity() + sum_polarity(chosen_sectors) - orig_charge = polarity.charge() - max_charge = max([x.polarity().charge() for x in sector_pool]) - starting_guess = (orig_charge // max_charge) + (1 if orig_charge % max_charge > 0 else 0) - r, finished, filtered = max(1, starting_guess), False, False - # main_pool = filter_sector_pool(polarity, sector_pool) - main_pool = list(sector_pool) - candidates, official_candidates = [], [] - while not finished: - if r > len(main_pool): - if filtered: - main_pool = list(sector_pool) - filtered = False - r = max(1, starting_guess) - continue - raise NeutralizingException('Possible counter example') - combinations = ncr(len(main_pool), r) - if combinations > 1000000: - logging.getLogger('').warning("Exceeding 1 million combinations, re-examine this bit of the algorithm") - for i in range(0, combinations): - choice = kth_combination(i, main_pool, r) - if (polarity + sum_polarity(choice)).is_neutral() and choice not in rejects: - finished = True - candidates.append(choice) - r += 1 - if finished: - try: - official_candidates = weed_candidates(builder, {0: candidates}, 0) - except NeutralizingException: - finished = False - return official_candidates - - -def find_exact_neutralizing_candidates_parallel(builders, proposal, unchosen_sectors, current_depth): - logger = logging.getLogger('') - candidate_map = defaultdict(list) - polarity_map = {} - for builder in builders: - polarity_map[builder] = builder.polarity() + sum_polarity(proposal[builder]) - main_pool = list(unchosen_sectors) - finished = False - while not finished: - if current_depth > len(main_pool): - raise Exception('Complete traversal - no more solutions found') - combinations = ncr(len(main_pool), current_depth) - logger.debug(f'Combinations {combinations}') - kthcombotime = 0 - othertime = 0 - if combinations > 1000000: - logging.getLogger('').warning("Exceeding 1 million combinations, re-examine this bit of the algorithm") - for i in range(0, combinations): - start = time.process_time() - choice = kth_combination(i, main_pool, current_depth) - kthcombotime += time.process_time() - start - start = time.process_time() - choice_polarity = sum_polarity(choice) - for builder in builders: - if (polarity_map[builder] + choice_polarity).is_neutral(): - finished = True - candidate_map[builder].append(choice) - othertime += time.process_time() - start - logger.debug(f'kthcombotime {kthcombotime} VS. OTHER {othertime}') - if finished: - for builder in builders: - try: - candidate_map[builder] = weed_candidates(builder, {0: candidate_map[builder]}, 0) - except NeutralizingException: - del candidate_map[builder] - if len(candidate_map) == 0: - finished = False - if not finished: - current_depth += 1 - return candidate_map, current_depth - - -# best one so far - db for dynamic programming +# This is related to the perfect sum problem in CS +# * Best algorithm so far - db for dynamic programming +# * Keeps track of unique deviations from neutral in the index +# * Another assumption is that solutions that take fewer sector are more ideal +# * When attempting to add depth and there are no more possibilities, this raises an Exception +# * Each depth should be checked before asking for another one +# An alterative approach would be to trim the db after deciding the candidate at the current depth will be +# part of the propsoal def find_exact_neutralizing_candidates_parallel_db(builders, proposal, avail_sectors, current_depth): candidate_map = defaultdict(list) polarity_map = {} @@ -2374,7 +2289,7 @@ def find_exact_neutralizing_candidates_parallel_db(builders, proposal, avail_sec def create_db_for_depth(depth, avail_sectors): - db = {0: {Polarity(): {frozenset(): None}}} + db = {0: {Polarity(): {OrderedFrozenSet(): None}}} db_index = {Polarity()} for i in range(1, depth+1): add_depth_to_db(db, db_index, i, avail_sectors) @@ -2382,7 +2297,6 @@ def create_db_for_depth(depth, avail_sectors): def add_depth_to_db(db, db_index, i, avail_sectors): - start = time.process_time() previous = db[i-1] depth_map = defaultdict(dict) index_additions = set() @@ -2394,9 +2308,9 @@ def add_depth_to_db(db, db_index, i, avail_sectors): if combo_pol not in db_index: index_additions.add(combo_pol) for choice in choices: - if sector in choice: + if sector in choice.frozen_set: continue - new_set = frozenset(choice | sector_set) + new_set = choice.new_with_element(sector_set) depth_map[combo_pol][new_set] = None for addition in index_additions: if len(depth_map[addition]) > 0: @@ -2404,47 +2318,38 @@ def add_depth_to_db(db, db_index, i, avail_sectors): else: del depth_map[addition] if len(depth_map) == 0: - raise NeutralizingException('There is not a solution for this particular combination. QED') # restart required + raise NeutralizingException('There is not a solution for this particular combination. Crystal switch issue?') # restart required db[i] = depth_map - logging.getLogger('').debug(f'DB depth #{i} calculation took {time.process_time()-start}') -# todo: refactor to return prioritized lists -def find_neutralizing_candidates(builder, chosen_sectors, sector_pool, rejects): - polarity = builder.polarity() + sum_polarity(chosen_sectors) - candidates, official_candidates = defaultdict(list), [] - original_charge = polarity.charge() - best_charge = original_charge - main_pool = list(sector_pool) - last_r = 0 - scope = 2 - while len(official_candidates) == 0: - while len(candidates) == 0: - r_range = range(last_r + 1, last_r + 1 + scope) - for r in r_range: - if r > len(main_pool): - if len(candidates) == 0: - raise NeutralizingException('Cross Dungeon Builder: No possible neutralizers left %s' % builder.name) - else: - continue - last_r = r - combinations = ncr(len(main_pool), r) - if combinations > 1000000: - raise NeutralizingException('Cross Dungeon Builder: Too many combinations %s (>1mil)' % builder.name) - for i in range(0, combinations): - choice = kth_combination(i, main_pool, r) - p_charge = (polarity + sum_polarity(choice)).charge() - if p_charge < original_charge and p_charge <= best_charge and choice not in rejects: - candidates[p_charge].append(choice) - if p_charge < best_charge: - best_charge = p_charge - scope = 1 - try: - official_candidates = weed_candidates(builder, candidates, best_charge) - except NeutralizingException: - official_candidates = [] - return official_candidates +class OrderedFrozenSet: + def __init__(self): + self.frozen_set = frozenset() + self.order = [] + + def __eq__(self, other): + return self.frozen_set == other.frozen_set + + def __hash__(self): + return hash(self.frozen_set) + + def __iter__(self): + return self.order.__iter__() + + def __len__(self): + return len(self.order) + + def new_with_element(self, elements): + ret = OrderedFrozenSet() + ret.frozen_set = frozenset(self.frozen_set | elements) + ret.order = list(self.order) + ret.order.extend(elements) + return ret + + +# this could be re-worked for the more complete solution +# i'm not sure it does a whole lot now def weed_candidates(builder, candidates, best_charge): official_cand = [] while len(official_cand) == 0: @@ -2554,7 +2459,7 @@ def find_forced_groupings(sector_pool, dungeon_map): queue = deque(sector_pool) skips = set() while len(queue) > 0: - grouping = queue.pop() + grouping = queue.popleft() is_list = isinstance(grouping, List) if not is_list and grouping in skips: continue @@ -2744,20 +2649,28 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info): def split_dungeon_builder(builder, split_list, builder_info): - if builder.split_dungeon_map: + if builder.split_dungeon_map and len(builder.exception_list) == 0: + for name, proposal in builder.valid_proposal.items(): + builder.split_dungeon_map[name].valid_proposal = proposal return builder.split_dungeon_map # we made this earlier in gen, just use it - candidate_sectors = dict.fromkeys(builder.sectors) - global_pole = GlobalPolarity(candidate_sectors) + attempts = 0 + while attempts < 5: # does not solve coin flips 3% of the time + try: + candidate_sectors = dict.fromkeys(builder.sectors) + global_pole = GlobalPolarity(candidate_sectors) - dungeon_map = {} - for name, split_entrances in split_list.items(): - key = builder.name + ' ' + name - dungeon_map[key] = sub_builder = DungeonBuilder(key) - sub_builder.all_entrances = split_entrances - for r_name in split_entrances: - assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) - return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) + dungeon_map = {} + for name, split_entrances in split_list.items(): + key = builder.name + ' ' + name + dungeon_map[key] = sub_builder = DungeonBuilder(key) + sub_builder.all_entrances = split_entrances + for r_name in split_entrances: + assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) + return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) + except (GenerationException, NeutralizingException): + attempts += 1 + raise GenerationException('Unable to resolve in 5 attempts') def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info): @@ -2935,61 +2848,74 @@ class DoorEquation: def __init__(self, door): self.door = door - self.cost = defaultdict(list) + self.cost = None, None self.benefit = defaultdict(list) self.required = False self.access_id = None self.c_switch = False - self.blue_blocked = [] + self.crystal_blocked = {} + self.entrance_flag = False def copy(self): eq = DoorEquation(self.door) - for key, doors in self.cost.items(): - eq.cost[key] = doors.copy() + eq.cost = self.cost for key, doors in self.benefit.items(): eq.benefit[key] = doors.copy() eq.required = self.required eq.c_switch = self.c_switch - eq.blue_blocked.extend(self.blue_blocked) + eq.crystal_blocked = self.crystal_blocked.copy() return eq def total_cost(self): - ttl = 0 - for key, door_list in self.cost.items(): - ttl += len(door_list) - return ttl + return 0 if self.cost[0] is None else 1 - def gross(self): + def gross(self, current_access): + key, cost_door = self.cost + if key is None: + # todo: could just be Orange as well (multiple entrance case) + crystal_access = current_access.access_door[None] + else: + crystal_access = None + for match_door, crystal in current_access.outstanding_doors.items(): + if hook_from_door(match_door) == key: + if crystal_access is None or current_access._better_crystal(crystal_access, crystal): + crystal_access = crystal ttl = 0 for key, door_list in self.benefit.items(): - ttl += len(door_list) + for door in door_list: + if door in current_access.outstanding_doors.keys() or door in current_access.proposed_connections.keys(): + continue + if door in self.crystal_blocked.keys() and not self.c_switch: + if crystal_access == CrystalBarrier.Either or crystal_access == self.crystal_blocked[door]: + ttl += 1 + else: + ttl += 1 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 profit(self, current_access): + return self.gross(current_access) - self.total_cost() def neutral(self): - for key in Hook: - if len(self.cost[key]) != len(self.benefit[key]): - return False + key, door = self.cost + if key is not None and len(self.benefit[key]) <= 0: + return False return True def neutral_profit(self): - better_found = False - for key in Hook: - if len(self.cost[key]) > len(self.benefit[key]): + key, door = self.cost + if key is not None: + if len(self.benefit[key]) < 1: return False - if len(self.cost[key]) < len(self.benefit[key]): - better_found = True - return better_found + if len(self.benefit[key]) > 1: + return True + return False + else: + 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 + key, door = self.cost + if key is not None and current_access[key] < 1: + return False return True @@ -2997,8 +2923,158 @@ class DungeonAccess: def __init__(self): self.access = defaultdict(int) - self.blue_possible = False - self.child_sections = [] + self.door_access = {} # door -> crystal + self.door_sector_map = {} # door -> original sector + self.outstanding_doors = {} + self.blocked_doors = {} + self.door_access[None] = CrystalBarrier.Orange + self.proposed_connections = {} + self.reached_doors = set() + + def can_cover_equation(self, equation): + key, door = equation.cost + if key is None: + return True + return self.access[key] >= 1 + + def can_pay(self, key): + if key is None: + return True + return self.access[key] >= 1 + + def adjust_for_equation(self, equation, sector): + if equation.cost[0] is None: + original_crystal = self.door_access[None] + for key, door_list in equation.benefit.items(): + self.access[key] += len(door_list) + for door in door_list: + # I can't think of an entrance sector that forces blue + crystal_state = CrystalBarrier.Either if equation.c_switch else original_crystal + if crystal_state == CrystalBarrier.Either: + self.door_access[None] = CrystalBarrier.Either + self.door_access[door] = crystal_state + self.door_sector_map[door] = sector + self.outstanding_doors[door] = crystal_state + self.reached_doors.add(door) + else: + key, door = equation.cost + self.access[key] -= 1 + # find the a matching connection + best_door, best_crystal = None, None + for match_door, crystal in self.outstanding_doors.items(): + if hook_from_door(match_door) == key: + if best_door is None or self._better_crystal(best_crystal, crystal): + best_door = match_door + best_crystal = crystal + if best_door is None: + raise Exception('Something went terribly wrong I think') + # for match_door, crystal in self.blocked_doors.items(): + # if hook_from_door(match_door) == key: + # if best_door is None or self._better_crystal(best_crystal, crystal): + # best_door = match_door + # best_crystal = crystal + self.door_sector_map[door] = sector + self.door_access[door] = best_crystal + self.reached_doors.add(door) + self.proposed_connections[door] = best_door + self.proposed_connections[best_door] = door + if best_door in self.outstanding_doors.keys(): + del self.outstanding_doors[best_door] + elif best_door in self.blocked_doors.keys(): + del self.blocked_doors[best_door] + self.reached_doors.add(best_door) + + # todo: backpropagate crystal access + if equation.c_switch or best_crystal == CrystalBarrier.Either: + # if not equation.door.blocked: + self.door_access[door] = CrystalBarrier.Either + self.door_access[best_door] = CrystalBarrier.Either + queue = deque([best_door, door]) + visited = set() + while len(queue) > 0: + next_door = queue.popleft() + visited.add(next_door) + curr_sector = self.door_sector_map[next_door] + next_eq = None + for eq in curr_sector.equations: + if eq.door == next_door: + next_eq = eq + break + if next_eq.entrance_flag: + crystal_state = self.door_access[next_door] + self.door_access[None] = crystal_state + for eq in curr_sector.equations: + cand_door = eq.door + crystal_state = self.door_access[None] + if cand_door in next_eq.crystal_blocked.keys(): + crystal_state = next_eq.crystal_blocked[cand_door] + if cand_door not in visited: + self.door_access[cand_door] = crystal_state + if not cand_door.blocked: + if cand_door in self.outstanding_doors.keys(): + self.outstanding_doors[cand_door] = crystal_state + if cand_door in self.proposed_connections.keys(): + partner_door = self.proposed_connections[cand_door] + self.door_access[partner_door] = crystal_state + if partner_door in self.outstanding_doors.keys(): + self.outstanding_doors[partner_door] = crystal_state + queue.append(partner_door) + else: + for key, door_list in next_eq.benefit.items(): + for cand_door in door_list: + crystal_state = self.door_access[next_door] + if cand_door in next_eq.crystal_blocked.keys(): + crystal_state = next_eq.crystal_blocked[cand_door] + if cand_door in self.blocked_doors.keys(): + needed_crystal = self.blocked_doors[cand_door] + if meets_crystal_requirment(crystal_state, needed_crystal): + del self.blocked_doors[cand_door] + if cand_door != door: + self.access[key] += 1 + self.outstanding_doors[cand_door] = crystal_state + self.door_access[cand_door] = crystal_state + self.reached_doors.add(cand_door) + if cand_door not in visited: + self.door_access[cand_door] = crystal_state + if not cand_door.blocked: + if cand_door in self.outstanding_doors.keys(): + self.outstanding_doors[cand_door] = crystal_state + if cand_door in self.proposed_connections.keys(): + partner_door = self.proposed_connections[cand_door] + self.door_access[partner_door] = crystal_state + if partner_door in self.outstanding_doors.keys(): + self.outstanding_doors[partner_door] = crystal_state + queue.append(cand_door) + queue.append(partner_door) + + for key, door_list in equation.benefit.items(): + for door in door_list: + crystal_access = self.door_access[best_door] + can_access = True + if door in equation.crystal_blocked.keys(): + if crystal_access == CrystalBarrier.Either or crystal_access == equation.crystal_blocked[door]: + crystal_access = equation.crystal_blocked[door] + else: + self.blocked_doors[door] = equation.crystal_blocked[door] + can_access = False + self.door_sector_map[door] = sector + if can_access and door not in self.reached_doors: + self.access[key] += 1 + self.door_access[door] = crystal_access + self.outstanding_doors[door] = crystal_access + self.reached_doors.add(door) + + def _better_crystal(self, current_champ, contender): + if current_champ == CrystalBarrier.Either: + return False + elif contender == CrystalBarrier.Either: + return True + elif current_champ == CrystalBarrier.Blue: + return False + elif contender == CrystalBarrier.Blue: + return True + else: + return False def identify_branching_issues(dungeon_map, builder_info): @@ -3014,9 +3090,6 @@ def identify_branching_issues(dungeon_map, builder_info): def check_for_valid_layout(builder, sector_list, builder_info): dungeon_entrances, split_dungeon_entrances, world, player = builder_info - unreached_doors = resolve_equations(builder, sector_list) - if len(unreached_doors) == 0: - return True, unreached_doors if builder.name in split_dungeon_entrances.keys(): try: temp_builder = DungeonBuilder(builder.name) @@ -3033,46 +3106,31 @@ def check_for_valid_layout(builder, sector_list, builder_info): proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player) # record split proposals builder.valid_proposal[name] = proposal + builder.exception_list = list(sector_list) return True, {} except (GenerationException, NeutralizingException): builder.split_dungeon_map = None builder.valid_proposal = None + unreached_doors = resolve_equations(builder, sector_list) + return False, unreached_doors else: - try: - temp_builder = DungeonBuilder(builder.name) - temp_builder.sectors.extend(sector_list) - split_flag = builder.name not in dungeon_entrances.keys() - if split_flag: - name_bits = builder.name.split(" ") - orig_name = " ".join(name_bits[:-1]) - # todo: destination regions? - entrance_regions = split_dungeon_entrances[orig_name][name_bits[-1]] - else: - entrance_regions = dungeon_entrances[builder.name] - proposal = generate_dungeon_find_proposal(temp_builder, entrance_regions, split_flag, world, player) - builder.valid_proposal = proposal - return True, {} - except (GenerationException, NeutralizingException): - pass - return False, unreached_doors + unreached_doors = resolve_equations(builder, sector_list) + return len(unreached_doors) == 0, unreached_doors def resolve_equations(builder, sector_list): unreached_doors = defaultdict(list) equations = copy_door_equations(builder, sector_list) - reached_doors = set() current_access = {} sector_split = {} # those sectors that belong to a certain sector if builder.name in split_region_starts.keys(): for name, region_list in split_region_starts[builder.name].items(): - current_access[name] = defaultdict(int) - # current_access[name] = DungeonAccess() + current_access[name] = DungeonAccess() for r_name in region_list: sector = find_sector(r_name, builder.sectors) sector_split[sector] = name else: - current_access[builder.name] = defaultdict(int) - # current_access[builder.name] = DungeonAccess() + current_access[builder.name] = DungeonAccess() # resolve all that provide more access free_sector, eq_list, free_eq = find_free_equation(equations) @@ -3083,7 +3141,7 @@ def resolve_equations(builder, sector_list): else: access_id = next(iter(current_access.keys())) access = current_access[access_id] - resolve_equation(free_eq, eq_list, free_sector, access_id, access, reached_doors, equations) + resolve_equation(free_eq, eq_list, free_sector, access_id, access, equations) free_sector, eq_list, free_eq = find_free_equation(equations) while len(equations) > 0: valid_access = next_access(current_access) @@ -3095,14 +3153,15 @@ def resolve_equations(builder, sector_list): access_id, access = valid_access[0] eq, eq_list, sector = find_greedy_equation(equations, access_id, access, sector_split) if eq: - resolve_equation(eq, eq_list, sector, access_id, access, reached_doors, equations) + resolve_equation(eq, eq_list, sector, access_id, access, 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 valid_access = next_access(current_access) - for access_id, access in valid_access: + for access_id, dungeon_access in valid_access: + access = dungeon_access.access access[Hook.Stairs] = access[Hook.Stairs] % 2 ns_leftover = min(access[Hook.North], access[Hook.South]) access[Hook.North] -= ns_leftover @@ -3118,8 +3177,8 @@ def resolve_equations(builder, sector_list): def next_access(current_access): - valid_ones = [(x, y) for x, y in current_access.items() if sum(y.values()) > 0] - valid_ones.sort(key=lambda x: sum(x[1].values())) + valid_ones = [(x, y) for x, y in current_access.items() if sum(y.access.values()) > 0] + valid_ones.sort(key=lambda x: sum(x[1].access.values())) return valid_ones @@ -3137,11 +3196,11 @@ def find_priority_equation(equations, access_id, current_access): local_profit_map = {} for sector, eq_list in equations.items(): - eq_list.sort(key=lambda eq: eq.profit(), reverse=True) + eq_list.sort(key=lambda eq: eq.profit(current_access), reverse=True) best_local_profit = None for eq in eq_list: - profit = eq.profit() - if eq.can_cover_cost(current_access) and (eq.access_id is None or eq.access_id == access_id): + profit = eq.profit(current_access) + if current_access.can_cover_equation(eq) and (eq.access_id is None or eq.access_id == access_id): # if eq.neutral_profit() or eq.neutral(): # return eq, eq_list, sector # don't need to compare - just use it now if best_local_profit is None or profit > best_local_profit: @@ -3155,7 +3214,7 @@ def find_priority_equation(equations, access_id, current_access): wanted_candidates.append(eq) local_profit_map[sector] = best_local_profit filtered_candidates = filter_requirements(all_candidates, equations, required, current_access) - filtered_candidates = [x for x in filtered_candidates if x[0].gross() > 0] + filtered_candidates = [x for x in filtered_candidates if x[0].gross(current_access) > 0] if len(filtered_candidates) == 0: filtered_candidates = all_candidates # probably bad things if len(filtered_candidates) == 0: @@ -3178,7 +3237,7 @@ def find_priority_equation(equations, access_id, current_access): triplet_candidates = [] best_profit = None for eq, eq_list, sector in filtered_candidates: - profit = eq.profit() + profit = eq.profit(current_access) if best_profit is None or profit >= best_profit: if best_profit is None or profit > best_profit: triplet_candidates = [(eq, eq_list, sector)] @@ -3198,13 +3257,23 @@ def find_priority_equation(equations, access_id, current_access): if len(required_candidates) == 1: return required_candidates[0] - flexible_candidates = [x for x in required_candidates if x[0].can_cover_cost(flex)] + c_switch_candidates = [x for x in required_candidates if x[0].c_switch] + if len(c_switch_candidates) == 0: + c_switch_candidates = required_candidates + if len(c_switch_candidates) == 1: + return c_switch_candidates[0] + + loop_candidates = find_enabling_switch_connections(current_access) + if len(loop_candidates) >= 1: + return loop_candidates[0] # just pick one + + flexible_candidates = [x for x in c_switch_candidates if x[0].can_cover_cost(flex)] if len(flexible_candidates) == 0: - flexible_candidates = required_candidates + flexible_candidates = c_switch_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()] + good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit(current_access)] if len(good_local_candidates) == 0: good_local_candidates = flexible_candidates if len(good_local_candidates) == 1: @@ -3224,12 +3293,29 @@ def find_priority_equation(equations, access_id, current_access): return cost_point_candidates[0] # just pick one I guess +def find_enabling_switch_connections(current_access): + triad_list = [] + # probably should check for loop/branches in builder at some stage + # - but this could indicate that a loop or branch is necessary + for cand_door, crystal in current_access.outstanding_doors.items(): + for blocked_door, req_crystal in current_access.blocked_doors.items(): + if hook_from_door(cand_door) == hanger_from_door(blocked_door): + if crystal == CrystalBarrier.Either or crystal == req_crystal: + sector, equation = current_access.door_sector_map[blocked_door], None + for eq in sector.equations: + if eq.door == blocked_door: + equation = eq.copy() + break + if equation: + triad_list.append((equation, [equation], sector)) + return triad_list + + 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 + key, cost_door = eq_triplet[0].cost + if cost_door is not None: + cost_point += access.access[key] - 1 return cost_point @@ -3237,15 +3323,15 @@ def find_greedy_equation(equations, access_id, current_access, sector_split): all_candidates = [] for sector, eq_list in equations.items(): if sector not in sector_split.keys() or sector_split[sector] == access_id: - eq_list.sort(key=lambda eq: eq.profit(), reverse=True) + eq_list.sort(key=lambda eq: eq.profit(current_access), reverse=True) for eq in eq_list: - if eq.can_cover_cost(current_access) and (eq.access_id is None or eq.access_id == access_id): + if current_access.can_cover_equation(eq) and (eq.access_id is None or eq.access_id == access_id): all_candidates.append((eq, eq_list, sector)) if len(all_candidates) == 0: return None, None, None # can't pay for anything if len(all_candidates) == 1: return all_candidates[0] - filtered_candidates = [x for x in all_candidates if x[0].profit() + 2 >= len(x[2].outstanding_doors)] + filtered_candidates = [x for x in all_candidates if x[0].profit(current_access) + 2 >= len(x[2].outstanding_doors)] if len(filtered_candidates) == 0: filtered_candidates = all_candidates # terrible! ugly dead ends if len(filtered_candidates) == 1: @@ -3254,7 +3340,7 @@ def find_greedy_equation(equations, access_id, current_access, sector_split): triplet_candidates = [] worst_profit = None for eq, eq_list, sector in filtered_candidates: - profit = eq.profit() + profit = eq.profit(current_access) if worst_profit is None or profit <= worst_profit: if worst_profit is None or profit < worst_profit: triplet_candidates = [(eq, eq_list, sector)] @@ -3267,14 +3353,12 @@ def find_greedy_equation(equations, access_id, current_access, sector_split): def calc_required(equations, current_access): - ttl = 0 - for num in current_access.values(): - ttl += num + ttl = sum(current_access.access.values()) local_profit_map = {} for sector, eq_list in equations.items(): best_local_profit = None for eq in eq_list: - profit = eq.profit() + profit = eq.profit(current_access) if best_local_profit is None or profit > best_local_profit: best_local_profit = profit local_profit_map[sector] = best_local_profit @@ -3285,7 +3369,7 @@ def calc_required(equations, current_access): if len(eq_list) > 1: rem_list = [] for eq in eq_list: - if eq.profit() < local_profit_map[sector]: + if eq.profit(current_access) < local_profit_map[sector]: rem_list.append(eq) if len(rem_list) > 0: new_lists[sector] = [x for x in eq_list if x not in rem_list] @@ -3299,8 +3383,8 @@ def calc_required(equations, current_access): 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) + key, door = eq.cost + required_costs[key] += 1 for key, door_list in eq.benefit.items(): required_benefits[key] += len(door_list) return required_costs, required_benefits @@ -3312,10 +3396,10 @@ def calc_flex(equations, current_access): 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) + key, door = eq.cost + required_costs[key] += 1 for key in Hook: - flex_spending[key] = max(0, current_access[key]-required_costs[key]) + flex_spending[key] = max(0, current_access.access[key]-required_costs[key]) return flex_spending @@ -3324,19 +3408,19 @@ def filter_requirements(triplet_candidates, equations, required, current_access) valid_candidates = [] for cand, cand_list, cand_sector in triplet_candidates: valid = True - if not cand.required: + if not cand.required and not cand.c_switch: potential_benefit = defaultdict(int) benefit_counted = set() potential_costs = defaultdict(int) - for h_type, benefit in current_access.items(): - cur_cost = len(cand.cost[h_type]) + for h_type, benefit in current_access.access.items(): + cur_cost = 1 if cand.cost[0] is not None else 0 if benefit - cur_cost > 0: potential_benefit[h_type] += benefit - cur_cost for h_type, benefit_list in cand.benefit.items(): potential_benefit[h_type] += len(benefit_list) for sector, eq_list in equations.items(): if sector == cand_sector: - affected_doors = [d for x in cand.benefit.values() for d in x] + [d for x in cand.cost.values() for d in x] + affected_doors = [d for x in cand.benefit.values() for d in x] + [cand.cost[1]] adj_list = [x for x in eq_list if x.door not in affected_doors] else: adj_list = eq_list @@ -3345,8 +3429,8 @@ def filter_requirements(triplet_candidates, equations, required, current_access) total_benefit = set(benefit_list) - benefit_counted potential_benefit[h_type] += len(total_benefit) benefit_counted.update(benefit_list) - for h_type, cost_list in eq.cost.items(): - potential_costs[h_type] += len(cost_list) + h_type, cost_door = eq.cost + potential_costs[h_type] += 1 for h_type, requirement in r_costs.items(): if requirement > 0 and potential_benefit[h_type] < requirement: valid = False @@ -3364,28 +3448,21 @@ def filter_requirements(triplet_candidates, equations, required, current_access) def can_enable_wanted(test_eq, wanted_candidates): for wanted in wanted_candidates: covered = True - for key, door_list in wanted.cost.items(): - if len(test_eq.benefit[key]) < len(door_list): - covered = False - break + key, cost_door = wanted.cost + if len(test_eq.benefit[key]) < 1: + covered = False if covered: return True return False -def resolve_equation(equation, eq_list, sector, access_id, current_access, reached_doors, equations): - for key, door_list in equation.cost.items(): - if current_access[key] - len(door_list) < 0: - raise GenerationException('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) +def resolve_equation(equation, eq_list, sector, access_id, current_access, equations): + if not current_access.can_pay(equation.cost[0]): + raise GenerationException('Cannot pay for this connection') + current_access.adjust_for_equation(equation, sector) eq_list.remove(equation) - # removing = [x for x in eq_list if x.door in reached_doors] + reached_doors = set(current_access.reached_doors) + reached_doors.update(current_access.blocked_doors.keys()) for r_eq in list(eq_list): all_benefits_met = r_eq.door in reached_doors for key in Hook: @@ -3395,7 +3472,7 @@ def resolve_equation(equation, eq_list, sector, access_id, current_access, reach all_benefits_met = False if all_benefits_met: eq_list.remove(r_eq) - if len(eq_list) == 0: + if len(eq_list) == 0 and sector in equations.keys(): del equations[sector] else: for eq in eq_list: @@ -3451,30 +3528,37 @@ def calc_door_equation(door, sector, look_for_entrance): eq = DoorEquation(door) eq.benefit[hook_from_door(door)].append(door) eq.required = True + eq.c_switch = door.crystal == CrystalBarrier.Either + eq.entrance_flag = True return eq, flag eq = DoorEquation(door) eq.required = door.blocked or door.dead - eq.cost[hanger_from_door(door)].append(door) + eq.cost = (hanger_from_door(door), door) + eq.entrance_flag = sector.is_entrance_sector() if not door.stonewall: start_region = door.entrance.parent_region - visited = {(start_region, False)} - queue = deque([(start_region, False)]) + visited = {(start_region, CrystalBarrier.Null)} + queue = deque([(start_region, CrystalBarrier.Null)]) found_events = set() event_doors = set() while len(queue) > 0: - region, blue_flag = queue.popleft() - if region.crystal_switch and not blue_flag: + region, crystal_barrier = queue.popleft() + if region.crystal_switch and crystal_barrier == CrystalBarrier.Null: eq.c_switch = True + crystal_barrier = CrystalBarrier.Either + # todo: backtracking from double switch with orange on-- 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: - blue_barrier_flag = blue_flag or d.crystal == CrystalBarrier.Blue - visited.add((connect, blue_barrier_flag)) - queue.append((connect, blue_barrier_flag)) + if connect is not None and connect.type == RegionType.Dungeon and valid_crystal(d, crystal_barrier): + cb_flag = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal + cb_flag = CrystalBarrier.Null if cb_flag == CrystalBarrier.Either else cb_flag + if (connect, cb_flag) not in visited: + visited.add((connect, cb_flag)) + queue.append((connect, cb_flag)) for ext in region.exits: d = ext.door if d is not None: @@ -3484,24 +3568,44 @@ def calc_door_equation(door, sector, look_for_entrance): eq_list = eq.benefit[hook_from_door(d)] if d not in eq_list: eq_list.append(d) - if blue_flag: - eq.blue_blocked.append(d) - elif not blue_flag and d in eq.blue_blocked: - eq.blue_blocked.remove(d) + crystal_barrier = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal + if crystal_barrier != CrystalBarrier.Null: + if d in eq.crystal_blocked.keys() and eq.crystal_blocked[d] != crystal_barrier: + del eq.crystal_blocked[d] + else: + eq.crystal_blocked[d] = crystal_barrier + elif d.crystal == CrystalBarrier.Null: + if d in eq.crystal_blocked.keys() and eq.crystal_blocked[d] != crystal_barrier: + del eq.crystal_blocked[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 ext.door.controller is None else d.entrance.parent_region - if connect is not None and connect.type == RegionType.Dungeon: - blue_barrier_flag = blue_flag or d.crystal == CrystalBarrier.Blue - if (connect, blue_barrier_flag) not in visited: - visited.add((connect, blue_barrier_flag)) - queue.append((connect, blue_barrier_flag)) + if connect is not None and connect.type == RegionType.Dungeon and valid_crystal(d, crystal_barrier): + cb_flag = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal + cb_flag = CrystalBarrier.Null if cb_flag == CrystalBarrier.Either else cb_flag + if (connect, cb_flag) not in visited: + visited.add((connect, cb_flag)) + queue.append((connect, cb_flag)) if len(eq.benefit) == 0: eq.required = True return eq, False +def meets_crystal_requirment(current_crystal, requirement): + if current_crystal == CrystalBarrier.Either: + return True + return current_crystal == requirement + + +def valid_crystal(door, current_crystal): + if door.crystal in [CrystalBarrier.Null, CrystalBarrier.Either]: + return True + if current_crystal in [CrystalBarrier.Either, CrystalBarrier.Null]: + return True + return door.crystal == current_crystal + + # common functions - todo: move to a common place def kth_combination(k, l, r): if r == 0: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 3ca2407a..277b1dcf 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -991,7 +991,7 @@ def count_locations_exclude_big_chest(state): return cnt -def count_key_only_locations(state): +def count_small_key_only_locations(state): cnt = 0 for loc in state.found_locations: if loc.forced_item and loc.item.smallkey: @@ -1252,8 +1252,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return True # I think that's the end # todo: fix state to separate out these types ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state) - ttl_key_only = count_key_only_locations(state) - available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player) + ttl_small_key_only = count_small_key_only_locations(state) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False @@ -1267,7 +1267,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa state_copy = state.copy() open_a_door(exp_door.door, state_copy, flat_proposal) state_copy.used_smalls += 1 - if state_copy.used_smalls > ttl_key_only: + if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 code = state_id(state_copy, flat_proposal) if code not in checked_states.keys(): @@ -1343,7 +1343,10 @@ def create_key_counters(key_layout, world, player): key_counters = {} flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) - state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + if world.doorShuffle[player] == 'vanilla': + state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + else: + state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions for region in key_layout.start_regions: state.visit_region(region, key_checks=True) @@ -1471,6 +1474,8 @@ def find_counter_hint(opened_doors, bk_hint, key_layout): def find_max_counter(key_layout): max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) + if max_counter is None: + raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout) return max_counter @@ -1685,7 +1690,10 @@ def validate_key_placement(key_layout, world, player): for code, counter in key_layout.key_counters.items(): if len(counter.child_doors) == 0: continue - big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside + if key_layout.big_key_special: + big_found = any(i.forced_item is not None and i.item.bigkey for i in counter.other_locations) or big_key_outside + else: + big_found = any(i.item is not None and i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside if counter.big_key_opened and not big_found: continue # Can't get to this state found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 6a09f082..b0b82b71 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -10,7 +10,7 @@ "shuffling.dungeons": "Shuffling dungeons", "basic.traversal": "--Basic Traversal", "generating.dungeon": "Generating dungeons", - "shuffling.keydoors": "Shuffling Key doors for", + "shuffling.keydoors": "Shuffling Key doors", "lowering.keys.candidates": "Lowering key door count because not enough candidates", "lowering.keys.layouts": "Lowering key door count because no valid layouts", "keydoor.shuffle.time": "Key door shuffle time",