From 9dfd93adbc5f36348f6bbaccf3a5102fc56e431f Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Dec 2019 15:50:15 -0700 Subject: [PATCH] More trap doors (mostly interior) DungeonGen refinements --More Big Key door considerations --Backtracks earlier when hook candidates comes up empty Minor work on key shuffling - lots of bad rules still Playthrough gen doesn't flood swamp keys now --- DoorShuffle.py | 2 +- Doors.py | 10 ++--- DungeonGenerator.py | 21 +++++++---- KeyDoorShuffle.py | 91 +++++++++++++++++++++++++++++++++++++++++---- Main.py | 2 +- 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 38b5b664..c607ab8a 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -12,7 +12,7 @@ from Dungeons import dungeon_regions, region_starts, split_region_starts, dungeo from Dungeons import drop_entrances from RoomData import DoorKind, PairedDoor from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon -from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic +from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, validate_key_layout_ex def link_doors(world, player): diff --git a/Doors.py b/Doors.py index a540af01..fefe631d 100644 --- a/Doors.py +++ b/Doors.py @@ -299,7 +299,7 @@ def create_doors(world, player): create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High), create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High), create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1), - create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).pos(1), + create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1), create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0), create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0), @@ -481,7 +481,7 @@ def create_doors(world, player): create_door(player, 'Swamp I S', Intr).dir(So, 0x16, Mid, High).pos(0), create_door(player, 'Swamp T SW', Intr).dir(So, 0x16, Left, High).small_key().pos(1), create_door(player, 'Swamp T NW', Nrml).dir(No, 0x16, Left, High).pos(3), - create_door(player, 'Swamp Boss SW', Nrml).dir(So, 0x06, Left, High).trap(0x4).pos(0), + create_door(player, 'Swamp Boss SW', Nrml).dir(So, 0x06, Left, High).no_exit().trap(0x4).pos(0), create_door(player, 'Skull 1 Lobby WS', Nrml).dir(We, 0x58, Bot, High).small_key().pos(1), create_door(player, 'Skull 1 Lobby ES', Intr).dir(Ea, 0x58, Bot, High).pos(5), @@ -643,7 +643,7 @@ def create_doors(world, player): create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3), create_door(player, 'Ice Firebar Down Ladder', Lddr), create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1), - create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).pos(1), + create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1), create_door(player, 'Ice Falling Square Hole', Hole), create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0), create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True), @@ -980,7 +980,7 @@ def create_doors(world, player): create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0), create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0), create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1), - create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).pos(1), + create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1), create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2), create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2), create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True), @@ -1012,7 +1012,7 @@ def create_doors(world, player): create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1), - create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).pos(1), + create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1), create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True), create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True), create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2158e8e8..43fb2784 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -52,7 +52,7 @@ def generate_dungeon(available_sectors, entrance_region_names, split_dungeon, wo if itr > 5000: raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0]) if depth not in dungeon_cache.keys(): - dungeon, hangers, hooks = gen_dungeon_info(available_sectors, entrance_regions, proposed_map, doors_to_connect, world, player) + dungeon, hangers, hooks = gen_dungeon_info(available_sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, world, player) dungeon_cache[depth] = dungeon, hangers, hooks valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed) else: @@ -109,10 +109,10 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, world, player): +def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player): # step 1 create dungeon: Dict dungeon = {} - original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, world, player) + original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, bk_needed, world, player) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map) doors_to_connect = set() hanger_set = set() @@ -123,7 +123,7 @@ def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_do if not door.stonewall and door not in proposed_map.keys(): hanger_set.add(door) parent = parent_region(door, world, player).parent_region - o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, world, player) + o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, False, world, player) o_state_cache[door.name] = o_state piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map) dungeon[door.name] = piece @@ -183,7 +183,7 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player): parent = parent_region(door, world, player).parent_region blue_start = ExplorationState(CrystalBarrier.Blue) - b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, world, player) + b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player) dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map) @@ -234,6 +234,8 @@ def make_a_choice(dungeon, hangers, avail_hooks, prev_choices): if len(hook_candidates) > 0: hook_candidates.sort(key=lambda x: x.name) # sort for deterministic seeds hook = random.choice(tuple(hook_candidates)) + else: + return None, None return next_hanger, hook @@ -252,7 +254,7 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg return False # origin has no more hooks, but not all doors have been proposed possible_bks = len(dungeon['Origin'].possible_bk_locations) - true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0] + true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed] if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect): return False for key in hangers.keys(): @@ -553,7 +555,7 @@ class ExplorationState(object): return ret def next_avail_door(self): - self.avail_doors.sort(key=lambda x: 0 if x.flag else 1) + self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2) exp_door = self.avail_doors.pop() self.crystal = exp_door.crystal return exp_door @@ -784,13 +786,16 @@ def extend_reachable_state(search_regions, state, world, player): return local_state -def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, world, player): +def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player): local_state = state.copy() for region in search_regions: local_state.visit_region(region) local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player) while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() + if explorable_door.door.bigKey: + if isOrigin and local_state.count_locations_exclude_specials() == 0: + continue # we can't open this door if explorable_door.door in proposed_map: connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region else: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index dd889feb..b09c32ef 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -138,7 +138,8 @@ def analyze_dungeon(key_layout, world, player): key_sphere, key_counter = queue.popleft() chest_keys = available_chest_small_keys(key_counter, False, world) # chest_keys_bk = available_chest_small_keys(key_counter, True, world) - available = chest_keys + len(key_counter.key_only_locations) - key_counter.used_keys + raw_avail = chest_keys + len(key_counter.key_only_locations) + available = raw_avail - key_counter.used_keys possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) if not key_counter.big_key_opened: if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls: @@ -167,10 +168,10 @@ def analyze_dungeon(key_layout, world, player): true_min = minimal_keys last_small_child = len([x for x in childqueue if not x[0].bigKey]) == 0 force_min = not minimal_satisfied and last_small_child - rule = create_rule(expanded_counter, key_layout, true_min, force_min, world) + rule = create_rule(expanded_counter, key_layout, true_min, force_min, raw_avail, world) minimal_satisfied = minimal_satisfied or rule.small_key_num <= minimal_keys check_for_self_lock_key(rule, next_sphere, key_layout, world) - bk_restricted_rules(rule, next_sphere, key_counter, key_layout, true_min, force_min, world) + bk_restricted_rules(rule, next_sphere, key_counter, key_layout, true_min, force_min, raw_avail, world) key_logic.door_rules[child.name] = rule doors_completed.add(next_sphere.access_door) next_counter = increment_key_counter(child, next_sphere, key_counter, key_layout.flat_prop) @@ -298,10 +299,12 @@ def expand_counter_to_last_door(door, key_counter, key_layout, ignored_doors): return new_counter -def create_rule(key_counter, key_layout, minimal_keys, force_min, world): +def create_rule(key_counter, key_layout, minimal_keys, force_min, prev_avail, world): chest_keys = available_chest_small_keys(key_counter, key_counter.big_key_opened, world) - available = chest_keys + len(key_counter.key_only_locations) - key_counter.used_keys + raw_avail = chest_keys + len(key_counter.key_only_locations) + available = raw_avail - key_counter.used_keys possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) + # key_gain = max(raw_avail - prev_avail, 0) required_keys = min(available, possible_smalls) + key_counter.used_keys if not force_min or required_keys <= minimal_keys: return DoorRules(required_keys) @@ -347,11 +350,11 @@ def available_chest_small_keys(key_counter, bk, world): return key_counter.max_chests -def bk_restricted_rules(rule, sphere, key_counter, key_layout, minimal_keys, force_min, world): +def bk_restricted_rules(rule, sphere, key_counter, key_layout, minimal_keys, force_min, prev_avail, world): if sphere.bk_locked: return expanded_counter = expand_counter_no_big_doors(sphere.access_door, key_counter, key_layout, set()) - bk_number = create_rule(expanded_counter, key_layout, minimal_keys, force_min, world).small_key_num + bk_number = create_rule(expanded_counter, key_layout, minimal_keys, force_min, prev_avail, world).small_key_num if bk_number == rule.small_key_num: return post_counter = KeyCounter(key_layout.max_chests) @@ -491,6 +494,15 @@ def unique_doors(doors): return unique_d_set +# does not allow dest doors +def count_unique_doors(doors): + unique_d_set = set() + for d in doors: + if d not in unique_d_set and d.dest not in unique_d_set: + unique_d_set.add(d) + return len(unique_d_set) + + # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 @@ -545,6 +557,71 @@ def flatten_pair_list(paired_list): # Soft lock stuff +def validate_key_layout_ex(key_layout, world, player): + key_layout = KeyLayout(key_layout.sector, key_layout.start_regions, key_layout.proposal) + key_layout.flat_prop = flatten_pair_list(key_layout.proposal) + key_layout.max_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + counters = create_key_counters(key_layout, world, player) + pass + + +def create_key_counters(key_layout, world, player): + key_counters = {} + flat_proposal = key_layout.flat_prop + state = ExplorationState() + state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + 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) + 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_x(state, key_layout.max_chests, world, player) + queue = collections.deque([(key_counters[code], state)]) + while len(queue) > 0: + next_key_sphere, parent_state = queue.popleft() + for door in next_key_sphere.child_doors: + child_state = parent_state.copy() + # open the door + open_a_door(door, child_state, flat_proposal) + 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(): + child_kr = create_key_counter_x(child_state, key_layout.max_chests, world, player) + key_counters[code] = child_kr + queue.append((child_kr, child_state)) + return key_counters + + +def create_key_counter_x(state, max_chests, world, player): + key_sphere = KeyCounter(max_chests) + key_sphere.child_doors.update(unique_doors(state.small_doors+state.big_doors)) + for loc in state.found_locations: + if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: + key_sphere.important_location = True + # todo: zelda's cell is special in standard, and probably crossed too + elif loc.name in ['Attic Cracked Floor', 'Suspicious Maiden']: + key_sphere.important_location = True + elif loc.event and 'Small Key' in loc.item.name: + key_sphere.key_only_locations.add(loc) + elif loc.name not in dungeon_events: + key_sphere.free_locations.add(loc) + key_sphere.open_doors.update(state.opened_doors) + key_sphere.used_keys = count_unique_doors(state.opened_doors) + if state.big_key_special: + key_sphere.big_key_opened = state.visited(world.get_region('Hyrule Dungeon Cellblock', player)) + else: + key_sphere.big_key_opened = state.big_key_opened + return key_sphere + + +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' + return s_id + + class SoftLockException(Exception): pass diff --git a/Main.py b/Main.py index 3300a1fc..e095db6f 100644 --- a/Main.py +++ b/Main.py @@ -381,7 +381,7 @@ def create_playthrough(world): if not world.keysanity: state.sweep_for_events(key_only=True) - sphere = list(filter(state.can_reach, required_locations)) + sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations)) for location in sphere: required_locations.remove(location)