diff --git a/BaseClasses.py b/BaseClasses.py index 70c3f0c0..f1088c04 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -963,6 +963,7 @@ class Sector(object): def __init__(self): self.regions = [] self.outstanding_doors = [] + self.name = None # todo: make these lazy init? - when do you invalidate them def polarity(self): diff --git a/DoorShuffle.py b/DoorShuffle.py index f84f4b9d..0acff55c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,7 +6,8 @@ import operator as op from functools import reduce from BaseClasses import RegionType, DoorType, Direction, Sector, pol_idx from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions -from Dungeons import dungeon_regions +from Dungeons import dungeon_regions, region_starts, split_region_starts +from Regions import key_only_locations from RoomData import DoorKind, PairedDoor @@ -391,58 +392,51 @@ def doors_fit_mandatory_pair(pair_list, a, b): def cross_dungeon(world, player): - hc = convert_to_sectors(dungeon_regions['Hyrule Castle'], world, player) - ep = convert_to_sectors(dungeon_regions['Eastern'], world, player) - dp = convert_to_sectors(dungeon_regions['Desert'], world, player) - th = convert_to_sectors(dungeon_regions['Hera'], world, player) - at = convert_to_sectors(dungeon_regions['Tower'], world, player) - pd = convert_to_sectors(dungeon_regions['PoD'], world, player) - dungeon_split = split_up_sectors(hc + ep + dp + th + at + pd, default_dungeon_sets) - dp_split = split_up_sectors(dungeon_split.pop(2), desert_default_entrance_sets) + all_sectors = [] + for key in dungeon_regions.keys(): + all_sectors.extend(convert_to_sectors(dungeon_regions[key], world, player)) + dungeon_split = split_up_sectors(all_sectors, default_dungeon_sets) dungeon_sectors = [] - # todo - adjust dungeon item pools for idx, sector_list in enumerate(dungeon_split): - dungeon_sectors.append((sector_list, entrance_sets[idx])) - for idx, sector_list in enumerate(dp_split): - dungeon_sectors.append((sector_list, desert_default_entrance_sets[idx])) + name = dungeon_x_idx_to_name[idx] + if name in split_region_starts.keys(): + split = split_up_sectors(sector_list, split_region_starts[name]) + for sub_idx, sub_sector_list in enumerate(split): + dungeon_sectors.append((name, sub_sector_list, split_region_starts[name][sub_idx])) + else: + dungeon_sectors.append((name, sector_list, region_starts[name])) + # todo - adjust dungeon item pools -- ? + dungeon_layouts = [] + for key, sector_list, entrance_list in dungeon_sectors: + ds = shuffle_dungeon_no_repeats(world, player, sector_list, entrance_list) + ds.name = key + dungeon_layouts.append((ds, entrance_list)) - for sector_list, entrance_list in dungeon_sectors: - shuffle_dungeon_no_repeats(world, player, sector_list, entrance_list) + combine_layouts(dungeon_layouts) + + for layout in dungeon_layouts: + shuffle_key_doors(layout[1], layout[2], world, player) def experiment(world, player): fix_big_key_doors_with_ugly_smalls(world, player) - hc = convert_to_sectors(dungeon_regions['Hyrule Castle'], world, player) - ep = convert_to_sectors(dungeon_regions['Eastern'], world, player) - dp = convert_to_sectors(dungeon_regions['Desert'], world, player) - th = convert_to_sectors(dungeon_regions['Hera'], world, player) - at = convert_to_sectors(dungeon_regions['Tower'], world, player) - pd = convert_to_sectors(dungeon_regions['PoD'], world, player) dungeon_sectors = [] - for idx, sector_list in enumerate([hc, ep, th, at, pd]): - dungeon_sectors.append((sector_list, entrance_sets[idx])) - dp_split = split_up_sectors(dp, desert_default_entrance_sets) - for idx, sector_list in enumerate(dp_split): - dungeon_sectors.append((sector_list, desert_default_entrance_sets[idx])) + for key in dungeon_regions.keys(): + sector_list = convert_to_sectors(dungeon_regions[key], world, player) + if key in split_region_starts.keys(): + split_sectors = split_up_sectors(sector_list, split_region_starts[key]) + for idx, sub_sector_list in enumerate(split_sectors): + dungeon_sectors.append((key, sub_sector_list, split_region_starts[key][idx])) + else: + dungeon_sectors.append((key, sector_list, region_starts[key])) dungeon_layouts = [] - for sector_list, entrance_list in dungeon_sectors: + for key, sector_list, entrance_list in dungeon_sectors: ds = shuffle_dungeon_no_repeats(world, player, sector_list, entrance_list) + ds.name = key dungeon_layouts.append((ds, entrance_list)) - desert_combined = None - desert_entrances = [] - queue = collections.deque(dungeon_layouts) - while len(queue) > 0: - sector, entrance_list = queue.pop() - if entrance_list in desert_default_entrance_sets: - dungeon_layouts.remove((sector, entrance_list)) - desert_entrances.extend(entrance_list) - if desert_combined is None: - desert_combined = sector - else: - desert_combined.regions.extend(sector.regions) - dungeon_layouts.append((desert_combined, desert_entrances)) + combine_layouts(dungeon_layouts) # shuffle_key_doors for dungeons for layout in dungeon_layouts: @@ -493,6 +487,23 @@ def convert_to_sectors(region_names, world, player): return sectors +# those with split region starts like Desert/Skull combine for key layouts +def combine_layouts(dungeon_layouts): + combined = {} + queue = collections.deque(dungeon_layouts) + while len(queue) > 0: + sector, entrance_list = queue.pop() + if sector.name in split_region_starts: + dungeon_layouts.remove((sector, entrance_list)) + # desert_entrances.extend(entrance_list) + if sector.name not in combined: + combined[sector.name] = sector + else: + combined[sector.name].regions.extend(sector.regions) + for key in combined.keys(): + dungeon_layouts.append((combined[key], region_starts[key])) + + def split_up_sectors(sector_list, entrance_sets): new_sector_grid = [] leftover_sectors = [] @@ -810,7 +821,7 @@ def are_there_outstanding_doors_of_type(door_a, door_b, sector_a, sector_b, avai def shuffle_key_doors(dungeon_sector, entrances, world, player): start_regions = convert_regions(entrances, world, player) - # count number of key doors + # count number of key doors - this could be a table? num_key_doors = 0 current_doors = [] skips = [] @@ -849,16 +860,19 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player): paired_candidates = build_pair_list(flat_candidates) if len(paired_candidates) < num_key_doors: num_key_doors = len(paired_candidates) # reduce number of key doors + logging.getLogger('').debug('Lowering key door count because not enough candidates: %s', dungeon_sector.name) random.shuffle(paired_candidates) combinations = ncr(len(paired_candidates), num_key_doors) itr = 0 proposal = kth_combination(itr, paired_candidates, num_key_doors) - while not validate_key_layout(start_regions, proposal, world, player): + while not validate_key_layout(dungeon_sector, start_regions, proposal, world, player): itr += 1 + if itr >= combinations: + logging.getLogger('').debug('Lowering key door count because no valid layouts: %s', dungeon_sector.name) + num_key_doors -= 1 + combinations = ncr(len(paired_candidates), num_key_doors) + itr = 0 proposal = kth_combination(itr, paired_candidates, num_key_doors) - if itr > combinations: - raise Exception('No valid key layouts!') - # make changes reassign_key_doors(current_doors, proposal, world, player) @@ -937,70 +951,130 @@ def kth_combination(k, l, r): def ncr(n, r): + if r == 0: + return 1 r = min(r, n-r) numerator = reduce(op.mul, range(n, n-r, -1), 1) denominator = reduce(op.mul, range(1, r+1), 1) return numerator / denominator -def validate_key_layout(start_regions, key_door_proposal, world, player): +class KeyDoorState(object): + + def __init__(self): + self.avail_doors = [] + self.small_doors = [] + self.big_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 + + 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 + + return ret + + +def validate_key_layout(sector, start_regions, key_door_proposal, world, player): flat_proposal = flatten_pair_list(key_door_proposal) - available_doors = [] # Doors to explore - big_key_doors = [] - small_key_doors = [] - big_key_opened = False - visited_regions = set() # Regions we've been to and don't need to expand - ttl_locations = 0 - used_locations = 0 + state = KeyDoorState() + 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: - visited_regions.add(region) - ttl_locations += len(region.locations) - add_doors_to_lists(region, flat_proposal, available_doors, small_key_doors, - big_key_doors, big_key_opened, world, player) - while len(available_doors) > 0: - door = available_doors.pop() + state.visited_regions.add(region) + state.ttl_locations += len(region.locations) + for location in region.locations: + if location.name in key_only_locations: + state.key_locations += 1 + add_doors_to_lists(region, flat_proposal, state, world, player) + return validate_key_layout_r(state, flat_proposal, world, player) + + +def validate_key_layout_r(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + door = state.avail_doors.pop() connect_region = world.get_entrance(door.name, player).connected_region - if not door.blocked and connect_region not in visited_regions: - visited_regions.add(connect_region) - ttl_locations += len(connect_region.locations) - add_doors_to_lists(connect_region, flat_proposal, available_doors, small_key_doors, - big_key_doors, big_key_opened, world, player) - if len(available_doors) == 0: + if not door.blocked and connect_region not in state.visited_regions: + state.visited_regions.add(connect_region) + state.ttl_locations += len([x for x in connect_region.locations if '- Prize' not in x.name]) + for location in connect_region.locations: + if location.name in key_only_locations: + state.key_locations += 1 + if connect_region.name == 'Hyrule Dungeon Cellblock': + state.big_key_opened = True + state.avail_doors.extend(state.big_doors) + state.big_doors.clear() + add_doors_to_lists(connect_region, flat_proposal, state, world, player) + if len(state.avail_doors) == 0: num_smalls = 0 - for small in small_key_doors: - if small.dest in small_key_doors: + for small in state.small_doors: + if small.dest in state.small_doors: num_smalls += 0.5 # half now, half with the dest else: num_smalls += 1 - num_bigs = 1 if len(big_key_doors) > 0 else 0 # all or nothing + num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing if num_smalls == 0 and num_bigs == 0: return True # I think that's the end - available_locations = ttl_locations - used_locations - if available_locations >= num_smalls > 0: # todo: this not lenient at all - need a recursive function maybe - available_doors.extend(small_key_doors) - small_key_doors.clear() - used_locations += num_smalls - elif not big_key_opened and available_locations >= num_bigs > 0: - big_key_opened = True - used_locations += 1 # todo: this does not handle hc big key in crossed modes - available_doors.extend(big_key_doors) - big_key_doors.clear() - else: + available_small_locations = min(state.ttl_locations - state.used_locations, state.key_locations - state.used_smalls) + available_big_locations = state.ttl_locations - state.used_locations if not state.big_key_special else 0 + valid = True + if (num_smalls == 0 or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): return False - return len(small_key_doors) == 0 and len(big_key_doors) == 0 + else: + if num_smalls > 0 and available_small_locations > 0: + for d in state.small_doors: + state_copy = state.copy() + state_copy.opened_doors.append(d) + state_copy.avail_doors.append(d) + state_copy.small_doors.remove(d) + if d.dest in flat_proposal: + state_copy.opened_doors.append(d.dest) + if d.dest in state_copy.small_doors: + state_copy.small_doors.remove(d.dest) + state_copy.avail_doors.append(d.dest) + state_copy.used_locations += 1 + state_copy.used_smalls += 1 + valid = validate_key_layout_r(state_copy, flat_proposal, world, player) + if not valid: + return valid + if not state.big_key_opened and available_big_locations >= num_bigs > 0: + state_copy = state.copy() + state_copy.big_key_opened = True + state_copy.used_locations += 1 + state_copy.avail_doors.extend(state.big_doors) + state_copy.big_doors.clear() + valid = validate_key_layout_r(state_copy, flat_proposal, world, player) + return valid + return len(state.small_doors) == 0 and len(state.big_doors) == 0 -def add_doors_to_lists(region, key_door_proposal, available_doors, small_key_doors, - big_key_doors, big_key_opened, world, player): +def add_doors_to_lists(region, key_door_proposal, kd_state, world, player): for door in get_doors(world, region, player): if not door.blocked: - if door in key_door_proposal and door not in small_key_doors: - small_key_doors.append(door) - elif door.bigKey and not big_key_opened and door not in big_key_doors: - big_key_doors.append(door) - elif door not in available_doors: - available_doors.append(door) + if door in key_door_proposal and door not in kd_state.small_doors and door not in kd_state.opened_doors: + kd_state.small_doors.append(door) + elif door.bigKey and not kd_state.big_key_opened and door not in kd_state.big_doors: + kd_state.big_doors.append(door) + elif door not in kd_state.avail_doors: + kd_state.avail_doors.append(door) def reassign_key_doors(current_doors, proposal, world, player): @@ -1302,24 +1376,12 @@ default_dungeon_sets = [ ['PoD Lobby', 'PoD Boss'] ] - -desert_default_entrance_sets = [ - ['Desert Back Lobby'], - ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'] -] - -# 'Skull': ['Skull 1 Lobby', 'Skull 2 Mummy Lobby', 'Skull 2 Key Lobby', 'Skull 3 Lobby'], - -entrance_sets = [ - ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Secret Room', 'Sanctuary'], - ['Eastern Lobby'], - ['Hera Lobby'], - ['Tower Lobby'], - ['PoD Lobby'], - # ['Swamp Lobby'], - # ['TT Lobby'], - # ['Ice Lobby'], - # ['Mire Lobby'], - # ['TR Main Lobby', 'TR Eye Trap', 'TR Big Chest', 'TR Laser Bridge'], - # ['GT Lobby'] -] +dungeon_x_idx_to_name = { + 0: 'Hyrule Castle', + 1: 'Eastern Palace', + 2: 'Desert Palace', + 3: 'Tower of Hera', + 4: 'Agahnims Tower', + 5: 'Palace of Darkness', +# etc +} diff --git a/Dungeons.py b/Dungeons.py index d575377e..4b743ed5 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -217,11 +217,11 @@ pod_regions = [ dungeon_regions = { 'Hyrule Castle': hyrule_castle_regions, - 'Eastern': eastern_regions, - 'Desert': desert_regions, - 'Hera': hera_regions, - 'Tower': tower_regions, - 'PoD': pod_regions, + 'Eastern Palace': eastern_regions, + 'Desert Palace': desert_regions, + 'Tower of Hera': hera_regions, + 'Agahnims Tower': tower_regions, + 'Palace of Darkness': pod_regions, # 'Swamp': # 'Skull': # 'TT': @@ -230,3 +230,28 @@ dungeon_regions = { # 'TR': # 'GT': } + +region_starts = { + 'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Secret Room', 'Sanctuary'], + 'Eastern Palace': ['Eastern Lobby'], + 'Desert Palace': ['Desert Back Lobby', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'], + 'Tower of Hera': ['Hera Lobby'], + 'Agahnims Tower': ['Tower Lobby'], + 'Palace of Darkness': ['PoD Lobby'], + # ['Swamp Lobby'], + # ['TT Lobby'], + # ['Ice Lobby'], + # ['Mire Lobby'], + # ['TR Main Lobby', 'TR Eye Trap', 'TR Big Chest', 'TR Laser Bridge'], + # ['GT Lobby'] +} + +split_region_starts = { + 'Desert Palace': [ + ['Desert Back Lobby'], + ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'] + ] + # 'Skull': ['Skull 1 Lobby', 'Skull 2 Mummy Lobby', 'Skull 2 Key Lobby', 'Skull 3 Lobby'], +} + + diff --git a/Main.py b/Main.py index fd5d4ab2..3d2342aa 100644 --- a/Main.py +++ b/Main.py @@ -89,7 +89,7 @@ def main(args, seed=None): # todo: remove this later. this is for debugging for player in range(1, world.players + 1): all_state = world.get_all_state(keys=True) - for bossregion in ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1']: + for bossregion in ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss']: if world.get_region(bossregion, player) not in all_state.reachable_regions[player]: raise Exception(bossregion + ' missing from generation') diff --git a/Regions.py b/Regions.py index 9a1abbe5..44e0e11f 100644 --- a/Regions.py +++ b/Regions.py @@ -293,7 +293,7 @@ def create_regions(world, player): create_dungeon_region(player, 'Sewers Rope Room', 'A dungeon', None, ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs']), create_dungeon_region(player, 'Sewers Dark Cross', 'A dungeon', ['Sewers - Dark Cross'], ['Sewers Dark Cross Key Door N', 'Sewers Dark Cross South Stairs']), create_dungeon_region(player, 'Sewers Water', 'A dungeon', None, ['Sewers Dark Cross Key Door S', 'Sewers Water W']), - create_dungeon_region(player, 'Sewers Key Rat', 'A dungeon', None, ['Sewers Key Rat E', 'Sewers Key Rat Key Door N']), + create_dungeon_region(player, 'Sewers Key Rat', 'A dungeon', ['Hyrule Castle - Key Rat Key Drop'], ['Sewers Key Rat E', 'Sewers Key Rat Key Door N']), create_dungeon_region(player, 'Sewers Secret Room Blocked Path', 'A dungeon', None, ['Sewers Secret Room Up Stairs']), create_dungeon_region(player, 'Sewers Secret Room', 'A dungeon', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right'], ['Sewers Secret Room Key Door S', 'Sewers Secret Room Push Block']), @@ -537,7 +537,7 @@ default_shop_contents = { key_only_locations = { 'Hyrule Castle - Map Guard Key Drop': 'Small Key (Escape)', 'Hyrule Castle - Boomerang Guard Key Drop': 'Small Key (Escape)', - # todo: escape big key? + 'Hyrule Castle - Key Rat Key Drop': 'Small Key (Escape)', 'Eastern Palace - Dark Square Pot Key': 'Small Key (Eastern Palace)', 'Eastern Palace - Dark Eyegore Key Drop': 'Small Key (Eastern Palace)', 'Desert Palace - Desert Tiles 1 Pot Key': 'Small Key (Desert Palace)', @@ -547,6 +547,8 @@ key_only_locations = { 'Castle Tower - Circle of Pots Key Drop': 'Small Key (Agahnims Tower)', } +# todo: escape big key? - should be separate from above for dungeon key layout validation + location_table = {'Mushroom': (0x180013, False, 'in the woods'), 'Bottle Merchant': (0x2eb18, False, 'with a merchant'), 'Flute Spot': (0x18014a, False, 'underground'), diff --git a/Rules.py b/Rules.py index f9aa4320..50192102 100644 --- a/Rules.py +++ b/Rules.py @@ -1,6 +1,8 @@ import collections +from collections import defaultdict import logging from BaseClasses import CollectionState +from Dungeons import region_starts def set_rules(world, player): @@ -258,20 +260,10 @@ def global_rules(world, player): # TODO: Do these need to flag off when door rando is off? # If these generate fine rules with vanilla shuffle - then no. - # Hyrule Castle: There are three keys and we don't know how we shuffled, so we - # need three keys to be accessible before you use any of these doors. - # TODO: Generate key rules in the shuffler. (But make sure this way works first.) - for door in ['Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S', - 'Sewers Dark Cross Key Door N', 'Sewers Dark Cross Key Door S', 'Hyrule Dungeon Armory Interior Key Door N', 'Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Map Room Key Door S', 'Hyrule Dungeon North Abyss Key Door N']: - set_rule(world.get_entrance(door, player), lambda state: state.has_key('Small Key (Escape)', player, 3)) - + # Escape/ Hyrule Castle + generate_key_logic(region_starts['Hyrule Castle'], 'Small Key (Escape)', world, player) + # Eastern Palace - # The stalfos room and eyegore with a key can be killed with pots. - # Eastern Palace has dark rooms. - for location in ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop']: - add_rule(world.get_location(location, player), lambda state: state.has('Lamp', player)) - for door in ['Eastern Darkness S', 'Eastern Darkness Up Stairs', 'Eastern Dark Square NW', 'Eastern Dark Square Key Door WN']: - add_rule(world.get_entrance(door, player), lambda state: state.has('Lamp', player)) # Eyegore room needs a bow set_rule(world.get_entrance('Eastern Eyegores NE', player), lambda state: state.can_shoot_arrows(player)) # Big key rules @@ -280,7 +272,7 @@ def global_rules(world, player): 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)) - # TODO: Key logic for eastern + generate_key_logic(region_starts['Eastern Palace'], 'Small Key (Eastern Palace)', world, player) # Boss rules. Same as below but no BK or arrow requirement. set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player)) @@ -294,7 +286,7 @@ def global_rules(world, 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)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player)) - # TODO: Key logic for desert + generate_key_logic(region_starts['Desert Palace'], 'Small Key (Desert Palace)', 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)) @@ -304,9 +296,10 @@ def global_rules(world, 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)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) - # TODO: Key logic for hera + generate_key_logic(region_starts['Tower of Hera'], 'Small Key (Tower of Hera)', world, player) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) + generate_key_logic(region_starts['Agahnims Tower'], 'Small Key (Agahnims Tower)', world, player) set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player)) @@ -320,7 +313,7 @@ def global_rules(world, player): set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player)) set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Prize', player)) - # TODO: Key logic for pod + generate_key_logic(region_starts['Palace of Darkness'], 'Small Key (Palace of Darkness)', world, player) # End of door rando rules. @@ -1674,3 +1667,94 @@ def set_inverted_bunny_rules(world, player): add_rule(location, get_rule_to_add(location.parent_region)) +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 + current_kr = 0 # Key regions are numbered, starting at 0 + 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 + # 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 + # Search into the dungeon + logger.debug('Begin key region search. %s', small_key_name) + while len(available_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] + 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: + continue + # Once we open a key door, we need a new region. + if door.smallKey: + current_kr += 1 + kr_parents[current_kr] = local_kr + local_kr = current_kr + 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 + # 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); + door = world.check_for_door(door_name, player) + region_krs[exit.parent_region.name] = kr + if door.smallKey and not door.blocked: + kr_doors[kr].append(exit) + 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: + # 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 + # Add key count from parent region + if kr in kr_parents: + keys += kr_keys[kr_parents[kr]] + kr_keys[kr] = keys + # Generate logic + for door in logic_doors: + logger.info(' %s in kr %s needs %s keys', door.name, kr, keys) + add_rule(world.get_entrance(door.name, player), create_key_rule(small_key_name, player, keys)) + + +def create_key_rule(small_key_name, player, keys): + return lambda state: state.has_key(small_key_name, player, keys) + + +def get_doors(world, region, player): + res = [] + for exit in region.exits: + door = world.check_for_door(exit.name, player) + if door is not None: + res.append(door) + return res