diff --git a/DoorShuffle.py b/DoorShuffle.py index 0e372aff..691ccad4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -12,6 +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 def link_doors(world, player): @@ -124,6 +125,7 @@ def vanilla_key_logic(world, player): valid = validate_key_layout(key_layout, world, player) if not valid: raise Exception('Vanilla key layout not valid %s' % sector.name) + analyze_dungeon(key_layout, world, player) if player not in world.key_logic.keys(): world.key_logic[player] = {} world.key_logic[player][sector.name] = key_layout.key_logic @@ -288,11 +290,12 @@ def within_dungeon(world, player): for key, sector_list, entrance_list in dungeon_sectors: origin_list = list(entrance_list) find_enabled_origins(sector_list, enabled_entrances, origin_list) - remove_drop_origins(origin_list) + origin_list = remove_drop_origins(origin_list) ds = generate_dungeon(sector_list, origin_list, world, player) find_new_entrances(ds, connections, potentials, enabled_entrances) ds.name = key - dungeon_layouts.append((ds, entrance_list)) + layout_starts = origin_list if len(entrance_list) <= 0 else entrance_list + dungeon_layouts.append((ds, layout_starts)) combine_layouts(dungeon_layouts, entrances_map) world.dungeon_layouts[player] = {} @@ -891,6 +894,7 @@ def flatten_pair_list(paired_list): def find_key_door_candidates(region, checked, world, player): + dungeon = region.dungeon candidates = [] checked_doors = list(checked) queue = collections.deque([(region, None, None)]) @@ -920,7 +924,8 @@ def find_key_door_candidates(region, checked, world, player): valid = True if valid: candidates.append(d) - queue.append((ext.connected_region, d, current)) # - todo: fix isolated ledge from re-entering + if ext.connected_region.type != RegionType.Dungeon or ext.connected_region.dungeon == dungeon: + queue.append((ext.connected_region, d, current)) if d is not None: checked_doors.append(d) return candidates, checked_doors @@ -1206,7 +1211,8 @@ def determine_required_paths(world): paths['Turtle Rock'].insert(0, 'TR Lazy Eyes') if world.mode == 'standard': paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock') - paths['Hyrule Castle'].append('Sanctuary') + # noinspection PyTypeChecker + paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary')) if world.doorShuffle in ['basic', 'experimental']: paths['Thieves Town'].append('Thieves Attic Window') return paths @@ -1235,6 +1241,9 @@ def find_inaccessible_regions(world, player): if connect is not None and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions: queue.append(connect) world.inaccessible_regions.extend([r.name for r in all_regions.difference(visited_regions) if r.type is not RegionType.Cave]) + if world.mode == 'standard': + world.inaccessible_regions.append('Hyrule Castle Ledge') + world.inaccessible_regions.append('Sewer Drop') logger = logging.getLogger('') logger.info('Inaccessible Regions:') for r in world.inaccessible_regions: @@ -1462,6 +1471,9 @@ logical_connections = [ ('TR Pipe Ledge Drop Down', 'TR Pipe Pit'), ('TR Big Chest Gap', 'TR Big Chest Entrance'), ('TR Big Chest Entrance Gap', 'TR Big Chest'), + ('TR Crystal Maze Forwards Path', 'TR Crystal Maze End'), + ('TR Crystal Maze Blue Path', 'TR Crystal Maze'), + ('TR Crystal Maze Cane Path', 'TR Crystal Maze'), ('GT Blocked Stairs Block Path', 'GT Big Chest'), ('GT Hookshot East-North Path', 'GT Hookshot North Platform'), ('GT Hookshot East-South Path', 'GT Hookshot South Platform'), diff --git a/Doors.py b/Doors.py index 75599c49..21e276e6 100644 --- a/Doors.py +++ b/Doors.py @@ -878,6 +878,9 @@ def create_doors(world, player): create_door(player, 'TR Dash Bridge WS', Nrml).dir(We, 0xc5, Bot, High).small_key().pos(0), create_door(player, 'TR Eye Bridge NW', Nrml).dir(No, 0xd5, Left, High).pos(1), create_door(player, 'TR Crystal Maze ES', Nrml).dir(Ea, 0xc4, Bot, High).small_key().pos(0), + create_door(player, 'TR Crystal Maze Forwards Path', Lgcl), + create_door(player, 'TR Crystal Maze Blue Path', Lgcl), + create_door(player, 'TR Crystal Maze Cane Path', Lgcl), create_door(player, 'TR Crystal Maze North Stairs', StrS).dir(No, 0xc4, Mid, High), create_door(player, 'TR Final Abyss South Stairs', StrS).dir(No, 0xb4, Right, High), create_door(player, 'TR Final Abyss NW', Nrml).dir(No, 0xb4, Left, High).big_key().pos(0), @@ -1154,7 +1157,9 @@ def create_doors(world, player): world.get_door('TR Crystaroller SW', player).c_switch() world.get_door('TR Crystaroller Down Stairs', player).c_switch() world.get_door('TR Crystal Maze ES', player).c_switch() - world.get_door('TR Crystal Maze North Stairs', player).c_switch() + world.get_door('TR Crystal Maze Forwards Path', player).c_switch() + world.get_door('TR Crystal Maze Cane Path', player).c_switch() + world.get_door('TR Crystal Maze Blue Path', player).barrier(CrystalBarrier.Blue) world.get_door('GT Crystal Conveyor NE', player).c_switch() world.get_door('GT Crystal Conveyor WN', player).c_switch() diff --git a/DungeonGenerator.py b/DungeonGenerator.py index a63665cd..32bf3482 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -41,8 +41,9 @@ def generate_dungeon(available_sectors, entrance_region_names, world, player): dungeon_cache = {} backtrack = False itr = 0 + finished = False # last_choice = None - while len(proposed_map) < len(doors_to_connect): + while not finished: # what are my choices? itr += 1 if itr > 5000: @@ -55,6 +56,9 @@ def generate_dungeon(available_sectors, entrance_region_names, world, player): dungeon, hangers, hooks = dungeon_cache[depth] valid = True if valid: + if len(proposed_map) == len(doors_to_connect): + finished = True + continue prev_choices = choices_master[depth] # make a choice hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices) diff --git a/Dungeons.py b/Dungeons.py index b8fe1fcd..83a1ec54 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -274,7 +274,8 @@ tr_regions = [ 'TR Tile Room', 'TR Refill', 'TR Pokey 1', 'TR Chain Chomps', 'TR Pipe Pit', 'TR Pipe Ledge', 'TR Lava Dual Pipes', 'TR Lava Island', 'TR Lava Escape', 'TR Pokey 2', 'TR Twin Pokeys', 'TR Hallway', 'TR Dodgers', 'TR Big View', 'TR Big Chest', 'TR Big Chest Entrance', 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', 'TR Rupees', - 'TR Crystaroller', 'TR Dark Ride', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze', 'TR Final Abyss', 'TR Boss' + 'TR Crystaroller', 'TR Dark Ride', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze', 'TR Crystal Maze End', + 'TR Final Abyss', 'TR Boss' ] gt_regions = [ diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 2a3ccce9..31a4248d 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3138,7 +3138,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Lumberjack House', 'Lumberjack House'), ("Hyrule Castle Secret Entrance Drop", "Hyrule Castle Secret Entrance"), ("Hyrule Castle Secret Entrance Stairs", "Hyrule Castle Secret Entrance"), - ("Hyrule Castle Secret Entrance Exit", "Light World"), + ("Hyrule Castle Secret Entrance Exit", "Hyrule Castle Courtyard"), ('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), @@ -3449,7 +3449,7 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Main L ('Hyrule Castle Entrance (South)', 'Hyrule Castle Lobby'), ('Hyrule Castle Entrance (West)', 'Hyrule Castle West Lobby'), ('Hyrule Castle Entrance (East)', 'Hyrule Castle East Lobby'), - ('Hyrule Castle Exit (South)', 'Light World'), + ('Hyrule Castle Exit (South)', 'Hyrule Castle Courtyard'), ('Hyrule Castle Exit (West)', 'Hyrule Castle Ledge'), ('Hyrule Castle Exit (East)', 'Hyrule Castle Ledge'), ('Agahnims Tower', 'Tower Lobby'), diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py new file mode 100644 index 00000000..103a6386 --- /dev/null +++ b/KeyDoorShuffle.py @@ -0,0 +1,198 @@ +import collections + +from Regions import dungeon_events +from DungeonGenerator import ExplorationState + + +class KeyRegion(object): + + def __init__(self): + self.access_doors = set() + self.free_locations = [] + self.prize_region = False + self.key_only_locations = [] + self.child_doors = set() + self.bk_locked = False + self.parent_region = None + + def __eq__(self, other): + if self.prize_region != other.prize_region: + return False + if self.bk_locked != other.bk_locked: + return False + if len(self.free_locations) != len(other.free_locations): + return False + if len(self.key_only_locations) != len(other.key_only_locations): + return False + if len(set(self.free_locations).difference(set(other.free_locations))) > 0: + return False + if len(set(self.key_only_locations).difference(set(other.key_only_locations))) > 0: + return False + if not self.check_child_dest(self.child_doors, other.child_doors, other.access_doors): + return False + if not self.check_child_dest(other.child_doors, self.child_doors, self.access_doors): + return False + return True + + @staticmethod + def check_child_dest(child_doors, other_child, other_access): + for child in child_doors: + if child in other_child: + continue + else: + found = False + for access in other_access: + if access.dest == child: + found = True + break + if not found: + return False + return True + + # def issubset(self, other): + # if self.prize_region != other.prize_region: + # return False + # if self.bk_locked != other.bk_locked: + # return False + # if not set(self.free_locations).issubset(set(other.free_locations)): + # return False + # if not set(self.key_only_locations).issubset(set(other.key_only_locations)): + # return False + # if not set(self.child_doors).issubset(set(other.child_doors)): + # return False + # return True + # + # def issuperset(self, other): + # if self.prize_region != other.prize_region: + # return False + # if self.bk_locked != other.bk_locked: + # return False + # if not set(self.free_locations).issuperset(set(other.free_locations)): + # return False + # if not set(self.key_only_locations).issuperset(set(other.key_only_locations)): + # return False + # if not set(self.child_doors).issuperset(set(other.child_doors)): + # return False + # return True + + +def analyze_dungeon(key_layout, world, player): + flat_proposal = flatten_pair_list(key_layout.proposal) + key_regions = create_key_regions(key_layout, flat_proposal, world, player) + start = key_regions['Origin'] + + +def create_key_regions(key_layout, flat_proposal, world, player): + key_regions = {} + 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) + key_regions['Origin'] = create_key_region(state, None, None) + queue = collections.deque([(key_regions['Origin'], state)]) + while len(queue) > 0: + next_key_region, parent_state = queue.popleft() + for door in next_key_region.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) + child_kr = create_key_region(child_state, next_key_region, door) + check_for_duplicates_sub_super_set(key_regions, child_kr, door.name) + queue.append((child_kr, child_state)) + return key_regions + + +def check_for_duplicates_sub_super_set(key_regions, new_kr, door_name): + is_new = True + for kr in key_regions.values(): + if new_kr == kr: + kr.access_doors.update(new_kr.access_doors) + kr.child_doors.update(new_kr.child_doors) + key_regions[door_name] = kr + is_new = False + break + # if new_kr.issubset(kr): + # break + # if new_kr.issuperset(kr): + # break + if is_new: + key_regions[door_name] = new_kr + + +def create_key_region(state, parent_region, door): + key_region = KeyRegion() + key_region.parent_region = parent_region + p_region = parent_region + parent_doors = set() + parent_locations = set() + while p_region is not None: + parent_doors.update(p_region.child_doors) + parent_locations.update(p_region.free_locations+p_region.key_only_locations) + p_region = p_region.parent_region + u_doors = unique_doors(state.small_doors+state.big_doors).difference(parent_doors) + key_region.child_doors.update(u_doors) + region_locations = set(state.found_locations).difference(parent_locations) + for loc in region_locations: + if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: + key_region.prize_region = True + elif loc.event and 'Small Key' in loc.item.name: + key_region.key_only_locations.append(loc) + elif loc.name not in dungeon_events: + key_region.free_locations.append(loc) + key_region.bk_locked = state.big_key_opened + if door is not None: + key_region.access_doors.add(door) + return key_region + + +def open_a_door(door, child_state, flat_proposal): + if door.bigKey: + 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() + else: + child_state.opened_doors.append(door) + doors_to_open = [x for x in child_state.small_doors if x.door == door] + child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != door] + child_state.avail_doors.extend(doors_to_open) + dest_door = door.dest + if dest_door in flat_proposal: + child_state.opened_doors.append(dest_door) + if child_state.in_door_list_ic(dest_door, child_state.small_doors): + now_available = [x for x in child_state.small_doors if x.door == dest_door] + child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != dest_door] + child_state.avail_doors.extend(now_available) + + +def unique_doors(doors): + unique_d_set = set() + for d in doors: + if d.door not in unique_d_set: + unique_d_set.add(d.door) + return unique_d_set + + +def expand_key_state(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + exp_door = state.next_avail_door() + door = exp_door.door + connect_region = world.get_entrance(door.name, player).connected_region + if state.validate(door, connect_region, world): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) + + +def flatten_pair_list(paired_list): + flat_list = [] + for d in paired_list: + if type(d) is tuple: + flat_list.append(d[0]) + flat_list.append(d[1]) + else: + flat_list.append(d) + return flat_list diff --git a/Regions.py b/Regions.py index d0bc1b66..42d80525 100644 --- a/Regions.py +++ b/Regions.py @@ -611,7 +611,8 @@ def create_regions(world, player): create_dungeon_region(player, 'TR Eye Bridge', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], ['Turtle Rock Isolated Ledge Exit', 'TR Eye Bridge NW']), - create_dungeon_region(player, 'TR Crystal Maze', 'Turtle Rock', None, ['TR Crystal Maze ES', 'TR Crystal Maze North Stairs']), + create_dungeon_region(player, 'TR Crystal Maze', 'Turtle Rock', None, ['TR Crystal Maze ES', 'TR Crystal Maze Forwards Path']), + create_dungeon_region(player, 'TR Crystal Maze End', 'Turtle Rock', None, ['TR Crystal Maze Blue Path', 'TR Crystal Maze Cane Path', 'TR Crystal Maze North Stairs']), create_dungeon_region(player, 'TR Final Abyss', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss NW']), create_dungeon_region(player, 'TR Boss', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize'], ['TR Boss SW']), @@ -692,7 +693,7 @@ def create_regions(world, player): create_dungeon_region(player, 'GT Mini Helmasaur Room', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', 'Ganons Tower - Mini Helmasuar Key Drop'], ['GT Mini Helmasaur Room SE', 'GT Mini Helmasaur Room WN']), create_dungeon_region(player, 'GT Bomb Conveyor', 'Ganon\'s Tower', None, ['GT Bomb Conveyor EN', 'GT Bomb Conveyor SW']), - create_dungeon_region(player, 'GT Crystal Circles', 'Ganon\'s Tower', None, ['GT Crystal Circles NW', 'GT Crystal Circles SW']), + create_dungeon_region(player, 'GT Crystal Circles', 'Ganon\'s Tower', ['Ganons Tower - Pre-Moldorm Chest'], ['GT Crystal Circles NW', 'GT Crystal Circles SW']), create_dungeon_region(player, 'GT Left Moldorm Ledge', 'Ganon\'s Tower', None, ['GT Left Moldorm Ledge Drop Down', 'GT Left Moldorm Ledge NW']), create_dungeon_region(player, 'GT Right Moldorm Ledge', 'Ganon\'s Tower', None, ['GT Right Moldorm Ledge Down Stairs', 'GT Right Moldorm Ledge Drop Down']), create_dungeon_region(player, 'GT Moldorm', 'Ganon\'s Tower', None, ['GT Moldorm Hole', 'GT Moldorm Gap']), diff --git a/Rules.py b/Rules.py index 72242a17..b18d1c5d 100644 --- a/Rules.py +++ b/Rules.py @@ -420,6 +420,7 @@ def global_rules(world, player): set_rule(world.get_entrance('TR Dodgers NE', player), lambda state: state.has('Big Key (Turtle Rock)', player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Crystal Maze Cane Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Final Abyss South Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Big Key (Turtle Rock)', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) @@ -1046,7 +1047,10 @@ def standard_rules(world, player): set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.can_kill_most_things(player)) - + set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_location('Hyrule Castle - Key Rat Key Drop', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Hyrule Dungeon Armory S', player), lambda state: state.can_kill_most_things(player)) def set_trock_key_rules(world, player):