diff --git a/BaseClasses.py b/BaseClasses.py index f06d077c..c1115837 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1027,6 +1027,7 @@ class Location(object): from Items import ItemFactory self.forced_item = ItemFactory([forced_item], player)[0] self.item = self.forced_item + self.item.location = self self.event = True else: self.forced_item = None diff --git a/DoorShuffle.py b/DoorShuffle.py index 82f46758..f3ccae58 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -628,6 +628,279 @@ def find_proposal(proposal, buckets, candidates): # code below is for an algorithm without restarts +class ExplorableDoor(object): + + def __init__(self, door, crystal): + self.door = door + self.crystal = crystal + + def __str__(self): + return str(self.__unicode__()) + + def __unicode__(self): + return '%s (%s)' % (self.door.name, self.crystal.name) + + +class ExplorationState(object): + + def __init__(self): + + self.unattached_doors = [] + self.avail_doors = [] + self.event_doors = [] + + self.visited_orange = [] + self.visited_blue = [] + self.events = set() + self.crystal = CrystalBarrier.Orange + + # key region stuff + self.door_krs = {} + + # key validation stuff + self.small_doors = [] + self.big_doors = [] + self.opened_doors = [] + self.big_key_opened = False + self.big_key_special = False + + self.found_locations = [] + self.ttl_locations = 0 + self.used_locations = 0 + self.key_locations = 0 + self.used_smalls = 0 + + def copy(self): + ret = ExplorationState() + ret.avail_doors = list(self.avail_doors) + ret.event_doors = list(self.event_doors) + ret.visited_orange = list(self.visited_orange) + ret.visited_blue = list(self.visited_blue) + ret.events = set(self.events) + ret.crystal = self.crystal + ret.door_krs = self.door_krs.copy() + + ret.small_doors = list(self.small_doors) + ret.big_doors = list(self.big_doors) + ret.opened_doors = list(self.opened_doors) + ret.big_key_opened = self.big_key_opened + ret.big_key_special = self.big_key_special + ret.ttl_locations = self.ttl_locations + ret.key_locations = self.key_locations + ret.used_locations = self.used_locations + ret.used_smalls = self.used_smalls + ret.found_locations = list(self.found_locations) + return ret + + def next_avail_door(self): + exp_door = self.avail_doors.pop() + self.crystal = exp_door.crystal + return exp_door + + def visit_region(self, region, key_region=None, key_checks=False): + if self.crystal == CrystalBarrier.Either: + if region not in self.visited_blue: + self.visited_blue.append(region) + if region not in self.visited_orange: + self.visited_orange.append(region) + elif self.crystal == CrystalBarrier.Orange: + self.visited_orange.append(region) + elif self.crystal == CrystalBarrier.Blue: + self.visited_blue.append(region) + for location in region.locations: + if location.name in dungeon_events and location.name not in self.events: + self.events.add(location.name) + queue = collections.deque(self.event_doors) + while len(queue) > 0: + exp_door = queue.pop() + if exp_door.door.req_event == location.name: + self.avail_doors.append(exp_door) + self.event_doors.remove(exp_door) + if key_region is not None: + d_name = exp_door.door.name + if d_name not in self.door_krs.keys(): + self.door_krs[d_name] = key_region + if key_checks and location not in self.found_locations: + if location.name in key_only_locations: + self.key_locations += 1 + if location.name not in dungeon_events and '- Prize' not in location.name: + self.ttl_locations += 1 + self.found_locations.append(location) + if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened: + self.big_key_opened = True + self.avail_doors.extend(self.big_doors) + self.big_doors.clear() + + def add_all_doors_check_unattached(self, region, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.dest is None and door not in self.unattached_doors: + self.append_door_to_list(door, self.unattached_doors) + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + + def add_all_doors_check_key_region(self, region, key_region, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + if door.name not in self.door_krs.keys(): + self.door_krs[door.name] = key_region + + + def add_all_doors_check_keys(self, region, key_door_proposal, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door in key_door_proposal and not self.in_door_list(door, self.small_doors) and door not in self.opened_doors: + self.append_door_to_list(door, self.small_doors) + elif door.bigKey and not self.big_key_opened and not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + + def visited(self, region): + if self.crystal == CrystalBarrier.Either: + return region in self.visited_blue and region in self.visited_orange + elif self.crystal == CrystalBarrier.Orange: + return region in self.visited_orange + elif self.crystal == CrystalBarrier.Blue: + return region in self.visited_blue + return False + + def can_traverse(self, door): + if door.blocked: + return False + if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: + return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal + return True + + def in_door_list(self, door, door_list): + for d in door_list: + if d.door == door and d.crystal == self.crystal: + return True + return False + + @staticmethod + def in_door_list_ic(door, door_list): + for d in door_list: + if d.door == door: + return True + return False + + def append_door_to_list(self, door, door_list): + if door.crystal == CrystalBarrier.Null: + door_list.append(ExplorableDoor(door, self.crystal)) + else: + door_list.append(ExplorableDoor(door, door.crystal)) + + def key_door_sort(self, d): + if d.door.smallKey: + if d.door in self.opened_doors: + return 1 + else: + return 0 + return 2 + + +def extend_reachable_state(search_regions, state, world, player): + local_state = state.copy() + for region in search_regions: + local_state.visit_region(region) + local_state.add_all_doors_check_unattached(region, world, player) + while len(local_state.avail_doors) > 0: + explorable_door = local_state.next_avail_door() + entrance = world.get_entrance(explorable_door.door.name, player) + connect_region = entrance.connected_region + if connect_region is not None: + if not local_state.visited(connect_region): + local_state.visit_region(connect_region) + local_state.add_all_doors_check_unattached(connect_region, world, player) + return local_state + + +def shuffle_dungeon_no_repeats_new(world, player, available_sectors, entrance_region_names): + logger = logging.getLogger('') + random.shuffle(available_sectors) + for sector in available_sectors: + random.shuffle(sector.outstanding_doors) + + entrance_regions = [] + # current_sector = None + for region_name in entrance_region_names: + entrance_regions.append(world.get_region(region_name, player)) + + state = extend_reachable_state(entrance_regions, ExplorationState(), world, player) + reachable_doors_new = state.unattached_doors + # Loop until all available doors are used + while len(reachable_doors_new) > 0: + # Pick a random available door to connect + explorable_door = random.choice(reachable_doors_new) + door = explorable_door.door + sector = find_sector_for_door(door, available_sectors) + sector.outstanding_doors.remove(door) + # door_connected = False + logger.info('Linking %s', door.name) + # Find an available region that has a compatible door + reachable_doors = [d.door for d in reachable_doors_new] + compatibles = find_all_compatible_door_in_sectors_ex(door, available_sectors, reachable_doors) + while len(compatibles) > 0: + connect_sector, connect_door = compatibles.pop() + logger.info(' Found possible new sector via %s', connect_door.name) + # Check if valid + if is_valid(door, connect_door, sector, connect_sector, available_sectors): + # Apply connection and add the new region's doors to the available list + maybe_connect_two_way(world, door, connect_door, player) + reachable_doors_new.remove(explorable_door) # todo: does this remove it from the state list? + connect_sector.outstanding_doors.remove(connect_door) + if sector != connect_sector: # combine if not the same + available_sectors.remove(connect_sector) + sector.outstanding_doors.extend(connect_sector.outstanding_doors) + sector.regions.extend(connect_sector.regions) + if not door.blocked: + connect_region = world.get_entrance(door.dest.name, player).parent_region + state = extend_reachable([connect_region], state, world, player) + break # skips else block below + logger.info(' Not Linking %s to %s', door.name, connect_door.name) + if len(compatibles) == 0: # time to try again + sector.outstanding_doors.insert(0, door) + if len(reachable_doors_new) <= 1: + raise Exception('Rejected last option due to dead end... infinite loop ensues') + else: + # If there's no available region with a door, use an internal connection + compatibles = find_all_compatible_door_in_list(door, reachable_doors) + while len(compatibles) > 0: + connect_door = compatibles.pop() + logger.info(' Adding loop via %s', connect_door.name) + # Check if valid + if is_loop_valid(door, connect_door, sector, available_sectors): + maybe_connect_two_way(world, door, connect_door, player) + reachable_doors_new.remove(explorable_door) + reachable_doors_new[:] = [ed for ed in reachable_doors_new if ed.door != connect_door] + connect_sector = find_sector_for_door(connect_door, available_sectors) + connect_sector.outstanding_doors.remove(connect_door) + if sector != connect_sector: # combine if not the same + available_sectors.remove(connect_sector) + sector.outstanding_doors.extend(connect_sector.outstanding_doors) + sector.regions.extend(connect_sector.regions) + break # skips else block with exception + else: + logger.info(' Not Linking %s to %s', door.name, connect_door.name) + sector.outstanding_doors.insert(0, door) + if len(reachable_doors_new) <= 2: + raise Exception('Rejected last option due to likely improper loops...') + else: + raise Exception('Nothing is apparently compatible with %s', door.name) + # Check that we used everything, we failed otherwise + if len(available_sectors) != 1: + logger.warning('Failed to add all regions/doors to dungeon, generation will likely fail.') + return available_sectors[0] + def shuffle_dungeon_no_repeats(world, player, available_sectors, entrance_region_names): logger = logging.getLogger('') @@ -956,107 +1229,30 @@ def ncr(n, r): return numerator / denominator -class KeyDoorState(object): - - def __init__(self): - self.avail_doors = [] - self.small_doors = [] - self.big_doors = [] - self.event_doors = [] - self.opened_doors = [] - self.big_key_opened = False - self.big_key_special = False - - self.visited_regions = set() - self.ttl_locations = 0 - self.used_locations = 0 - self.key_locations = 0 - self.used_smalls = 0 - self.events = set() - self.crystal = CrystalBarrier.Orange - - def copy(self): - ret = KeyDoorState() - ret.avail_doors = list(self.avail_doors) - ret.small_doors = list(self.small_doors) - ret.big_doors = list(self.big_doors) - ret.opened_doors = list(self.opened_doors) - ret.big_key_opened = self.big_key_opened - ret.big_key_special = self.big_key_special - ret.visited_regions = set(self.visited_regions) - ret.ttl_locations = self.ttl_locations - ret.key_locations = self.key_locations - ret.used_locations = self.used_locations - ret.used_smalls = self.used_smalls - ret.events = set(self.events) - ret.crystal = self.crystal - - return ret - - def validate_key_layout(sector, start_regions, key_door_proposal, world, player): flat_proposal = flatten_pair_list(key_door_proposal) - state = KeyDoorState() + state = ExplorationState() state.key_locations = len(world.get_dungeon(sector.name, player).small_keys) state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in sector.regions # Everything in a start region is in key region 0. for region in start_regions: - visit_region(state, region) - count_locations(state, region) - add_doors_to_lists(region, flat_proposal, state, state.crystal, world, player) + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) checked_states = set() return validate_key_layout_r(state, flat_proposal, checked_states, world, player) -def visit_region(state, region): - if state.crystal == CrystalBarrier.Either: - state.visited_regions.add((region, CrystalBarrier.Orange)) - state.visited_regions.add((region, CrystalBarrier.Blue)) - else: - state.visited_regions.add((region, state.crystal)) - for location in region.locations: - if location.name in dungeon_events and location.name not in state.events: - state.events.add(location.name) - queue = collections.deque(state.event_doors) - while len(queue) > 0: - door, crystal = queue.pop() - if door.req_event == location.name: - state.avail_doors.append((door, crystal)) - state.event_doors.remove((door, crystal)) - if region.name == 'Hyrule Dungeon Cellblock' and not state.big_key_opened: - state.big_key_opened = True - state.avail_doors.extend(state.big_doors) - state.big_doors.clear() - - -def count_locations(state, region): - for location in region.locations: - if location.name in key_only_locations: - state.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name: - state.ttl_locations += 1 - - -def is_region_visited(state, region): - if state.crystal == CrystalBarrier.Either: - return (region, CrystalBarrier.Blue) in state.visited_regions and (region, CrystalBarrier.Orange) in state.visited_regions - else: - return (region, state.crystal) in state.visited_regions - - def validate_key_layout_r(state, flat_proposal, checked_states, world, player): # improvements: remove recursion to make this iterative # store a cache of various states of opened door to increase speed of checks - many are repetitive while len(state.avail_doors) > 0: - door, crystal_state = state.avail_doors.pop() + exp_door = state.next_avail_door() + door = exp_door.door connect_region = world.get_entrance(door.name, player).connected_region - if can_traverse(crystal_state, door) and not is_region_visited(state, connect_region): - visit_region(state, connect_region) - count_locations(state, connect_region) - if door.crystal != CrystalBarrier.Null: - crystal_state = door.crystal - add_doors_to_lists(connect_region, flat_proposal, state, crystal_state, world, player) + if state.can_traverse(door) and not state.visited(connect_region): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) smalls_avail = len(state.small_doors) > 0 num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing if not smalls_avail and num_bigs == 0: @@ -1068,16 +1264,18 @@ def validate_key_layout_r(state, flat_proposal, checked_states, world, player): return False else: if smalls_avail and available_small_locations > 0: - for d, crystal_sd in state.small_doors: + for exp_door in state.small_doors: state_copy = state.copy() - state_copy.opened_doors.append(d) - state_copy.avail_doors.append((d, crystal_sd)) - state_copy.small_doors.remove((d, crystal_sd)) - if d.dest in flat_proposal: - state_copy.opened_doors.append(d.dest) - if in_door_list(d.dest, state_copy.small_doors): - state_copy.small_doors[:] = [x for x in state_copy.small_doors if x[0] != d.dest] - state_copy.avail_doors.append((d.dest, crystal_sd)) + state_copy.opened_doors.append(exp_door.door) + state_copy.avail_doors.append(exp_door) + state_copy.small_doors.remove(exp_door) + dest_door = exp_door.door.dest + if dest_door in flat_proposal: + state_copy.opened_doors.append(dest_door) + if state_copy.in_door_list_ic(dest_door, state_copy.small_doors): + now_available = [x for x in state_copy.small_doors if x.door == dest_door] + state_copy.small_doors[:] = [x for x in state_copy.small_doors if x.door != dest_door] + state_copy.avail_doors.extend(now_available) state_copy.used_locations += 1 state_copy.used_smalls += 1 code = state_id(state_copy, flat_proposal) @@ -1101,52 +1299,6 @@ def validate_key_layout_r(state, flat_proposal, checked_states, world, player): return valid -def can_traverse(crystal, door): - if door.blocked: - return False - if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]: - return crystal == CrystalBarrier.Either or door.crystal == crystal - return True - - -def add_doors_to_lists(region, key_door_proposal, state, crystal, world, player): - for door in get_doors(world, region, player): - if can_traverse(crystal, door): - if door in key_door_proposal and not_in_door_list(door, state.small_doors, crystal) and door not in state.opened_doors: - append_door_to_list(door, state.small_doors, crystal) - elif door.bigKey and not state.big_key_opened and not_in_door_list(door, state.big_doors, crystal): - append_door_to_list(door, state.big_doors, crystal) - elif door.req_event is not None and door.req_event not in state.events and not_in_door_list(door, state.event_doors, crystal): - append_door_to_list(door, state.event_doors, crystal) - elif not_in_door_list(door, state.avail_doors, crystal): - append_door_to_list(door, state.avail_doors, crystal) - - -def in_door_list(door, door_list): - for d, crystal in door_list: - if d == door: - return True - return False - -def not_in_door_list(door, door_list, crystal): - if crystal == CrystalBarrier.Either: - return (door, CrystalBarrier.Orange) not in door_list or (door, CrystalBarrier.Blue) not in door_list - else: - return (door, crystal) not in door_list - - -def append_door_to_list(door, door_list, crystal): - if crystal == CrystalBarrier.Either: - if (door, CrystalBarrier.Blue) not in door_list and (door, CrystalBarrier.Orange) not in door_list: - door_list.append((door, crystal)) - elif (door, CrystalBarrier.Blue) not in door_list: - door_list.append((door, CrystalBarrier.Blue)) - elif (door, CrystalBarrier.Orange) not in door_list: - door_list.append((door, CrystalBarrier.Orange)) - else: - door_list.append((door, crystal)) - - def state_id(state, flat_proposal): state_id = '1' if state.big_key_opened else '0' for d in flat_proposal: @@ -1180,6 +1332,7 @@ def reassign_key_doors(current_doors, proposal, world, player): d2 = obj[1] if d1.type is DoorType.Interior: change_door_to_small_key(d1, world, player) + d2.smallKey = True # ensure flag is set else: names = [d1.name, d2.name] found = False @@ -1201,6 +1354,7 @@ def reassign_key_doors(current_doors, proposal, world, player): d = obj if d.type is DoorType.Interior: change_door_to_small_key(d, world, player) + d.dest.smallKey = True # ensure flag is set elif d.type is DoorType.SpiralStairs: pass # we don't have spiral stairs candidates yet that aren't already key doors elif d.type is DoorType.Normal: diff --git a/Doors.py b/Doors.py index c3eeb9de..bcc98ff6 100644 --- a/Doors.py +++ b/Doors.py @@ -454,10 +454,38 @@ def create_doors(world, player): world.get_door('Swamp Drain Left Switch', player).event('Swamp Drain') world.get_door('Swamp Drain Right Switch', player).event('Swamp Drain') world.get_door('Swamp Flooded Room Ladder', player).event('Swamp Drain') - #todo - new region for flooded chests??? - # world.get_door('', player).event('') # crystal switches and barriers + world.get_door('Hera Lobby Down Stairs', player).c_switch() + world.get_door('Hera Lobby Key Stairs', player).c_switch() + world.get_door('Hera Lobby Up Stairs', player).c_switch() + world.get_door('Hera Basement Cage Up Stairs', player).c_switch() + world.get_door('Hera Tile Room Up Stairs', player).c_switch() + world.get_door('Hera Tile Room EN', player).c_switch() + world.get_door('Hera Tridorm WN', player).c_switch() + world.get_door('Hera Tridorm SE', player).c_switch() + world.get_door('Hera Beetles Down Stairs', player).c_switch() + world.get_door('Hera Beetles WS', player).c_switch() + world.get_door('Hera Beetles Holes', player).c_switch() + world.get_door('Hera Startile Wide SW', player).c_switch() + world.get_door('Hera Startile Wide Up Stairs', player).c_switch() + world.get_door('Hera Startile Wide Holes', player).c_switch() + + world.get_door('PoD Arena Main SW', player).c_switch() + world.get_door('PoD Arena Bridge SE', player).c_switch() + world.get_door('PoD Arena Main NW', player).barrier(CrystalBarrier.Orange) + world.get_door('PoD Arena Main NE', player).barrier(CrystalBarrier.Orange) + # maybe you can cross this way with blue up?? + world.get_door('PoD Arena Main Crystal Path', player).barrier(CrystalBarrier.Blue) + world.get_door('PoD Arena Crystals E', player).barrier(CrystalBarrier.Blue) + world.get_door('PoD Arena Crystal Path', player).barrier(CrystalBarrier.Blue) + world.get_door('PoD Sexy Statue W', player).c_switch() + world.get_door('PoD Sexy Statue NW', player).c_switch() + world.get_door('PoD Bow Statue SW', player).c_switch() + world.get_door('PoD Bow Statue Down Ladder', player).c_switch() + world.get_door('PoD Dark Pegs Up Ladder', player).c_switch() + world.get_door('PoD Dark Pegs WN', player).c_switch() + world.get_door('Swamp Crystal Switch EN', player).c_switch() world.get_door('Swamp Crystal Switch SE', player).c_switch() world.get_door('Swamp Shortcut Blue Barrier', player).barrier(CrystalBarrier.Blue) diff --git a/Rules.py b/Rules.py index 31a0cc49..be4dae43 100644 --- a/Rules.py +++ b/Rules.py @@ -1,8 +1,9 @@ import collections from collections import defaultdict import logging -from BaseClasses import CollectionState +from BaseClasses import CollectionState, DoorType from Dungeons import region_starts +from DoorShuffle import ExplorationState def set_rules(world, player): @@ -1695,72 +1696,79 @@ def generate_key_logic(start_region_names, small_key_name, world, player): logger = logging.getLogger('') # Now that the dungeon layout is done, we need to search again to generate key logic. # TODO: This assumes all start doors are accessible, which isn't always true. - # TODO: This can generate solvable-but-really-annoying layouts due to one ways. - available_doors = [] # Doors to explore - visited_regions = set() # Regions we've been to and don't need to expand + state = ExplorationState() + # available_doors = [] # Doors to explore + # visited_regions = set() # Regions we've been to and don't need to expand current_kr = 0 # Key regions are numbered, starting at 0 - door_krs = {} # Map of key door name to KR it lives in + # door_krs = {} # Map of key door name to KR it lives in kr_parents = {} # Key region to parent map - kr_location_counts = defaultdict(int) # Number of locations in each key region + # kr_location_counts = defaultdict(int) # Number of locations in each key region # Everything in a start region is in key region 0. for name in start_region_names: region = world.get_region(name, player) - visited_regions.add(name) - kr_location_counts[current_kr] += len(region.locations) - for door in get_doors(world, region, player): - if not door.blocked: - available_doors.append(door) - door_krs[door.name] = current_kr + state.visit_region(region, current_kr) + state.add_all_doors_check_key_region(region, current_kr, world, player) # Search into the dungeon logger.debug('Begin key region search. %s', small_key_name) - while len(available_doors) > 0: + while len(state.avail_doors) > 0: # Open as many non-key doors as possible before opening a key door. # This guarantees that we're only exploring one key region at a time. - available_doors.sort(key=lambda door: 0 if door.smallKey else 1) - door = available_doors.pop() - # Bail early if we've been here before or the door is blocked - local_kr = door_krs[door.name] + state.avail_doors.sort(key=state.key_door_sort) + explorable_door = state.next_avail_door() + door = explorable_door.door + local_kr = state.door_krs[door.name] logger.debug(' kr %s: Door %s', local_kr, door.name) - exit = world.get_entrance(door.name, player).connected_region - if door.blocked or exit.name in visited_regions: + connect_region = world.get_entrance(door.name, player).connected_region + # Bail early if we've been here before or the door is blocked + if not state.can_traverse(door) or state.visited(connect_region): continue # Once we open a key door, we need a new region. - if door.smallKey: + if door.smallKey and door not in state.opened_doors: # we tend to open doors in a DFS manner current_kr += 1 kr_parents[current_kr] = local_kr local_kr = current_kr + state.opened_doors.append(door) + if door.dest.smallKey: + state.opened_doors.append(door.dest) logger.debug(' New KR %s', current_kr) # Account for the new region - visited_regions.add(exit.name) - kr_location_counts[local_kr] += len(exit.locations) - for new_door in get_doors(world, exit, player): - available_doors.append(new_door) - door_krs[new_door.name] = local_kr + state.visit_region(connect_region, local_kr) + state.add_all_doors_check_key_region(connect_region, local_kr, world, player) + # kr_location_counts[local_kr] += len(exit.locations) # Now that we have doors divided up into key regions, we can analyze the map # Invert the door -> kr map into one that lists doors by region. kr_doors = defaultdict(list) region_krs = {} - for door_name in door_krs: - kr = door_krs[door_name] - exit = world.get_entrance(door_name, player); + for door_name in state.door_krs: + kr = state.door_krs[door_name] + entrance = world.get_entrance(door_name, player) door = world.check_for_door(door_name, player) - region_krs[exit.parent_region.name] = kr + ent_name = entrance.parent_region.name + if ent_name in region_krs.keys(): + region_krs[entrance.parent_region.name] = min(region_krs[entrance.parent_region.name], kr) + else: + region_krs[entrance.parent_region.name] = kr if door.smallKey and not door.blocked: - kr_doors[kr].append(exit) + kr_doors[kr].append(entrance) kr_keys = defaultdict(int) # Number of keys each region needs for kr in range(0, current_kr + 1): logic_doors = [] keys = 0 for door in kr_doors[kr]: dest_kr = region_krs[door.connected_region.name] - if dest_kr > kr: + if dest_kr > kr: # may be the case if dest_kr != parent_kr of kr # This door heads deeper into the dungeon. It needs a full key, and logic keys += 1 logic_doors.append(door) elif dest_kr == kr: # This door doesn't get us any deeper, but it's possible to waste a key. - # We're going to see its sibling in this search, so add half a key - keys += 0.5 + # If we're going to see its sibling in this search, add half a key + actual_door = world.get_door(door.name, player) + if actual_door.type == DoorType.SpiralStairs: + keys += 1 + else: + keys += 0.5 + logic_doors.append(door) # this may still need logic # Add key count from parent region if kr in kr_parents: keys += kr_keys[kr_parents[kr]]