From 8db8d066b43dd62e68752b31568f33f1174c27fc Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 22 Oct 2019 15:49:36 -0600 Subject: [PATCH] PoD and Swamp logical dead end detection improved --- DoorShuffle.py | 151 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 23 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 2ed2dea1..41bb2423 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -7,7 +7,7 @@ from functools import reduce from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBarrier, Polarity, pol_idx, pol_inc from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions from Dungeons import dungeon_regions, region_starts, split_region_starts -from Regions import key_only_locations, dungeon_events , flooded_keys, flooded_keys_reverse +from Regions import key_only_locations, dungeon_events, flooded_keys, flooded_keys_reverse from RoomData import DoorKind, PairedDoor @@ -111,6 +111,23 @@ oppositemap = { Direction.Down: Direction.Up, } +similar_directions = { + Direction.South: [Direction.South], + Direction.North: [Direction.North], + Direction.West: [Direction.West], + Direction.East: [Direction.East], + Direction.Up: [Direction.Down, Direction.Up], + Direction.Down: [Direction.Down, Direction.Up], +} + +paired_directions = { + Direction.South: [Direction.North], + Direction.North: [Direction.South], + Direction.West: [Direction.East], + Direction.East: [Direction.West], + Direction.Up: [Direction.Down, Direction.Up], + Direction.Down: [Direction.Down, Direction.Up], +} def switch_dir(direction): return oppositemap[direction] @@ -354,6 +371,15 @@ def get_doors(world, region, player): return res +def get_entrance_doors(world, region, player): + res = [] + for exit in region.entrances: + door = world.check_for_door(exit.name, player) + if door is not None: + res.append(door) + return res + + def doors_compatible(a, b): if a.type != b.type: return False @@ -671,6 +697,8 @@ class ExplorationState(object): self.key_locations = 0 self.used_smalls = 0 + self.non_door_entrances = [] + def copy(self): ret = ExplorationState() ret.unattached_doors = list(self.unattached_doors) @@ -692,6 +720,8 @@ class ExplorationState(object): ret.used_locations = self.used_locations ret.used_smalls = self.used_smalls ret.found_locations = list(self.found_locations) + + ret.non_door_entrances = list(self.non_door_entrances) return ret def next_avail_door(self): @@ -715,11 +745,12 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name: self.ttl_locations += 1 - self.found_locations.append(location) + if location not in self.found_locations: + self.found_locations.append(location) if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) - if location.name in flooded_keys_reverse.keys() and flooded_keys_reverse[location.name] in self.found_locations: + 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 key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened: self.big_key_opened = True @@ -731,13 +762,14 @@ class ExplorationState(object): return True return flooded_keys[location.name] in self.found_locations - def perform_event(self, location_name, key_region): - location = None + def location_found(self, location_name): for l in self.found_locations: if l.name == location_name: - location = l - break - self.events.add(location) + return True + return False + + def perform_event(self, location_name, key_region): + self.events.add(location_name) queue = collections.deque(self.event_doors) while len(queue) > 0: exp_door = queue.pop() @@ -749,6 +781,22 @@ class ExplorationState(object): if d_name not in self.door_krs.keys(): self.door_krs[d_name] = key_region + def add_all_entrance_doors_check_unattached(self, region, world, player): + door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]] + door_list.extend(get_entrance_doors(world, region, player)) + for door in door_list: + if self.can_traverse(door): + if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + for entrance in region.entrances: + door = world.check_for_door(entrance.name, player) + if door is None: + self.non_door_entrances.append(entrance) + def add_all_doors_check_unattached(self, region, world, player): for door in get_doors(world, region, player): if self.can_traverse(door): @@ -853,6 +901,22 @@ def extend_reachable_state(search_regions, state, world, player): return local_state +def extend_state_backward(search_regions, state, world, player): + local_state = state.copy() + for region in search_regions: + local_state.visit_region(region) + local_state.add_all_entrance_doors_check_unattached(region, world, player) + while len(local_state.avail_doors) > 0: + explorable_door = local_state.next_avail_door() + entrance = world.get_entrance(explorable_door.door.name, player) + connect_region = entrance.parent_region + if connect_region is not None: + if valid_region_to_explore(connect_region, world) and not local_state.visited(connect_region): + local_state.visit_region(connect_region) + local_state.add_all_entrance_doors_check_unattached(connect_region, world, player) + return local_state + + # todo: this sometimes generates two independent parts - that could be valid if the entrances are accessible # todo: prevent crystal barrier dead ends def shuffle_dungeon_no_repeats_new(world, player, available_sectors, entrance_region_names): @@ -1071,31 +1135,72 @@ def logical_dead_end(door_a, door_b, state, world, player, available_sectors, re if len(new_state.unattached_doors) == 0: return True d_type = door_a.type - directions = [door_a.direction, switch_dir(door_a.direction)] + directions = paired_directions[door_a.direction] + hook_directions = similar_directions[door_a.direction] number_of_hooks = 0 + opposing_hooks = 0 for door in new_state.unattached_doors: - if door.door.type == d_type and door.door.direction in directions: + if door.door.type == d_type and door.door.direction in hook_directions: number_of_hooks += 1 - only_dead_ends = True + if door.door.type == d_type and door.door.direction in directions: + opposing_hooks += 1 + hooks_needed = 0 + visited_regions = set() outstanding_doors_of_type = 0 - if number_of_hooks == 0: - for sector in available_sectors: - for door in sector.outstanding_doors: - if door != door_b and door.type == d_type and door.direction in directions: + outstanding_hooks = 0 + only_dead_ends = True + for sector in available_sectors: + for door in sector.outstanding_doors: + if door_of_interest(door, door_b, d_type, directions, hook_directions, new_state): + needed = True + if door.direction in directions: outstanding_doors_of_type += 1 - region = world.get_entrance(door.name, player).parent_region - local_state = extend_reachable_state([region], ExplorationState(), world, player) + region = world.get_entrance(door.name, player).parent_region + local_state = extend_state_backward([region], ExplorationState(), world, player) + if len(local_state.non_door_entrances) > 0 and not door.blocked: + needed = False + else: for exp_d in local_state.unattached_doors: - if exp_d.door.type != d_type or exp_d.door.direction not in directions and exp_d.door not in reachable_doors: - only_dead_ends = False + if different_direction(exp_d.door, d_type, directions, hook_directions, reachable_doors): + needed = False break - if not only_dead_ends: - break - if outstanding_doors_of_type > 0 and only_dead_ends: - return True + region_set = set(local_state.visited_orange+local_state.visited_blue) + if needed and len(visited_regions.intersection(region_set)) == 0 and door.direction in directions: + hooks_needed += 1 + elif door.direction in hook_directions: + outstanding_hooks += 1 + if opposing_hooks > 0 and more_than_one_hook(local_state, hook_directions): + needed = False + visited_regions.update(region_set) + if not needed: + only_dead_ends = False + if outstanding_doors_of_type > 0 and ((number_of_hooks == 0 and only_dead_ends) or hooks_needed > number_of_hooks + outstanding_hooks): + return True return False +def door_of_interest(door, door_b, d_type, directions, hook_directions, state): + if door == door_b or door.type != d_type: + return False + if door.direction not in directions and door.direction not in hook_directions: + return False + return not state.in_door_list_ic(door, state.unattached_doors) + + +def different_direction(door, d_type, directions, hook_directions, reachable_doors): + if door in reachable_doors: + return False + return door.type != d_type or (door.direction not in directions and door.direction not in hook_directions) + + +def more_than_one_hook(state, hook_directions): + cnt = 0 + for exp_d in state.unattached_doors: + if exp_d.door.direction in hook_directions: + cnt += 1 + return cnt > 1 + + def pinball_exception(door_a, door_b, sector_a): if door_a.name == 'Skull Pot Prison SE' and door_b.name == 'Skull Pinball NE': for r in sector_a.regions: