import collections from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs from DungeonGenerator import ExplorationState class KeySphere(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_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 class KeyLayout(object): def __init__(self, sector, starts, proposal): self.sector = sector self.start_regions = starts self.proposal = proposal self.key_logic = KeyLogic(sector.name) self.checked_states = {} self.key_spheres = None self.flat_prop = None self.max_chests = None self.all_chest_locations = set() # bk special? # bk required? True if big chests or big doors exists def reset(self, proposal): self.proposal = proposal self.key_logic = KeyLogic(self.sector.name) self.checked_states = {} class KeyLogic(object): def __init__(self, dungeon_name): self.door_rules = {} self.bk_restricted = set() self.sm_restricted = set() self.small_key_name = dungeon_keys[dungeon_name] self.bk_name = dungeon_bigs[dungeon_name] self.logic_min = {} self.logic_max = {} class DoorRules(object): def __init__(self, number): self.small_key_num = number # allowing a different number if bk is behind this door in a set of locations self.alternate_small_key = None 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 class KeyCounter(object): def __init__(self, max_chests): self.max_chests = max_chests self.free_locations = set() self.key_only_locations = set() self.child_doors = set() self.open_doors = set() self.used_keys = 0 self.big_key_opened = 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) def open_door(self, door, flat_proposal): if door in flat_proposal: self.used_keys += 1 self.child_doors.remove(door) self.open_doors.add(door) if door.dest in flat_proposal: self.open_doors.add(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 copy(self): ret = KeyCounter(self.max_chests) ret.free_locations.update(self.free_locations) ret.key_only_locations.update(self.key_only_locations) ret.child_doors.update(self.child_doors) ret.used_keys = self.used_keys ret.open_doors.update(self.open_doors) return ret def analyze_dungeon(key_layout, world, player): key_layout = KeyLayout(key_layout.sector, key_layout.start_regions, key_layout.proposal) key_layout.flat_prop = flatten_pair_list(key_layout.proposal) key_layout.key_spheres = create_key_spheres(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) 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)]) 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_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 # 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 child.bigKey: # todo: calculate based on big key doors vs smalls - eastern dark square rule = DoorRules(min(available, possible_smalls) + key_counter.used_keys) key_logic.door_rules[child.name] = rule 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): key_spheres = key_layout.key_spheres key_logic = key_layout.key_logic bk_key_not_required = set() big_chest_allowed_big_key = True 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: big_chest_allowed_big_key = False if not sphere.bk_locked: bk_key_not_required.update(sphere.free_locations) key_logic.bk_restricted.update(key_layout.all_chest_locations.difference(bk_key_not_required)) if not big_chest_allowed_big_key: key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations)) def empty_sphere(sphere): if len(sphere.key_only_locations) != 0 or len(sphere.free_locations) != 0 or len(sphere.child_doors) != 0: return False return not sphere.prize_region def increment_key_counter(door, sphere, key_counter, flat_proposal): new_counter = key_counter.copy() new_counter.open_door(door, flat_proposal) new_counter.update(sphere) return new_counter def check_for_big_doors(door, key_counter, key_layout): 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: 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? 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! 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 available_chest_small_keys(key_counter, bk, world): if not world.keysanity and world.mode != 'retro': cnt = 0 for loc in key_counter.free_locations: if bk or '- Big Chest' not in loc.name: cnt += 1 return min(cnt, key_counter.max_chests) 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 create_key_spheres(key_layout, world, player): key_spheres = {} flat_proposal = key_layout.flat_prop state = ExplorationState() state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions for region in key_layout.start_regions: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) key_spheres['Origin'] = create_key_sphere(state, None, None) queue = collections.deque([(key_spheres['Origin'], state)]) while len(queue) > 0: next_key_sphere, parent_state = queue.popleft() for door in next_key_sphere.child_doors: child_state = parent_state.copy() # open the door open_a_door(door, child_state, flat_proposal) expand_key_state(child_state, flat_proposal, world, player) 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)) 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 p_region = parent_sphere 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_sphere u_doors = unique_doors(state.small_doors+state.big_doors).difference(parent_doors) key_sphere.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_sphere.prize_region = True elif loc.event and 'Small Key' in loc.item.name: key_sphere.key_only_locations.append(loc) elif loc.name not in dungeon_events: key_sphere.free_locations.append(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) return key_sphere 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) # allows dest doors 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 # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 counted = set() for door in key_counter.child_doors: if door in proposal and door not in counted: cnt += 1 counted.add(door) counted.add(door.dest) return cnt def count_locations_big_optional(locations, bk=False): cnt = 0 for loc in locations: if bk or '- Big Chest' not in loc.name: cnt += 1 return cnt def big_chest_in_locations(locations): return len(find_big_chest_locations(locations)) > 0 def find_big_chest_locations(locations): ret = [] for loc in locations: if 'Big Chest' in loc.name: ret.append(loc) return ret 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, player): 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 ## vanilla validation code def validate_vanilla_key_logic(world, player): validators = { 'Hyrule Castle': val_unimplemented, '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 } key_logic_dict = world.key_logic[player] for key, key_logic in key_logic_dict.items(): validators[key](key_logic) def val_unimplemented(key_logic): assert True 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 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 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 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