diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5a51fa3a..bd2967a3 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -79,14 +79,24 @@ def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, dungeon_cache = {} backtrack = False itr = 0 + attempt = 1 finished = False # flag if standard and this is hyrule castle std_flag = world.mode[player] == 'standard' and bk_special while not finished: # what are my choices? itr += 1 - if itr > 5000: - raise Exception('Generation taking too long. Ref %s' % name) + if itr > 1000: + if attempt > 9: + raise Exception('Generation taking too long. Ref %s' % name) + proposed_map = {} + choices_master = [[]] + depth = 0 + dungeon_cache = {} + backtrack = False + itr = 0 + attempt += 1 + logger.debug(f'Starting new attempt {attempt}') if depth not in dungeon_cache.keys(): dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) @@ -158,6 +168,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va dungeon = {} start = ExplorationState(dungeon=name) start.big_key_special = bk_special + group_flags, door_map = find_bk_groups(name, available_sectors, proposed_map, bk_special) def exception(d): return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' @@ -176,16 +187,17 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va for door in sector.outstanding_doors: if not door.stonewall and door not in proposed_map.keys(): hanger_set.add(door) + bk_flag = group_flags[door_map[door]] parent = door.entrance.parent_region crystal_start = CrystalBarrier.Either if parent.crystal_switch else init_crystal init_state = ExplorationState(crystal_start, dungeon=name) init_state.big_key_special = start.big_key_special o_state = extend_reachable_state_improved([parent], init_state, proposed_map, - valid_doors, False, world, player, exception) + valid_doors, bk_flag, world, player, exception) o_state_cache[door.name] = o_state piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception) dungeon[door.name] = piece - check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception) + check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, group_flags, door_map, world, player, exception) # catalog hooks: Dict> # and hangers: Dict> @@ -205,7 +217,43 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va return dungeon, hangers, avail_hooks -def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception): +def find_bk_groups(name, available_sectors, proposed_map, bk_special): + groups = {} + door_ids = {} + gid = 1 + for sector in available_sectors: + if bk_special: + my_gid = None + for door in sector.outstanding_doors: + if door in proposed_map and proposed_map[door] in door_ids: + if my_gid: + merge_gid = door_ids[proposed_map[door]] + for door in door_ids.keys(): + if door_ids[door] == merge_gid: + door_ids[door] = my_gid + groups[my_gid] = groups[my_gid] or groups[merge_gid] + else: + my_gid = door_ids[proposed_map[door]] + if not my_gid: + my_gid = gid + gid += 1 + for door in sector.outstanding_doors: + door_ids[door] = my_gid + if my_gid not in groups.keys(): + groups[my_gid] = False + for region in sector.regions: + for loc in region.locations: + if loc.forced_item and loc.item.bigkey and name in loc.item.name: + groups[my_gid] = True + else: + for door in sector.outstanding_doors: + door_ids[door] = gid + groups[gid] = False + return groups, door_ids + + +def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, group_flags, door_map, + world, player, exception): not_blue = set() not_blue.update(hanger_set) doors_to_check = set() @@ -233,17 +281,18 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do hang_type = hanger_from_door(door) # am I hangable on a hook? hook_type = hook_from_door(door) # am I hookable onto a hanger? if (hang_type in blue_hooks and not door.stonewall) or hook_type in blue_hangers: - explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors, + bk_flag = group_flags[door_map[door]] + explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors, bk_flag, world, player, exception) doors_to_check.add(door) not_blue.difference_update(doors_to_check) -def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player, exception): +def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, bk_flag, world, player, exception): parent = door.entrance.parent_region blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon) blue_start.big_key_special = o_state.big_key_special - b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, + b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, bk_flag, world, player, exception) dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception) @@ -915,7 +964,7 @@ 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, exception): +def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, bk_flag, world, player, exception): local_state = state.copy() for region in search_regions: local_state.visit_region(region) @@ -923,7 +972,7 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() if explorable_door.door.bigKey: - if isOrigin: + if bk_flag: big_not_found = not special_big_key_found(local_state, world, player) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 if big_not_found: continue # we can't open this door diff --git a/Fill.py b/Fill.py index 84a6cf6d..fd395736 100644 --- a/Fill.py +++ b/Fill.py @@ -226,7 +226,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = def valid_key_placement(item, location, itempool, world): - if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player]: + if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic': return True dungeon = location.parent_region.dungeon if dungeon: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 74ef30f8..fbf41197 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -41,7 +41,8 @@ class KeyLogic(object): def __init__(self, dungeon_name): self.door_rules = {} - self.bk_restricted = set() + self.bk_restricted = set() # subset of free locations + self.bk_locked = set() # includes potentially other locations and key only locations self.sm_restricted = set() self.small_key_name = dungeon_keys[dungeon_name] self.bk_name = dungeon_bigs[dungeon_name] @@ -50,7 +51,9 @@ class KeyLogic(object): self.logic_min = {} self.logic_max = {} self.placement_rules = [] + self.location_rules = {} self.outside_keys = 0 + self.dungeon = dungeon_name def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: @@ -77,6 +80,18 @@ class DoorRules(object): self.opposite = None +class LocationRule(object): + def __init__(self): + self.small_key_num = 0 + self.conditional_sets = [] + + +class ConditionalLocationRule(object): + def __init__(self, conditional_set): + self.conditional_set = conditional_set + self.small_key_num = 0 + + class PlacementRule(object): def __init__(self): @@ -88,6 +103,7 @@ class PlacementRule(object): self.check_locations_w_bk = None self.check_locations_wo_bk = None self.bk_relevant = True + self.key_reduced = False def contradicts(self, rule, unplaced_keys, big_key_loc): bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False @@ -208,6 +224,7 @@ def analyze_dungeon(key_layout, world, player): find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) + key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations)) if world.retro[player] and world.mode[player] != 'standard': return @@ -297,7 +314,9 @@ def create_exhaustive_placement_rules(key_layout, world, player): rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc)) if valid_rule: key_logic.placement_rules.append(rule) + adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) refine_placement_rules(key_layout, max_ctr) + refine_location_rules(key_layout) def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player): @@ -319,6 +338,37 @@ def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked): return False +def adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr): + if rule.bk_conditional_set: + test_set = (rule.bk_conditional_set - key_logic.bk_locked) - set(max_ctr.key_only_locations.keys()) + needed = rule.needed_keys_wo_bk if test_set else 0 + else: + test_set = None + needed = rule.needed_keys_w_bk + if needed > 0: + accessible_loc.update(key_counter.other_locations) + blocked_loc = key_layout.all_locations-accessible_loc + for location in blocked_loc: + if location not in key_logic.location_rules.keys(): + loc_rule = LocationRule() + key_logic.location_rules[location] = loc_rule + else: + loc_rule = key_logic.location_rules[location] + if test_set: + if location not in key_logic.bk_locked: + cond_rule = None + for other in loc_rule.conditional_sets: + if other.conditional_set == test_set: + cond_rule = other + break + if not cond_rule: + cond_rule = ConditionalLocationRule(test_set) + loc_rule.conditional_sets.append(cond_rule) + cond_rule.small_key_num = max(needed, cond_rule.small_key_num) + else: + loc_rule.small_key_num = max(needed, loc_rule.small_key_num) + + def refine_placement_rules(key_layout, max_ctr): key_logic = key_layout.key_logic changed = True @@ -407,6 +457,20 @@ def refine_placement_rules(key_layout, max_ctr): removed_rules[r2] = r1 +def refine_location_rules(key_layout): + locs_to_remove = [] + for loc, rule in key_layout.key_logic.location_rules.items(): + conditions_to_remove = [] + for cond_rule in rule.conditional_sets: + if cond_rule.small_key_num <= rule.small_key_num: + conditions_to_remove.append(cond_rule) + rule.conditional_sets = [x for x in rule.conditional_sets if x not in conditions_to_remove] + if rule.small_key_num == 0 and len(rule.conditional_sets) == 0: + locs_to_remove.append(loc) + for loc in locs_to_remove: + del key_layout.key_logic.location_rules[loc] + + def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player): key_logic = key_layout.key_logic rule = PlacementRule() @@ -421,6 +485,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a rule.check_locations_w_bk = accessible_loc check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) key_logic.placement_rules.append(rule) + adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) def queue_sorter(queue_item): @@ -438,12 +503,10 @@ def queue_sorter_2(queue_item): def find_bk_locked_sections(key_layout, world, player): - if key_layout.big_key_special: - return key_counters = key_layout.key_counters key_logic = key_layout.key_logic - bk_key_not_required = set() + bk_not_required = set() big_chest_allowed_big_key = world.accessibility[player] != 'locations' for counter in key_counters.values(): key_layout.all_chest_locations.update(counter.free_locations) @@ -452,10 +515,19 @@ def find_bk_locked_sections(key_layout, world, player): if counter.big_key_opened and counter.important_location: big_chest_allowed_big_key = False if not counter.big_key_opened: - bk_key_not_required.update(counter.free_locations) - key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_key_not_required))) + bk_not_required.update(counter.free_locations) + bk_not_required.update(counter.key_only_locations) + bk_not_required.update(counter.other_locations) + # todo?: handle bk special differently in cross dungeon + # notably: things behind bk doors - relying on the bk door logic atm + if not key_layout.big_key_special: + key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_not_required))) + key_logic.bk_locked.update(dict.fromkeys(set(key_layout.all_locations) - bk_not_required)) if not big_chest_allowed_big_key: - key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations)) + bk_required_locations = find_big_chest_locations(key_layout.all_chest_locations) + bk_required_locations += find_big_key_locked_locations(key_layout.all_chest_locations) + key_logic.bk_restricted.update(bk_required_locations) + key_logic.bk_locked.update(bk_required_locations) def empty_counter(counter): @@ -936,6 +1008,14 @@ def find_big_chest_locations(locations): return ret +def find_big_key_locked_locations(locations): + ret = [] + for loc in locations: + if loc.name in ["Thieves' Town - Blind's Cell", "Hyrule Castle - Zelda's Chest"]: + 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() @@ -1149,7 +1229,7 @@ def set_paired_rules(key_logic, world, player): # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode - if world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle'): + if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic': return True flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) @@ -1294,8 +1374,10 @@ def create_key_counter(state, key_layout, world, player): if important_location(loc, world, player): key_counter.important_location = True key_counter.other_locations[loc] = None - elif loc.event and 'Small Key' in loc.item.name: + elif loc.forced_item and loc.item.name == key_layout.key_logic.small_key_name: key_counter.key_only_locations[loc] = None + elif loc.forced_item and loc.item.name == key_layout.key_logic.bk_name: + key_counter.other_locations[loc] = None elif loc.name not in dungeon_events: key_counter.free_locations[loc] = None else: @@ -1306,11 +1388,6 @@ def create_key_counter(state, key_layout, world, player): key_counter.big_key_opened = state.visited(world.get_region('Hyrule Dungeon Cellblock', player)) else: key_counter.big_key_opened = state.big_key_opened - # if soft_lock_check: - # avail_chests = available_chest_small_keys(key_counter, key_counter.big_key_opened, world) - # avail_keys = avail_chests + len(key_counter.key_only_locations) - # if avail_keys <= key_counter.used_keys and avail_keys < key_layout.max_chests + key_layout.max_drops: - # raise SoftLockException() return key_counter @@ -1322,13 +1399,14 @@ def imp_locations_factory(world, player): if imp_locations: return imp_locations imp_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden'] - if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed': - imp_locations.append('Hyrule Dungeon Cellblock') + if world.mode[player] == 'standard': + imp_locations.append('Zelda Pickup') + imp_locations.append('Zelda Dropoff') return imp_locations def important_location(loc, world, player): - return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) + return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) or (loc.forced_item is not None and loc.item.bigkey) def create_odd_key_counter(door, parent_counter, key_layout, world, player): diff --git a/Main.py b/Main.py index 5e181f82..c37c1547 100644 --- a/Main.py +++ b/Main.py @@ -24,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names -__version__ = '0.1.0.1-u' +__version__ = '0.1.0.2-u' class EnemizerError(RuntimeError): pass @@ -145,17 +145,18 @@ def main(args, seed=None, fish=None): fill_dungeons(world) for player in range(1, world.players+1): - for key_layout in world.key_layout[player].values(): - if not validate_key_placement(key_layout, world, player): - raise RuntimeError( - "%s: %s (%s %d)" % - ( - world.fish.translate("cli","cli","keylock.detected"), - key_layout.sector.name, - world.fish.translate("cli","cli","player"), - player - ) - ) + if world.logic[player] != 'nologic': + for key_layout in world.key_layout[player].values(): + if not validate_key_placement(key_layout, world, player): + raise RuntimeError( + "%s: %s (%s %d)" % + ( + world.fish.translate("cli", "cli", "keylock.detected"), + key_layout.sector.name, + world.fish.translate("cli", "cli", "player"), + player + ) + ) logger.info(world.fish.translate("cli","cli","fill.world")) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 347f98db..8e4f75c2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,7 +10,9 @@ * Crossed Dungeon generation improvements * Fix for Animated Tiles in crossed dungeon * Stonewall hardlock no longer reachable from certain drops (Sewer Drop, some Skull Woods drops) that were previously possible +* No logic uses less key door logic ##### In Progress -* ~~TT Attic Hint tile should have a crystal switch accessible now~~ \ No newline at end of file +* TT Attic Hint tile should have a crystal switch accessible now +* Different key logic rules in development \ No newline at end of file diff --git a/Regions.py b/Regions.py index cbe722de..98e936e2 100644 --- a/Regions.py +++ b/Regions.py @@ -177,7 +177,7 @@ def create_regions(world, player): create_cave_region(player, 'Dark Desert Hint', 'a storyteller'), create_dw_region(player, 'Dark Death Mountain (West Bottom)', None, ['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']), create_dw_region(player, 'Dark Death Mountain (Top)', None, ['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower', 'Superbunny Cave (Top)', - 'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']), + 'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']), create_dw_region(player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)', 'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']), create_dw_region(player, 'Dark Death Mountain Isolated Ledge', None, ['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']), create_dw_region(player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Fairy Ascension Mirror Spot']),