From 65c583c082cf7951eb3113fd47230b9021392938 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 16:00:05 -0600 Subject: [PATCH] Initial work on dungeon prize logic in key layouts --- DungeonGenerator.py | 10 ++++ Dungeons.py | 16 ++++++ KeyDoorShuffle.py | 137 +++++++++++++++++++++++++++++++++----------- 3 files changed, 129 insertions(+), 34 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 9d76ddb5..86be2a04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -807,6 +807,10 @@ class ExplorationState(object): self.dungeon = dungeon self.pinball_used = False + self.prize_door_set = {} + self.prize_doors = [] + self.prize_doors_opened = False + def copy(self): ret = ExplorationState(dungeon=self.dungeon) ret.unattached_doors = list(self.unattached_doors) @@ -833,6 +837,10 @@ class ExplorationState(object): ret.non_door_entrances = list(self.non_door_entrances) ret.dungeon = self.dungeon ret.pinball_used = self.pinball_used + + ret.prize_door_set = dict(self.prize_door_set) + ret.prize_doors = list(self.prize_doors) + ret.prize_doors_opened = self.prize_doors_opened return ret def next_avail_door(self): @@ -868,6 +876,8 @@ class ExplorationState(object): if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) + if '- Prize' in location.name: + self.prize_received = True def flooded_key_check(self, location): if location.name not in flooded_keys.keys(): diff --git a/Dungeons.py b/Dungeons.py index 6f0f2197..eca190c1 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -392,6 +392,22 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } +dungeon_prize = { + 'Hyrule Castle': None, + 'Eastern Palace': 'Eastern Palace - Prize', + 'Desert Palace': 'Desert Palace - Prize', + 'Tower of Hera': 'Tower of Hera - Prize', + 'Agahnims Tower': None, + 'Palace of Darkness': 'Palace of Darkness - Prize', + 'Swamp Palace': 'Swamp Palace - Prize', + 'Skull Woods': 'Skull Woods - Prize', + 'Thieves Town': 'Thieves Town - Prize', + 'Ice Palace': 'Ice Palace - Prize', + 'Misery Mire': 'Misery Mire - Prize', + 'Turtle Rock': 'Turtle Rock - Prize', + 'Ganons Tower': None +} + dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 89fa307d..0ecb98e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType, dungeon_keys, KeyRuleType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_bigs +from Dungeons import dungeon_bigs, dungeon_prize from DungeonGenerator import ExplorationState, special_big_key_doors @@ -26,6 +26,7 @@ class KeyLayout(object): self.item_locations = set() self.found_doors = set() + self.prize_relevant = False # bk special? # bk required? True if big chests or big doors exists @@ -36,6 +37,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() + self.prize_relevant = False class KeyLogic(object): @@ -183,6 +185,8 @@ class KeyCounter(object): self.important_location = False self.other_locations = {} self.important_locations = {} + self.prize_doors_opened = False + self.prize_received = False def used_smalls_loc(self, reserve=0): return max(self.used_keys + reserve - len(self.key_only_locations), 0) @@ -242,7 +246,7 @@ def analyze_dungeon(key_layout, world, player): if world.retro[player] and world.mode[player] != 'standard': return - original_key_counter = find_counter({}, False, key_layout) + original_key_counter = find_counter({}, False, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -270,15 +274,18 @@ def analyze_dungeon(key_layout, world, player): child_queue.append((child, odd_counter, empty_flag)) while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() - if not child.bigKey and child not in doors_completed: + prize_flag = key_counter.prize_doors_opened + if child in key_layout.flat_prop and child not in doors_completed: best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) rule = create_rule(best_counter, key_counter, world, player) create_worst_case_rule(rule, best_counter, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) key_logic.door_rules[child.name] = rule + elif not child.bigKey and child not in doors_completed: + prize_flag = True doors_completed.add(child) - next_counter = find_next_counter(child, key_counter, key_layout) + next_counter = find_next_counter(child, key_counter, key_layout, prize_flag) ctr_id = cid(next_counter, key_layout) if ctr_id not in visited_cid: queue.append((child, next_counter)) @@ -300,6 +307,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): + if key_counter.prize_received and not key_counter.prize_doors_opened: + continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) accessible_loc.update(key_counter.key_only_locations) @@ -684,7 +693,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # for new_door in door_set: proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened) bk_open = new_counter.big_key_opened if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter): ignored_doors.add(new_door) @@ -736,7 +745,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, for new_child in new_children: proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened) if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player): wasted_keys += 1 if new_avail - wasted_keys < old_avail: @@ -744,10 +753,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, return False -def find_next_counter(new_door, old_counter, key_layout): +def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = old_counter.big_key_opened or new_door.bigKey - return find_counter(proposed_doors, bk_open, key_layout) + prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened + return find_counter(proposed_doors, bk_open, key_layout, prize_flag) def check_special_locations(locations): @@ -835,7 +845,7 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -856,7 +866,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened) changed = True return counter @@ -939,12 +949,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c return worst_counter, post_counter, bk_rule_num -def open_a_door(door, child_state, flat_proposal): +def open_a_door(door, child_state, flat_proposal, world, player): if door.bigKey or door.name in special_big_key_doors: child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) child_state.big_doors.clear() + elif door in child_state.prize_door_set: + child_state.prize_doors_opened = True + for exp_door in child_state.prize_doors: + new_region = exp_door.door.entrance.parent_region + child_state.visit_region(new_region, key_checks=True) + child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player) + child_state.prize_doors.clear() else: child_state.opened_doors.append(door) doors_to_open = [x for x in child_state.small_doors if x.door == door] @@ -1002,7 +1019,7 @@ def count_unique_small_doors(key_counter, proposal): def exist_relevant_big_doors(key_counter, key_layout): - bk_counter = find_counter(key_counter.open_doors, True, key_layout, False) + bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False) if bk_counter is not None: diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) if len(diff) > 0: @@ -1338,8 +1355,13 @@ def validate_key_layout(key_layout, world, player): state.key_locations = key_layout.max_chests state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player) @@ -1370,7 +1392,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() - open_a_door(exp_door.door, state_copy, flat_proposal) + open_a_door(exp_door.door, state_copy, flat_proposal, world, player) state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 @@ -1385,7 +1407,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return False if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)): state_copy = state.copy() - open_a_door(state.big_doors[0].door, state_copy, flat_proposal) + open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 code = state_id(state_copy, flat_proposal) @@ -1397,6 +1419,18 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa valid = checked_states[code] if not valid: return False + if not state.prize_doors_opened and key_layout.prize_relevant: + state_copy = state.copy() + open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) + code = state_id(state_copy, flat_proposal) + if code not in checked_states.keys(): + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, + state, available_small_locations, world, player) + checked_states[code] = valid + else: + valid = checked_states[code] + if not valid: + return False return True @@ -1407,7 +1441,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p state_copy = state.copy() while len(new_bk_doors) > 0: for door in new_bk_doors: - open_a_door(door.door, state_copy, key_layout.flat_prop) + open_a_door(door.door, state_copy, key_layout.flat_prop, world, player) new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors)) expand_key_state(state_copy, key_layout.flat_prop, world, player) new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations)) @@ -1461,8 +1495,14 @@ def create_key_counters(key_layout, world, player): state.big_key_special = True special_region = region for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) code = state_id(state, key_layout.flat_prop) key_counters[code] = create_key_counter(state, key_layout, world, player) @@ -1478,7 +1518,7 @@ def create_key_counters(key_layout, world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): - open_a_door(door, child_state, flat_proposal) + open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) if code not in key_counters.keys(): @@ -1488,9 +1528,19 @@ def create_key_counters(key_layout, world, player): return key_counters +def find_outside_connection(region): + portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None) + if portal: + dungeon_entrance = next(x for x in portal.parent_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region) + return dungeon_entrance, portal_entrance.door + return None, None + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) - key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) + key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) for loc in state.found_locations: if important_location(loc, world, player): key_counter.important_location = True @@ -1507,6 +1557,10 @@ def create_key_counter(state, key_layout, world, player): key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.big_key_opened = state.big_key_opened + if len(state.prize_door_set) > 0 and state.prize_doors_opened: + key_counter.prize_doors_opened = True + if any(x for x in key_counter.important_locations if '- Prize' in x.name): + key_counter.prize_received = True return key_counter @@ -1557,11 +1611,13 @@ def state_id(state, flat_proposal): s_id = '1' if state.big_key_opened else '0' for d in flat_proposal: s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' return s_id -def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): - counter = find_counter_hint(opened_doors, bk_hint, key_layout) +def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): + counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: return counter more_doors = [] @@ -1570,43 +1626,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): if door.dest not in opened_doors.keys(): more_doors.append(door.dest) if len(more_doors) > len(opened_doors.keys()): - counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout) + counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag) if counter is not None: return counter if raise_on_error: - raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) + raise Exception(f'Unable to find door permutation. Init CID: {cid}') return None -def find_counter_hint(opened_doors, bk_hint, key_layout): - cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop) +def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag): + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] if not bk_hint: - cid = counter_id(opened_doors, True, key_layout.flat_prop) + cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] return None def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True) 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.found_doors), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True) return max_counter -def counter_id(opened_doors, bk_unlocked, flat_proposal): +def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag): s_id = '1' if bk_unlocked else '0' for d in flat_proposal: s_id += '1' if d in opened_doors.keys() else '0' + if prize_relevant: + s_id += '1' if prize_flag else '0' return s_id def cid(counter, key_layout): - return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop) + return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop, + key_layout.prize_relevant, counter.prize_doors_opened) # class SoftLockException(Exception): @@ -1816,8 +1876,17 @@ def validate_key_placement(key_layout, world, player): found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside + if key_layout.prize_relevant: + found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + if not found_prize: + prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + # todo: pyramid fairy only care about crystals 5 & 6 + found_prize = 'Crystal' not in prize_loc.item.name + else: + found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ - found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) + found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \ + (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]