diff --git a/DoorShuffle.py b/DoorShuffle.py index 1fb85f9d..9c472910 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -127,12 +127,9 @@ def vanilla_key_logic(world, player): raise Exception('Vanilla key layout not valid %s' % sector.name) if player not in world.key_logic.keys(): world.key_logic[player] = {} - if sector.name in ['Agahnims Tower', 'Tower of Hera', 'Desert Palace', 'Eastern Palace']: - key_layout_2 = KeyLayout(sector, start_regions, doors) - key_layout_2 = analyze_dungeon(key_layout_2, world, player) - world.key_logic[player][sector.name] = key_layout_2.key_logic - else: - world.key_logic[player][sector.name] = key_layout.key_logic + key_layout_2 = KeyLayout(sector, start_regions, doors) + key_layout_2 = analyze_dungeon(key_layout_2, world, player) + world.key_logic[player][sector.name] = key_layout_2.key_logic validate_vanilla_key_logic(world, player) @@ -297,16 +294,17 @@ def within_dungeon(world, player): last_key = None while len(sector_queue) > 0: key, sector_list, entrance_list = sector_queue.popleft() + split_dungeon = key in split_region_starts.keys() origin_list = list(entrance_list) find_enabled_origins(sector_list, enabled_entrances, origin_list, entrances_map, key) origin_list_sans_drops = remove_drop_origins(origin_list) - if len(origin_list) <= 0: + if len(origin_list_sans_drops) <= 0: if last_key == key: raise Exception('Infinte loop detected %s' % key) sector_queue.append((key, sector_list, entrance_list)) last_key = key else: - ds = generate_dungeon(sector_list, origin_list_sans_drops, world, player) + ds = generate_dungeon(sector_list, origin_list_sans_drops, split_dungeon, world, player) find_new_entrances(ds, connections, potentials, enabled_entrances, world, player) ds.name = key layout_starts = origin_list if len(entrance_list) <= 0 else entrance_list @@ -340,7 +338,7 @@ def determine_entrance_list(world, player): for ent in region.entrances: parent = ent.parent_region if parent.type != RegionType.Dungeon or parent.name == 'Sewer Drop': - if parent.name not in world.inaccessible_regions[player] or drop_exception(region_name): + if parent.name not in world.inaccessible_regions[player]: entrance_map[key].append(region_name) else: if ent.parent_region not in potential_entrances.keys(): @@ -366,8 +364,11 @@ def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key): for region in sector.regions: if region.name in enabled.keys() and region.name not in entrance_list: entrance_list.append(region.name) - if enabled[region.name] != region.name: + origin_reg, origin_dungeon = enabled[region.name] + if origin_reg != region.name and origin_dungeon != region.dungeon: entrance_map[key].append(region.name) + if drop_exception(region.name): # only because they have unique regions + entrance_list.append(region.name) def remove_drop_origins(entrance_list): @@ -379,7 +380,7 @@ def find_new_entrances(sector, connections, potentials, enabled, world, player): if region.name in connections.keys() and connections[region.name] in potentials.keys(): new_region = connections[region.name] for potential in potentials.pop(new_region): - enabled[potential] = region.name + enabled[potential] = (region.name, region.dungeon) # see if this unexplored region connects elsewhere queue = collections.deque(new_region.exits) visited = set() @@ -389,7 +390,7 @@ def find_new_entrances(sector, connections, potentials, enabled, world, player): region_name = ext.connected_region.name if region_name in connections.keys() and connections[region_name] in potentials.keys(): for potential in potentials.pop(connections[region_name]): - enabled[potential] = region.name + enabled[potential] = (region.name, region.dungeon) if ext.connected_region.name in world.inaccessible_regions[player]: for new_exit in ext.connected_region.exits: if new_exit not in visited: @@ -528,6 +529,7 @@ def find_compatible_door_in_regions(world, door, regions, player): return region, proposed_door return None, None + def find_compatible_door_in_list(ugly_regions, world, door, doors, player): if door.type in [DoorType.Hole, DoorType.Warp, DoorType.Logical]: return door @@ -877,8 +879,30 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player): # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} - world.key_logic[player][dungeon_sector.name] = key_layout.key_logic + key_layout_new = analyze_dungeon(key_layout, world, player) reassign_key_doors(current_doors, proposal, world, player) + log_key_logic(dungeon_sector.name, key_layout_new.key_logic) + world.key_logic[player][dungeon_sector.name] = key_layout_new.key_logic + + +def log_key_logic(d_name, key_logic): + logger = logging.getLogger('') + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Key Logic for %s', d_name) + if len(key_logic.bk_restricted) > 0: + logger.debug('-BK Restrictions') + for restriction in key_logic.bk_restricted: + logger.debug(restriction) + if len(key_logic.sm_restricted) > 0: + logger.debug('-Small Restrictions') + for restriction in key_logic.sm_restricted: + logger.debug(restriction) + for key in key_logic.door_rules.keys(): + rule = key_logic.door_rules[key] + logger.debug('--Rule for %s: Nrm:%s Allow:%s Loc:%s Alt:%s', key, rule.small_key_num, rule.allow_small, rule.small_location, rule.alternate_small_key) + if rule.alternate_small_key is not None: + for loc in rule.alternate_big_key_loc: + logger.debug('---BK Loc %s', loc.name) class KeyLayout(object): @@ -1792,7 +1816,7 @@ interior_doors = [ ('Skull East Bridge ES', 'Skull West Bridge Nook WS'), ('Skull Star Pits WS', 'Skull Torch Room ES'), ('Skull Torch Room EN', 'Skull Vines WN'), - ('Skull Spike Corner WS', 'Skull Final Drop ES'), + ('Skull Spike Corner ES', 'Skull Final Drop WS'), ('Thieves Hallway WS', 'Thieves Pot Alcove Mid ES'), ('Thieves Conveyor Maze SW', 'Thieves Pot Alcove Top NW'), ('Thieves Conveyor Maze EN', 'Thieves Hallway WN'), @@ -1943,7 +1967,7 @@ default_small_key_doors = { ('Skull Map Room SE', 'Skull Pinball NE'), ('Skull 2 West Lobby NW', 'Skull X Room SW'), ('Skull 3 Lobby NW', 'Skull Star Pits SW'), - ('Skull Spike Corner WS', 'Skull Final Drop ES') + ('Skull Spike Corner ES', 'Skull Final Drop WS') ], 'Thieves Town': [ ('Thieves Hallway WS', 'Thieves Pot Alcove Mid ES'), diff --git a/Doors.py b/Doors.py index 637a8ced..8c8121d2 100644 --- a/Doors.py +++ b/Doors.py @@ -524,8 +524,8 @@ def create_doors(world, player): create_door(player, 'Skull Vines WN', Intr).dir(We, 0x49, Top, High).pos(1), create_door(player, 'Skull Vines NW', Nrml).dir(No, 0x49, Left, High).pos(0), create_door(player, 'Skull Spike Corner SW', Nrml).dir(So, 0x39, Left, High).no_exit().trap(0x4).pos(0), - create_door(player, 'Skull Spike Corner WS', Intr).dir(We, 0x39, Bot, High).small_key().pos(1), - create_door(player, 'Skull Final Drop ES', Intr).dir(Ea, 0x39, Bot, High).small_key().pos(1), + create_door(player, 'Skull Spike Corner ES', Intr).dir(Ea, 0x39, Bot, High).small_key().pos(1), + create_door(player, 'Skull Final Drop WS', Intr).dir(We, 0x39, Bot, High).small_key().pos(1), create_door(player, 'Skull Final Drop Hole', Hole), create_door(player, 'Thieves Lobby N Edge', Open).dir(No, 0xdb, None, Low), @@ -984,7 +984,7 @@ def create_doors(world, player): 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), - create_door(player, 'GT Crystal Paths Down Stairs', Sprl).dir(Dn, 0x6b, 0, HTH).ss(A, 0x12, 0x00, True), + create_door(player, 'GT Crystal Paths Down Stairs', Sprl).dir(Dn, 0x6b, 0, HTH).ss(A, 0x12, 0x00, False, True), create_door(player, 'GT Crystal Paths SW', Intr).dir(So, 0x6b, Left, High).pos(3), create_door(player, 'GT Mimics 1 NW', Intr).dir(No, 0x6b, Left, High).pos(3), create_door(player, 'GT Mimics 1 ES', Intr).dir(Ea, 0x6b, Bot, High).pos(2), @@ -1044,7 +1044,7 @@ def create_doors(world, player): create_door(player, 'GT Right Moldorm Ledge Down Stairs', Sprl).dir(Dn, 0x4d, 0, HTH).ss(S, 0x12, 0x80), create_door(player, 'GT Moldorm Pit Up Stairs', Sprl).dir(Up, 0xa6, 0, HTH).ss(S, 0x1b, 0x6c), create_door(player, 'GT Frozen Over ES', Nrml).dir(Ea, 0x4c, Bot, High).trap(0x4).pos(0), - create_door(player, 'GT Frozen Over Up Stairs', Sprl).dir(Up, 0x4c, 0, HTH).ss(S, 0x1a, 0x6c, False, True), + create_door(player, 'GT Frozen Over Up Stairs', Sprl).dir(Up, 0x4c, 0, HTH).ss(S, 0x1a, 0x6c, True), create_door(player, 'GT Brightly Lit Hall Down Stairs', Sprl).dir(Dn, 0x1d, 0, HTH).ss(S, 0x11, 0x80, False, True), create_door(player, 'GT Brightly Lit Hall NW', Nrml).dir(No, 0x1d, Left, High).big_key().pos(0), create_door(player, 'GT Agahnim 2 SW', Nrml).dir(So, 0x0d, Left, High).no_exit().trap(0x4).pos(0) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index e76eb78b..9bd79661 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -24,17 +24,20 @@ class GraphPiece: self.hanger_crystal = None self.hooks = {} self.visited_regions = set() + self.possible_bk_locations = set() -def generate_dungeon(available_sectors, entrance_region_names, world, player): +def generate_dungeon(available_sectors, entrance_region_names, split_dungeon, world, player): logger = logging.getLogger('') entrance_regions = convert_regions(entrance_region_names, world, player) doors_to_connect = set() all_regions = set() + bk_needed = False for sector in available_sectors: for door in sector.outstanding_doors: doors_to_connect.add(door) all_regions.update(sector.regions) + bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) proposed_map = {} choices_master = [[]] depth = 0 @@ -51,7 +54,7 @@ def generate_dungeon(available_sectors, entrance_region_names, world, player): 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_cache[depth] = dungeon, hangers, hooks - valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions) + valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed) else: dungeon, hangers, hooks = dungeon_cache[depth] valid = True @@ -96,10 +99,20 @@ def generate_dungeon(available_sectors, entrance_region_names, world, player): return master_sector +def determine_if_bk_needed(sector, split_dungeon, world, player): + if not split_dungeon: + for region in sector.regions: + for ext in region.exits: + door = world.check_for_door(ext.name, player) + if door is not None and door.bigKey: + return True + return False + + def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, world, player): # step 1 create dungeon: Dict dungeon = {} - original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, True, world, player) + original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, world, player) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map) doors_to_connect = set() hanger_set = set() @@ -110,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, False, world, player) + o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, 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 @@ -170,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, False, world, player) + b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, world, player) dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map) @@ -231,7 +244,7 @@ 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): +def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed): # evaluate if everything is still plausible # only origin is left in the dungeon and not everything is connected @@ -265,10 +278,15 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg if len(outstanding_doors[key]) > 0 and len(hangers[key]) == 0 and len(hooks[opp_key]) == 0: return False all_visited = set() + bk_possible = not bk_needed for piece in dungeon.values(): all_visited.update(piece.visited_regions) + if not bk_possible and len(piece.possible_bk_locations) > 0: + bk_possible = True if len(all_regions.difference(all_visited)) > 0: return False + if not bk_possible: + return False new_hangers_found = True accessible_hook_types = [] hanger_matching = set() @@ -346,9 +364,15 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map): graph_piece.visited_regions.update(o_state.visited_orange) graph_piece.visited_regions.update(b_state.visited_blue) graph_piece.visited_regions.update(b_state.visited_orange) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found)) return graph_piece +def filter_for_potential_bk_locations(locations): + return [x for x in locations if '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events and x.name not in key_only_locations.keys()] + + def parent_region(door, world, player): return world.get_entrance(door.name, player) @@ -496,6 +520,7 @@ class ExplorationState(object): self.used_locations = 0 self.key_locations = 0 self.used_smalls = 0 + self.bk_found = set() self.non_door_entrances = [] @@ -520,6 +545,7 @@ class ExplorationState(object): ret.used_locations = self.used_locations ret.used_smalls = self.used_smalls ret.found_locations = list(self.found_locations) + ret.bk_found = set(self.bk_found) ret.non_door_entrances = list(self.non_door_entrances) return ret @@ -529,7 +555,7 @@ class ExplorationState(object): self.crystal = exp_door.crystal return exp_door - def visit_region(self, region, key_region=None, key_checks=False): + def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): if self.crystal == CrystalBarrier.Either: if region not in self.visited_blue: self.visited_blue.append(region) @@ -547,6 +573,8 @@ class ExplorationState(object): self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) + if not bk_Flag: + self.bk_found.add(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) @@ -607,22 +635,22 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) - def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, isOrigin, world, player): + def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player): for door in get_doors(world, region, player): - if self.can_traverse_bk_check(door, isOrigin): + if self.can_traverse(door): if door.controller is not None: door = door.controller if door.dest is None and door not in proposed_map.keys() and door in valid_doors: if not self.in_door_list_ic(door, self.unattached_doors): - self.append_door_to_list(door, self.unattached_doors) + self.append_door_to_list(door, self.unattached_doors, flag) else: other = self.find_door_in_list(door, self.unattached_doors) if self.crystal != other.crystal: other.crystal = CrystalBarrier.Either 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) + self.append_door_to_list(door, self.event_doors, flag) elif not self.in_door_list(door, self.avail_doors): - self.append_door_to_list(door, self.avail_doors) + self.append_door_to_list(door, self.avail_doors, flag) def add_all_doors_check_key_region(self, region, key_region, world, player): for door in get_doors(world, region, player): @@ -676,9 +704,16 @@ class ExplorationState(object): return False if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal - return not isOrigin or not door.bigKey or len(self.found_locations) > 0 + return not isOrigin or not door.bigKey or self.count_locations_exclude_specials() > 0 # return not door.bigKey or len([x for x in self.found_locations if '- Prize' not in x.name]) > 0 + def count_locations_exclude_specials(self): + cnt = 0 + for loc in self.found_locations: + if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and loc.name not in key_only_locations.keys(): + cnt += 1 + return cnt + def validate(self, door, region, world, player): return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, world, player) @@ -702,11 +737,11 @@ class ExplorationState(object): return d return None - def append_door_to_list(self, door, door_list): + def append_door_to_list(self, door, door_list, flag=False): if door.crystal == CrystalBarrier.Null: - door_list.append(ExplorableDoor(door, self.crystal)) + door_list.append(ExplorableDoor(door, self.crystal, flag)) else: - door_list.append(ExplorableDoor(door, door.crystal)) + door_list.append(ExplorableDoor(door, door.crystal, flag)) def key_door_sort(self, d): if d.door.smallKey: @@ -719,9 +754,10 @@ class ExplorationState(object): class ExplorableDoor(object): - def __init__(self, door, crystal): + def __init__(self, door, crystal, flag): self.door = door self.crystal = crystal + self.flag = flag def __str__(self): return str(self.__unicode__()) @@ -745,11 +781,11 @@ def extend_reachable_state(search_regions, state, world, player): return local_state -def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player): +def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, 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, isOrigin, world, player) + 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 in proposed_map: @@ -758,8 +794,9 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d connect_region = world.get_entrance(explorable_door.door.name, player).connected_region if connect_region is not None: if valid_region_to_explore(connect_region, world, player) and not local_state.visited(connect_region): - local_state.visit_region(connect_region) - local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, isOrigin, world, player) + flag = explorable_door.flag or explorable_door.door.bigKey + local_state.visit_region(connect_region, bk_Flag=flag) + local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player) return local_state diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index b05663cf..0f822348 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -8,73 +8,14 @@ from DungeonGenerator import ExplorationState class KeySphere(object): def __init__(self): - self.access_doors = set() - self.free_locations = [] + self.access_door = None + self.free_locations = set() self.prize_region = False - self.key_only_locations = [] + self.key_only_locations = set() self.child_doors = set() self.bk_locked = False self.parent_sphere = 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 + self.other_locations = set() class KeyLayout(object): @@ -121,6 +62,7 @@ class DoorRules(object): self.alternate_big_key_loc = set() # for a place with only 1 free location/key_only_location behind it ... no goals and locations self.allow_small = False + self.small_location = None class KeyCounter(object): @@ -133,11 +75,21 @@ class KeyCounter(object): self.open_doors = set() self.used_keys = 0 self.big_key_opened = False + self.important_location = False def update(self, key_sphere): self.free_locations.update(key_sphere.free_locations) self.key_only_locations.update(key_sphere.key_only_locations) - self.child_doors.update(key_sphere.child_doors) + self.child_doors.update([x for x in key_sphere.child_doors if x not in self.open_doors and x.dest not in self.open_doors]) + self.important_location = self.important_location or key_sphere.prize_region or self.special_region(key_sphere) + + @staticmethod + def special_region(key_sphere): + for other in key_sphere.other_locations: + # todo: zelda's cell is special in standard, and probably crossed too + if other.name in ['Attic Cracked Floor', 'Suspicious Maiden']: + return True + return False def open_door(self, door, flat_proposal): if door in flat_proposal: @@ -146,13 +98,15 @@ class KeyCounter(object): self.open_doors.add(door) if door.dest in flat_proposal: self.open_doors.add(door.dest) + if door.dest in self.child_doors: + self.child_doors.remove(door.dest) elif door.bigKey: self.big_key_opened = True self.child_doors.remove(door) self.open_doors.add(door) - def used_smalls_loc(self): - return max(self.used_keys - len(self.key_only_locations), 0) + def used_smalls_loc(self, reserve=0): + return max(self.used_keys + reserve - len(self.key_only_locations), 0) def copy(self): ret = KeyCounter(self.max_chests) @@ -161,6 +115,8 @@ class KeyCounter(object): ret.child_doors.update(self.child_doors) ret.used_keys = self.used_keys ret.open_doors.update(self.open_doors) + ret.big_key_opened = self.big_key_opened + ret.important_location = self.important_location return ret @@ -171,55 +127,56 @@ def analyze_dungeon(key_layout, world, player): key_logic = key_layout.key_logic key_layout.max_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys) - find_bk_locked_sections(key_layout) + find_bk_locked_sections(key_layout, world) key_counter = KeyCounter(key_layout.max_chests) key_counter.update(key_layout.key_spheres['Origin']) queue = collections.deque([(key_layout.key_spheres['Origin'], key_counter)]) + doors_completed = set() while len(queue) > 0: key_sphere, key_counter = queue.popleft() - chest_keys = available_chest_small_keys(key_counter, False, world) # todo: when to count the bk chests + 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 possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) - # todo: big chest counts? - if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls: - key_logic.bk_restricted.update(key_counter.free_locations) - # logic min - if not key_sphere.bk_locked and big_chest_in_locations(key_counter.free_locations): - key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) - # if available <= possible_smalls: - # in this case, at least 1 child must have the available rule - unless relaxing is possible + if not key_counter.big_key_opened: + if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls: + key_logic.bk_restricted.update(key_counter.free_locations) + # logic min? + if not key_sphere.bk_locked and big_chest_in_locations(key_counter.free_locations): + key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) + minimal_keys = None + # todo: this feels like big key doors aren't accounted for - you may or may not find the big_key door at this point + if available + key_counter.used_keys <= possible_smalls: + minimal_keys = available + key_counter.used_keys + # todo: detect forced subsequent keys - see keypuzzles # try to relax the rules here? for child in key_sphere.child_doors: next_sphere = key_layout.key_spheres[child.name] - if not empty_sphere(next_sphere): + if not empty_sphere(next_sphere) and child not in doors_completed: if not child.bigKey: - # todo: calculate based on big key doors vs smalls - eastern dark square - rule = DoorRules(min(available, possible_smalls) + key_counter.used_keys) + expanded_counter = expand_counter_to_last_door(child, key_counter, key_layout, set()) + rule = create_rule(expanded_counter, key_layout, minimal_keys, world) + check_for_self_lock_key(rule, next_sphere, key_layout, world) + bk_restricted_rules(rule, next_sphere, key_counter, key_layout, minimal_keys, 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) queue.append((next_sphere, next_counter)) return key_layout - # for child in key_sphere.child_doors: - # next_sphere = key_spheres[child.name] - # if not empty_sphere(next_sphere): - # sm_rule = calc_basic_small_key_rule(key_sphere, key_spheres, key_layout, flat_proposal, world, player) - - -def find_bk_locked_sections(key_layout): +def find_bk_locked_sections(key_layout, world): key_spheres = key_layout.key_spheres key_logic = key_layout.key_logic bk_key_not_required = set() - big_chest_allowed_big_key = True + big_chest_allowed_big_key = world.accessibility != 'locations' for key in key_spheres.keys(): sphere = key_spheres[key] key_layout.all_chest_locations.update(sphere.free_locations) - if sphere.bk_locked and sphere.prize_region: + if sphere.bk_locked and (sphere.prize_region or KeyCounter.special_region(sphere)): big_chest_allowed_big_key = False if not sphere.bk_locked: bk_key_not_required.update(sphere.free_locations) @@ -234,6 +191,21 @@ def empty_sphere(sphere): return not sphere.prize_region +def relative_empty_sphere(sphere, key_counter): + if len(sphere.key_only_locations.difference(key_counter.key_only_locations)) > 0: + return False + if len(sphere.free_locations.difference(key_counter.free_locations)) > 0: + return False + new_child_door = False + for child in sphere.child_doors: + if child not in key_counter.child_doors and child not in key_counter.open_doors and (not child.bigKey or not key_counter.big_key_opened): + new_child_door = True + break + if new_child_door: + return False + return True + + def increment_key_counter(door, sphere, key_counter, flat_proposal): new_counter = key_counter.copy() new_counter.open_door(door, flat_proposal) @@ -241,75 +213,75 @@ def increment_key_counter(door, sphere, key_counter, flat_proposal): return new_counter -def check_for_big_doors(door, key_counter, key_layout): +def expand_counter_to_last_door(door, key_counter, key_layout, ignored_doors): + door_sphere = key_layout.key_spheres[door.name] + small_doors = set() big_doors = set() for other in key_counter.child_doors: - if other != door and other.bigKey: - big_doors.add(other) - big_key_available = len(key_counter.free_location) - key_counter.used_smalls_loc > 0 - if len(big_doors) == 0 or not big_key_available: + if other != door and other not in ignored_doors: + if other.bigKey: + big_doors.add(other) + elif other.dest not in small_doors: + small_doors.add(other) + # I feel bk might be available if the current small door could use a key_only_loc - the param might cover this case + big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0 + if len(small_doors) == 0 and (len(big_doors) == 0 or not big_key_available): return key_counter new_counter = key_counter - for big_door in big_doors: - big_sphere = key_layout.key_spheres[big_door.name] - new_counter = increment_key_counter(big_door, big_sphere, new_counter, key_layout.flat_prop) - # nested big key doors? + last_counter = key_counter + new_ignored = set(ignored_doors) + for new_door in small_doors.union(big_doors): + new_sphere = key_layout.key_spheres[new_door.name] + new_counter = increment_key_counter(new_door, new_sphere, new_counter, key_layout.flat_prop) + # this means the new_door invalidates the door / leads to the same stuff + if relative_empty_sphere(door_sphere, new_counter): + new_counter = last_counter + new_ignored.add(new_door) + else: + last_counter = new_counter old_counter = None while old_counter != new_counter: old_counter = new_counter - new_counter = check_for_big_doors(door, old_counter, key_layout) - # I think I've opened them all! + new_counter = expand_counter_to_last_door(door, old_counter, key_layout, new_ignored) return new_counter -# def calc_basic_small_key_rule(key_sphere, key_spheres, key_layout, flat_proposal, world, player): -# free_locations = set() -# key_only_locations = set() -# offshoot_doors = set() -# queue = collections.deque() -# parent = key_sphere.parent_sphere -# while parent is not None: -# queue.append(parent) -# parent = parent.parent_sphere -# while len(queue) > 0: -# previous = queue.popleft() -# free_locations.update(previous.free_locations) -# key_only_locations.update(previous.key_only_locations) -# for other_door in parent.child_doors: -# if other_door not in key_sphere.access_doors: -# offshoot_doors.add(other_door) -# # todo: bk versions -# chest_keys = available_chest_small_keys(key_layout, free_locations, key_sphere.bk_locked, world, player) -# parent_avail = chest_keys + len(key_only_locations) -# -# usuable_elsewhere = 0 -# open_set = set() -# queue = collections.deque(offshoot_doors) -# while len(queue) > 0: -# offshoot = queue.popleft() -# open_set.add(offshoot) -# if offshoot in flat_proposal: -# usuable_elsewhere += 1 -# # else bk door -# if offshoot.dest in flat_proposal: -# open_set.add(offshoot.dest) -# off_sphere = key_spheres[offshoot.name] -# free_locations.update(off_sphere.free_locations) -# key_only_locations.update(off_sphere.key_only_locations) -# for other_door in off_sphere.child_doors: -# if other_door not in key_sphere.access_doors and other_door not in open_set: -# queue.append(other_door) -# # todo: bk versions -# offshoot_chest = available_chest_small_keys(key_layout, free_locations, key_sphere.bk_locked, world, player) -# offshoot_avail = offshoot_chest + len(key_only_locations) -# -# if usuable_elsewhere == parent_avail and offshoot_avail > parent_avail: -# return usuable_elsewhere + 1 -# if usuable_elsewhere == parent_avail and offshoot_avail == parent_avail: -# return usuable_elsewhere -# if usuable_elsewhere < parent_avail: -# return usuable_elsewhere + 1 -# return 10 +def create_rule(key_counter, key_layout, minimal_keys, 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 + possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) + required_keys = min(available, possible_smalls) + key_counter.used_keys + if minimal_keys is None or required_keys <= minimal_keys: + return DoorRules(required_keys) + else: + return DoorRules(minimal_keys) + + +def check_for_self_lock_key(rule, sphere, key_layout, world): + if world.accessibility != 'locations': + counter = KeyCounter(key_layout.max_chests) + counter.update(sphere) + if not self_lock_possible(counter): + return + queue = collections.deque(counter.child_doors) + already_queued = set(counter.child_doors) + while len(queue) > 0: + child = queue.popleft() + if child not in counter.open_doors: + counter = increment_key_counter(child, key_layout.key_spheres[child.name], counter, key_layout.flat_prop) + if not self_lock_possible(counter): + return + for new_door in counter.child_doors: + if new_door not in already_queued: + queue.append(new_door) + already_queued.add(new_door) + if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: + rule.allow_small = True + rule.small_location = next(iter(counter.free_locations)) + + +def self_lock_possible(counter): + return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location def available_chest_small_keys(key_counter, bk, world): @@ -322,14 +294,59 @@ def available_chest_small_keys(key_counter, bk, world): else: return key_counter.max_chests - # derive key rules from key regions - # how many small key available at a given point (locations found / keysanity / retro) - # how many doors can be opened before you vs. smalls available - # soft lock detection - should it be run here? - # run with both bk off (locked behind current door) and bk found (elsewhere in the dungeon) - # rules generally smaller if bk locked behind current door - # big key restriction based on bk_locked - # prize regions - TT is weird as there are intermediate goals - assume child doors as well? + +def bk_restricted_rules(rule, sphere, key_counter, key_layout, minimal_keys, 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, world).small_key_num + if bk_number == rule.small_key_num: + return + post_counter = KeyCounter(key_layout.max_chests) + post_counter.update(sphere) + other_doors_beyond_me = [x for x in post_counter.child_doors if not x.bigKey] + queue = collections.deque(other_doors_beyond_me) + already_queued = set(other_doors_beyond_me) + while len(queue) > 0: + child = queue.popleft() + if child not in post_counter.open_doors: + post_counter = increment_key_counter(child, key_layout.key_spheres[child.name], post_counter, key_layout.flat_prop) + for new_door in post_counter.child_doors: + if not new_door.bigKey and new_door not in already_queued and new_door.dest not in already_queued: + queue.append(new_door) + already_queued.add(new_door) + unique_loc = post_counter.free_locations.difference(expanded_counter.free_locations) + if len(unique_loc) > 0: + rule.alternate_small_key = bk_number + rule.alternate_big_key_loc.update(unique_loc) + + +def expand_counter_no_big_doors(door, key_counter, key_layout, ignored_doors): + door_sphere = key_layout.key_spheres[door.name] + small_doors = set() + for other in key_counter.child_doors: + if other != door and other not in ignored_doors: + if other.dest not in small_doors and not other.bigKey: + small_doors.add(other) + if len(small_doors) == 0: + return key_counter + new_counter = key_counter + last_counter = key_counter + new_ignored = set(ignored_doors) + for new_door in small_doors: + new_sphere = key_layout.key_spheres[new_door.name] + new_counter = increment_key_counter(new_door, new_sphere, new_counter, key_layout.flat_prop) + # this means the new_door invalidates the door / leads to the same stuff + if relative_empty_sphere(door_sphere, new_counter): + new_counter = last_counter + new_ignored.add(new_door) + else: + last_counter = new_counter + old_counter = None + while old_counter != new_counter: + old_counter = new_counter + new_counter = expand_counter_no_big_doors(door, old_counter, key_layout, new_ignored) + return new_counter def create_key_spheres(key_layout, world, player): @@ -352,28 +369,15 @@ def create_key_spheres(key_layout, world, player): open_a_door(door, child_state, flat_proposal) expand_key_state(child_state, flat_proposal, world, player) child_kr = create_key_sphere(child_state, next_key_sphere, door) - check_for_duplicates_sub_super_set(key_spheres, child_kr, door.name) - queue.append((child_kr, child_state)) + if door.name not in key_spheres.keys(): + key_spheres[door.name] = child_kr + queue.append((child_kr, child_state)) + else: + old_sphere = key_spheres[door.name] + old_sphere.bk_locked = old_sphere.bk_locked and child_kr.bk_locked return key_spheres -def check_for_duplicates_sub_super_set(key_spheres, new_kr, door_name): - is_new = True - for kr in key_spheres.values(): - if new_kr == kr: # todo: what about parent regions... - kr.access_doors.update(new_kr.access_doors) - kr.child_doors.update(new_kr.child_doors) - key_spheres[door_name] = kr - is_new = False - break - # if new_kr.issubset(kr): - # break - # if new_kr.issuperset(kr): - # break - if is_new: - key_spheres[door_name] = new_kr - - def create_key_sphere(state, parent_sphere, door): key_sphere = KeySphere() key_sphere.parent_sphere = parent_sphere @@ -382,7 +386,9 @@ def create_key_sphere(state, parent_sphere, door): 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) + parent_locations.update(p_region.free_locations) + parent_locations.update(p_region.key_only_locations) + parent_locations.update(p_region.other_locations) p_region = p_region.parent_sphere u_doors = unique_doors(state.small_doors+state.big_doors).difference(parent_doors) key_sphere.child_doors.update(u_doors) @@ -390,14 +396,17 @@ def create_key_sphere(state, parent_sphere, door): for loc in region_locations: if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: key_sphere.prize_region = True + key_sphere.other_locations.add(loc) elif loc.event and 'Small Key' in loc.item.name: - key_sphere.key_only_locations.append(loc) + key_sphere.key_only_locations.add(loc) elif loc.name not in dungeon_events: - key_sphere.free_locations.append(loc) + key_sphere.free_locations.add(loc) + else: + key_sphere.other_locations.add(loc) # todo: Cellblock in a dungeon with a big_key door or chest - Crossed Mode key_sphere.bk_locked = state.big_key_opened if not state.big_key_special else False if door is not None: - key_sphere.access_doors.add(door) + key_sphere.access_door = door return key_sphere @@ -483,66 +492,175 @@ def flatten_pair_list(paired_list): return flat_list -## vanilla validation code +# Soft lock stuff +class SoftLockException(Exception): + pass + +# vanilla validation code def validate_vanilla_key_logic(world, player): validators = { - 'Hyrule Castle': val_unimplemented, + 'Hyrule Castle': val_hyrule, 'Eastern Palace': val_eastern, 'Desert Palace': val_desert, 'Tower of Hera': val_hera, 'Agahnims Tower': val_tower, - 'Palace of Darkness': val_unimplemented, - 'Swamp Palace': val_unimplemented, - 'Skull Woods': val_unimplemented, - 'Thieves Town': val_unimplemented, - 'Ice Palace': val_unimplemented, - 'Misery Mire': val_unimplemented, - 'Turtle Rock': val_unimplemented, - 'Ganons Tower': val_unimplemented + 'Palace of Darkness': val_pod, + 'Swamp Palace': val_swamp, + 'Skull Woods': val_skull, + 'Thieves Town': val_thieves, + 'Ice Palace': val_ice, + 'Misery Mire': val_mire, + 'Turtle Rock': val_turtle, + 'Ganons Tower': val_ganons } key_logic_dict = world.key_logic[player] for key, key_logic in key_logic_dict.items(): - validators[key](key_logic) + validators[key](key_logic, world, player) -def val_unimplemented(key_logic): - assert True +def val_hyrule(key_logic, world, player): + val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2) + val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2) + val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2) + val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest') + # val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4) -def val_eastern(key_logic): - dark_square_rule = key_logic.door_rules['Eastern Dark Square Key Door WN'] - assert dark_square_rule.small_key_num == 2 - # todo: allow big_key behind the door - # assert dark_square_rule.alternate_small_key == 1 - # assert 'Eastern Palace - Big Key Chest' in dark_square_rule.alternat_big_key_loc - # assert len(dark_square_rule.alternat_big_key_loc) == 1 - assert key_logic.door_rules['Eastern Darkness Up Stairs'].small_key_num == 2 - assert 'Eastern Palace - Big Chest' in key_logic.bk_restricted - assert 'Eastern Palace - Boss' in key_logic.bk_restricted +def val_eastern(key_logic, world, player): + # val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, False, None, 1, {'Eastern Palace - Big Key Chest'}) + val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 1) + val_rule(key_logic.door_rules['Eastern Darkness Up Stairs'], 2) + assert world.get_location('Eastern Palace - Big Chest', player) in key_logic.bk_restricted + assert world.get_location('Eastern Palace - Boss', player) in key_logic.bk_restricted assert len(key_logic.bk_restricted) == 2 -def val_desert(key_logic): - assert key_logic.door_rules['Desert East Wing Key Door EN'].small_key_num == 2 - assert key_logic.door_rules['Desert Tiles 1 Up Stairs'].small_key_num == 2 - assert key_logic.door_rules['Desert Beamos Hall NE'].small_key_num == 3 - assert key_logic.door_rules['Desert Tiles 2 NE'].small_key_num == 4 - assert 'Desert Palace - Big Chest' in key_logic.bk_restricted - assert 'Desert Palace - Boss' in key_logic.bk_restricted +def val_desert(key_logic, world, player): + val_rule(key_logic.door_rules['Desert East Wing Key Door EN'], 2) + val_rule(key_logic.door_rules['Desert Tiles 1 Up Stairs'], 2) + val_rule(key_logic.door_rules['Desert Beamos Hall NE'], 3) + val_rule(key_logic.door_rules['Desert Tiles 2 NE'], 4) + assert world.get_location('Desert Palace - Big Chest', player) in key_logic.bk_restricted + assert world.get_location('Desert Palace - Boss', player) in key_logic.bk_restricted assert len(key_logic.bk_restricted) == 2 -def val_hera(key_logic): - assert key_logic.door_rules['Hera Lobby Key Stairs'].small_key_num == 1 - assert 'Tower of Hera - Big Chest' in key_logic.bk_restricted - assert 'Tower of Hera - Compass Chest' in key_logic.bk_restricted - assert 'Tower of Hera - Boss' in key_logic.bk_restricted +def val_hera(key_logic, world, player): + val_rule(key_logic.door_rules['Hera Lobby Key Stairs'], 1, True, 'Tower of Hera - Big Key Chest') + assert world.get_location('Tower of Hera - Big Chest', player) in key_logic.bk_restricted + assert world.get_location('Tower of Hera - Compass Chest', player) in key_logic.bk_restricted + assert world.get_location('Tower of Hera - Boss', player) in key_logic.bk_restricted assert len(key_logic.bk_restricted) == 3 -def val_tower(key_logic): - assert key_logic.door_rules['Tower Room 03 Up Stairs'].small_key_num == 1 - assert key_logic.door_rules['Tower Dark Maze ES'].small_key_num == 2 - assert key_logic.door_rules['Tower Dark Chargers Up Stairs'].small_key_num == 3 - assert key_logic.door_rules['Tower Circle of Pots WS'].small_key_num == 4 +def val_tower(key_logic, world, player): + val_rule(key_logic.door_rules['Tower Room 03 Up Stairs'], 1) + val_rule(key_logic.door_rules['Tower Dark Maze ES'], 2) + val_rule(key_logic.door_rules['Tower Dark Archers Up Stairs'], 3) + val_rule(key_logic.door_rules['Tower Circle of Pots WS'], 4) + + +def val_pod(key_logic, world, player): + val_rule(key_logic.door_rules['PoD Arena Main NW'], 4) + val_rule(key_logic.door_rules['PoD Basement Ledge Up Stairs'], 6, True, 'Palace of Darkness - Big Key Chest') + val_rule(key_logic.door_rules['PoD Compass Room SE'], 6, True, 'Palace of Darkness - Harmless Hellway') + val_rule(key_logic.door_rules['PoD Falling Bridge WN'], 6) + val_rule(key_logic.door_rules['PoD Dark Pegs WN'], 6) + assert world.get_location('Palace of Darkness - Big Chest', player) in key_logic.bk_restricted + assert world.get_location('Palace of Darkness - Boss', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 2 + + +def val_swamp(key_logic, world, player): + val_rule(key_logic.door_rules['Swamp Entrance Down Stairs'], 1) + val_rule(key_logic.door_rules['Swamp Pot Row WS'], 2) + val_rule(key_logic.door_rules['Swamp Trench 1 Key Ledge NW'], 3) + val_rule(key_logic.door_rules['Swamp Hub North Ledge N'], 5) + val_rule(key_logic.door_rules['Swamp Hub WN'], 6) + val_rule(key_logic.door_rules['Swamp Waterway NW'], 6) + assert world.get_location('Swamp Palace - Entrance', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 1 + + +def val_skull(key_logic, world, player): + val_rule(key_logic.door_rules['Skull 3 Lobby NW'], 4) + val_rule(key_logic.door_rules['Skull Spike Corner ES'], 5) + + +def val_thieves(key_logic, world, player): + val_rule(key_logic.door_rules['Thieves Hallway WS'], 1) + val_rule(key_logic.door_rules['Thieves Spike Switch Up Stairs'], 3) + val_rule(key_logic.door_rules['Thieves Conveyor Bridge WS'], 3, True, 'Thieves\' Town - Big Chest') + assert world.get_location('Thieves\' Town - Attic', player) in key_logic.bk_restricted + assert world.get_location('Thieves\' Town - Boss', player) in key_logic.bk_restricted + assert world.get_location('Thieves\' Town - Blind\'s Cell', player) in key_logic.bk_restricted + assert world.get_location('Thieves\' Town - Big Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 4 + + +def val_ice(key_logic, world, player): + val_rule(key_logic.door_rules['Ice Jelly Key Down Stairs'], 1) + val_rule(key_logic.door_rules['Ice Conveyor SW'], 2) + val_rule(key_logic.door_rules['Ice Backwards Room Down Stairs'], 5) + assert world.get_location('Ice Palace - Boss', player) in key_logic.bk_restricted + assert world.get_location('Ice Palace - Big Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 2 + + +def val_mire(key_logic, world, player): + mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'} + val_rule(key_logic.door_rules['Mire Spikes NW'], 5) + val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 4, mire_west_wing) + val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 5, mire_west_wing) + assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted + assert world.get_location('Misery Mire - Big Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 2 + + +def val_turtle(key_logic, world, player): + val_rule(key_logic.door_rules['TR Hub NW'], 1) + val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2) + val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3) + val_rule(key_logic.door_rules['TR Pokey 2 ES'], 6, True, 'Turtle Rock - Big Key Chest', 4, {'Turtle Rock - Big Key Chest'}) + val_rule(key_logic.door_rules['TR Crystaroller Down Stairs'], 5) + val_rule(key_logic.door_rules['TR Dash Bridge WS'], 6) + assert world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Top Left', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Top Right', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Boss', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Crystaroller Room', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Big Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 7 + + +def val_ganons(key_logic, world, player): + rando_room = {'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'} + compass_room = {'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'} + gt_middle = {'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Chest', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest'} + val_rule(key_logic.door_rules['GT Double Switch EN'], 7, False, None, 5, rando_room.union({'Ganons Tower - Firesnake Room'})) + val_rule(key_logic.door_rules['GT Hookshot ES'], 8, True, 'Ganons Tower - Map Chest', 6, {'Ganons Tower - Map Chest'}) + val_rule(key_logic.door_rules['GT Tile Room EN'], 7, False, None, 5, compass_room) + val_rule(key_logic.door_rules['GT Firesnake Room SW'], 8, False, None, 6, rando_room) + val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 8, False, None, 6, gt_middle) + val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 7) + val_rule(key_logic.door_rules['GT Crystal Circles SW'], 8) + assert world.get_location('Ganons Tower - Mini Helmasaur Room - Left', player) in key_logic.bk_restricted + assert world.get_location('Ganons Tower - Mini Helmasaur Room - Right', player) in key_logic.bk_restricted + assert world.get_location('Ganons Tower - Big Chest', player) in key_logic.bk_restricted + assert world.get_location('Ganons Tower - Pre-Moldorm Chest', player) in key_logic.bk_restricted + assert world.get_location('Ganons Tower - Validation Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 5 + + +def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None): + if setCheck is None: + setCheck = set() + assert rule.small_key_num == skn + assert rule.allow_small == allow + assert rule.small_location == loc or rule.small_location.name == loc + assert rule.alternate_small_key == askn + assert len(setCheck) == len(rule.alternate_big_key_loc) + for loc in rule.alternate_big_key_loc: + assert loc.name in setCheck diff --git a/Regions.py b/Regions.py index b73d717a..5441007a 100644 --- a/Regions.py +++ b/Regions.py @@ -438,8 +438,8 @@ def create_regions(world, player): create_dungeon_region(player, 'Skull Star Pits', 'Skull Woods', None, ['Skull Star Pits SW', 'Skull Star Pits WS']), create_dungeon_region(player, 'Skull Torch Room', 'Skull Woods', None, ['Skull Torch Room ES', 'Skull Torch Room EN']), create_dungeon_region(player, 'Skull Vines', 'Skull Woods', None, ['Skull Vines WN', 'Skull Vines NW']), - create_dungeon_region(player, 'Skull Spike Corner', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop'], ['Skull Spike Corner SW', 'Skull Spike Corner WS']), - create_dungeon_region(player, 'Skull Final Drop', 'Skull Woods', None, ['Skull Final Drop ES', 'Skull Final Drop Hole']), + create_dungeon_region(player, 'Skull Spike Corner', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop'], ['Skull Spike Corner SW', 'Skull Spike Corner ES']), + create_dungeon_region(player, 'Skull Final Drop', 'Skull Woods', None, ['Skull Final Drop WS', 'Skull Final Drop Hole']), create_dungeon_region(player, 'Skull Boss', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), # tt diff --git a/Rules.py b/Rules.py index b18d1c5d..1789bdb9 100644 --- a/Rules.py +++ b/Rules.py @@ -265,8 +265,6 @@ def global_rules(world, player): set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player)) # Big key rules set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Eastern Palace - Big Chest', player), 'Big Key (Eastern Palace)', player) set_rule(world.get_entrance('Eastern Big Key NE', player), lambda state: state.has('Big Key (Eastern Palace)', player)) set_rule(world.get_entrance('Eastern Courtyard N', player), lambda state: state.has('Big Key (Eastern Palace)', player)) @@ -276,8 +274,6 @@ def global_rules(world, player): # Desert set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Desert Palace - Big Chest', player), 'Big Key (Desert Palace)', player) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player)) @@ -285,8 +281,6 @@ def global_rules(world, player): # Tower of Hera set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Tower of Hera - Big Chest', player), 'Big Key (Tower of Hera)', player) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Hera Startile Corner NW', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) @@ -299,8 +293,6 @@ def global_rules(world, player): set_rule(world.get_entrance('PoD Bow Statue Down Ladder', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Dark Alley NE', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Palace of Darkness - Big Chest', player), 'Big Key (Palace of Darkness)', player) set_rule(world.get_entrance('PoD Map Balcony Drop Down', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs WN', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player)) @@ -336,15 +328,11 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)', player) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)', player) set_rule(world.get_entrance('Skull Torch Room EN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) @@ -353,8 +341,6 @@ def global_rules(world, player): set_rule(world.get_entrance('Thieves BK Corner NE', player), lambda state: state.has('Big Key (Thieves Town)', player)) # blind can't have the small key? - not necessarily true anymore - but likely still set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has('Big Key (Thieves Town)', player) and state.has('Hammer', player))) - if world.accessibility == 'locations': - forbid_item(world.get_location('Thieves\' Town - Big Chest', player), 'Big Key (Thieves Town)', player) for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']: set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player)) for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: @@ -368,8 +354,6 @@ def global_rules(world, player): set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Ice Palace - Big Chest', player), 'Big Key (Ice Palace)', player) set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) @@ -390,8 +374,6 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player) and (state.has_Boots(player) or state.has('Hookshot', player))) - if world.accessibility == 'locations': - forbid_item(world.get_location('Misery Mire - Big Chest', player), 'Big Key (Misery Mire)', player) set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire BK Door Room N', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_entrance('Mire Square Rail NW', player), lambda state: state.has('Big Key (Misery Mire)', player)) @@ -413,8 +395,6 @@ def global_rules(world, player): set_rule(world.get_entrance('TR Hub NE', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Turtle Rock - Big Chest', player), 'Big Key (Turtle Rock)', player) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Dodgers NE', player), lambda state: state.has('Big Key (Turtle Rock)', player)) @@ -443,8 +423,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Firesnake Room Hook Path', player), lambda state: state.has('Hookshot', player)) # I am tempted to stick an invincibility rule for getting across falling bridge set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) - if world.accessibility == 'locations': - forbid_item(world.get_location('Ganons Tower - Big Chest', player), 'Big Key (Ganons Tower)', player) set_rule(world.get_entrance('GT Ice Armos NE', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_entrance('GT Ice Armos WS', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) @@ -1700,12 +1678,10 @@ def set_inverted_bunny_rules(world, player): def add_key_logic_rules(world, player): - logger = logging.getLogger('') key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): for door_name, keys in d_logic.door_rules.items(): - logger.debug(' %s needs %s keys', door_name, keys) - add_rule(world.get_entrance(door_name, player), create_key_rule(d_logic.small_key_name, player, keys)) + add_rule(world.get_entrance(door_name, player), create_advanced_key_rule(d_logic, player, keys)) for location in d_logic.bk_restricted: if location.name not in key_only_locations.keys(): forbid_item(location, d_logic.bk_name, player) @@ -1717,5 +1693,31 @@ def create_key_rule(small_key_name, player, keys): return lambda state: state.has_key(small_key_name, player, keys) -def create_forced_small_rule(small_key_name, player): - return lambda item: item.name == small_key_name and item.player == player +def create_key_rule_allow_small(small_key_name, player, keys, location): + loc = location.name + return lambda state: state.has_key(small_key_name, player, keys) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) + + +def create_key_rule_bk_exception(small_key_name, big_key_name, player, keys, bk_keys, bk_locs): + chest_names = [x.name for x in bk_locs] + return lambda state: state.has_key(small_key_name, player, keys) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys)) + + +def create_key_rule_bk_exception_or_allow(small_key_name, big_key_name, player, keys, location, bk_keys, bk_locs): + loc = location.name + chest_names = [x.name for x in bk_locs] + return lambda state: state.has_key(small_key_name, player, keys) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys)) + + +def create_advanced_key_rule(key_logic, player, rule): + if not rule.allow_small and rule.alternate_small_key is None: + return create_key_rule(key_logic.small_key_name, player, rule.small_key_num) + if rule.allow_small and rule.alternate_small_key is None: + return create_key_rule_allow_small(key_logic.small_key_name, player, rule.small_key_num, rule.small_location) + if not rule.allow_small and rule.alternate_small_key is not None: + return create_key_rule_bk_exception(key_logic.small_key_name, key_logic.bk_name, player, rule.small_key_num, + rule.alternate_small_key, rule.alternate_big_key_loc) + if rule.allow_small and rule.alternate_small_key is not None: + return create_key_rule_bk_exception_or_allow(key_logic.small_key_name, key_logic.bk_name, player, + rule.small_key_num, rule.small_location, rule.alternate_small_key, + rule.alternate_big_key_loc)