From a18f5c4d512b40546e34482850b7a3c8973b6c36 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Mar 2020 09:05:07 -0600 Subject: [PATCH 1/3] Key logic rework for placement rules --exhaustive per key_counter --contradictions between rules Mire Lobby chest fix --- DoorShuffle.py | 11 +- Doors.py | 4 + Dungeons.py | 10 +- Fill.py | 2 +- KeyDoorShuffle.py | 350 ++++++++++++++++++++++++++++++++++------------ Regions.py | 3 +- Rules.py | 10 +- 7 files changed, 290 insertions(+), 100 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 7916218d..a38d97be 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1050,8 +1050,9 @@ def log_key_logic(d_name, key_logic): logger.debug('*Rule for %s:', rule.door_reference) if rule.bk_conditional_set: logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set])) - logger.debug('**BK Blocked By Door (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk])) - logger.debug('**BK Elsewhere (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk])) + logger.debug('**BK Blocked (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk])) + if rule.needed_keys_w_bk: + logger.debug('**BK Available (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk])) def build_pair_list(flat_list): @@ -1579,10 +1580,12 @@ logical_connections = [ ('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'), ('Mire Lobby Gap', 'Mire Post-Gap'), ('Mire Post-Gap Gap', 'Mire Lobby'), - ('Mire Hub Upper Blue Barrier', 'Mire Hub Top'), + ('Mire Hub Upper Blue Barrier', 'Mire Hub Switch'), ('Mire Hub Lower Blue Barrier', 'Mire Hub Right'), ('Mire Hub Right Blue Barrier', 'Mire Hub'), - ('Mire Hub Top Blue Barrier', 'Mire Hub'), + ('Mire Hub Top Blue Barrier', 'Mire Hub Switch'), + ('Mire Hub Switch Blue Barrier N', 'Mire Hub Top'), + ('Mire Hub Switch Blue Barrier S', 'Mire Hub'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), diff --git a/Doors.py b/Doors.py index e8c36fd4..125865f8 100644 --- a/Doors.py +++ b/Doors.py @@ -722,6 +722,8 @@ def create_doors(world, player): create_door(player, 'Mire Hub Lower Blue Barrier', Lgcl), create_door(player, 'Mire Hub Right Blue Barrier', Lgcl), create_door(player, 'Mire Hub Top Blue Barrier', Lgcl), + create_door(player, 'Mire Hub Switch Blue Barrier N', Lgcl), + create_door(player, 'Mire Hub Switch Blue Barrier S', Lgcl), create_door(player, 'Mire Hub Right EN', Nrml).dir(Ea, 0xc2, Top, High).small_key().pos(0), create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2), create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6), @@ -1161,6 +1163,8 @@ def create_doors(world, player): world.get_door('Mire Hub Lower Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Right Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Top Blue Barrier', player).barrier(CrystalBarrier.Blue) + world.get_door('Mire Hub Switch Blue Barrier N', player).barrier(CrystalBarrier.Blue) + world.get_door('Mire Hub Switch Blue Barrier S', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spike Side Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spot Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Crystal Dead End Left Barrier', player).barrier(CrystalBarrier.Blue) diff --git a/Dungeons.py b/Dungeons.py index d7a408f4..380c9898 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -255,11 +255,11 @@ ice_regions = [ ] mire_regions = [ - 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Lone Shooter', - 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End', - 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room', - 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge', - 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', + 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch', + 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', + 'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', + 'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', + 'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees', diff --git a/Fill.py b/Fill.py index 69a774bb..e38d757d 100644 --- a/Fill.py +++ b/Fill.py @@ -234,7 +234,7 @@ def valid_key_placement(item, location, itempool, world): return True key_logic = world.key_logic[item.player][dungeon.name] unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) - return key_logic.check_placement(unplaced_keys) + return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) else: inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) or (item.bigkey and not world.bigkeyshuffle[item.player])) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4d82d257..e28a9fdc 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1,3 +1,4 @@ +import itertools import logging from collections import defaultdict, deque @@ -22,6 +23,7 @@ class KeyLayout(object): self.all_chest_locations = {} self.big_key_special = False self.all_locations = set() + self.item_locations = set() # bk special? # bk required? True if big chests or big doors exists @@ -31,6 +33,8 @@ class KeyLayout(object): self.flat_prop = flatten_pair_list(self.proposal) self.key_logic = KeyLogic(self.sector.name) self.max_chests = calc_max_chests(builder, self, world, player) + self.all_locations = set() + self.item_locations = set() class KeyLogic(object): @@ -48,10 +52,14 @@ class KeyLogic(object): self.placement_rules = [] self.outside_keys = 0 - def check_placement(self, unplaced_keys): + def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: if not rule.is_satisfiable(self.outside_keys, unplaced_keys): return False + if big_key_loc: + for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): + if rule_a.contradicts(rule_b, unplaced_keys, big_key_loc): + return False return True @@ -66,6 +74,7 @@ class DoorRules(object): # for a place with only 1 free location/key_only_location behind it ... no goals and locations self.allow_small = False self.small_location = None + self.opposite = None class PlacementRule(object): @@ -79,6 +88,35 @@ class PlacementRule(object): self.check_locations_w_bk = None self.check_locations_wo_bk = None + 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 + rule_blocked = big_key_loc in rule.bk_conditional_set if rule.bk_conditional_set else False + check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk + rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk + if check_locations is None or rule_locations is None: + return False + check_locations = check_locations - {big_key_loc} + rule_locations = rule_locations - {big_key_loc} + threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk + rule_threshold = rule.needed_keys_wo_bk if rule_blocked else rule.needed_keys_w_bk + common_locations = rule_locations & check_locations + shared = len(common_locations) + if min(rule_threshold, threshold) - shared > 0: + left = unplaced_keys - shared + check_locations = check_locations - common_locations + check_needed = threshold - shared + if len(check_locations) < check_needed or left < check_needed: + return True + else: + left -= check_needed + rule_locations = rule_locations - common_locations + rule_needed = rule_threshold - shared + if len(rule_locations) < rule_needed or left < rule_needed: + return True + else: + left -= rule_needed + return False + def is_satisfiable(self, outside_keys, unplaced_keys): bk_blocked = False if self.bk_conditional_set: @@ -86,9 +124,11 @@ class PlacementRule(object): if loc.item and loc.item.bigkey: bk_blocked = True break + check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk + if not bk_blocked and check_locations is None: + return True available_keys = outside_keys empty_chests = 0 - check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk for loc in check_locations: if not loc.item: @@ -134,9 +174,27 @@ def build_key_layout(builder, start_regions, proposal, world, player): key_layout.max_drops = count_key_drops(key_layout.sector) key_layout.max_chests = calc_max_chests(builder, key_layout, world, player) key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set() + key_layout.all_locations = find_all_locations(key_layout.sector) return key_layout +def count_key_drops(sector): + cnt = 0 + for region in sector.regions: + for loc in region.locations: + if loc.event and 'Small Key' in loc.item.name: + cnt += 1 + return cnt + + +def find_all_locations(sector): + all_locations = set() + for region in sector.regions: + for loc in region.locations: + all_locations.add(loc) + return all_locations + + def calc_max_chests(builder, key_layout, world, player): if world.doorShuffle[player] != 'crossed': return len(world.get_dungeon(key_layout.sector.name, player).small_keys) @@ -165,17 +223,13 @@ def analyze_dungeon(key_layout, world, player): raw_avail = chest_keys + len(key_counter.key_only_locations) available = raw_avail - key_counter.used_keys possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) - avail_bigs = exist_relevant_big_doors(key_counter, key_layout) + avail_bigs = exist_relevant_big_doors(key_counter, key_layout) or exist_big_chest(key_counter) non_big_locs = count_locations_big_optional(key_counter.free_locations) if not key_counter.big_key_opened: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) - if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations): - key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) - # todo: detect forced subsequent keys - see keypuzzles # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() - smallest_rule = None for child in key_counter.child_doors.keys(): if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened: odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) @@ -183,34 +237,20 @@ def analyze_dungeon(key_layout, world, player): child_queue.append((child, odd_counter, empty_flag)) if child in doors_completed and child in key_logic.door_rules.keys(): rule = key_logic.door_rules[child] - if smallest_rule is None or rule.small_key_num < smallest_rule: - smallest_rule = rule.small_key_num while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() if not child.bigKey and child not in doors_completed: best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) rule = create_rule(best_counter, key_counter, key_layout, world, player) - # todo: seems to be caused by best_counter not opening the big key door when that's logically required. Re-evaluate usage of this - # if not rule.is_valid: - # logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name) - if smallest_rule is None or rule.small_key_num < smallest_rule: - smallest_rule = rule.small_key_num check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) key_logic.door_rules[child.name] = rule - create_placement_rule(key_layout, child, odd_counter, key_counter, world, player) doors_completed.add(child) next_counter = find_next_counter(child, key_counter, key_layout) ctr_id = cid(next_counter, key_layout) if ctr_id not in visited_cid: queue.append((child, next_counter)) visited_cid.add(ctr_id) - possible_smalls_collected = len(key_counter.key_only_locations) + non_big_locs - if not key_counter.big_key_opened: - if smallest_rule is not None and smallest_rule >= possible_smalls_collected and not avail_bigs: - key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) - if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations): - key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) check_rules(original_key_counter, key_layout, world, player) # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location @@ -219,59 +259,52 @@ def analyze_dungeon(key_layout, world, player): max_counter = find_max_counter(key_layout) rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc) rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num + create_exhaustive_placement_rules(key_layout, world, player) + set_paired_rules(key_logic, world, player) -def create_placement_rule(key_layout, door, odd_ctr, current_ctr, world, player): +def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic - worst_ctr = find_worst_counter(door, odd_ctr, current_ctr, key_layout, False) - sm_num = worst_ctr.used_keys + 1 - accessible_loc = set() - accessible_loc.update(worst_ctr.free_locations) - accessible_loc.update(worst_ctr.key_only_locations) - worst_ctr_wo_bk, post_ctr, alt_num = find_worst_counter_wo_bk(sm_num, accessible_loc, door, odd_ctr, current_ctr, key_layout) - blocked_loc = key_layout.all_locations.difference(accessible_loc) - - if len(blocked_loc) > 0: - rule = PlacementRule() - rule.door_reference = door - rule.small_key = key_logic.small_key_name - rule.needed_keys_w_bk = sm_num - placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player) - rule.check_locations_w_bk = accessible_loc - if worst_ctr_wo_bk: - accessible_wo_bk, post_set = set(), set() - accessible_wo_bk.update(worst_ctr_wo_bk.free_locations) - accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations) - post_set.update(post_ctr.free_locations) - post_set.update(post_ctr.key_only_locations) - blocked_wo_bk = post_set.difference(accessible_wo_bk) - if len(blocked_wo_bk) > 0: - rule.bk_conditional_set = blocked_wo_bk - rule.needed_keys_wo_bk = alt_num - # can this self lock a key if bk not avail? I'm thinking no. - # placement_self_lock_adjustment(rule, key_layout, ???, worst_ctr_wo_bk, world, player) - rule.check_locations_wo_bk = accessible_wo_bk - key_logic.placement_rules.append(rule) - if worst_ctr_wo_bk: - check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num) + max_ctr = find_max_counter(key_layout) + for code, key_counter in key_layout.key_counters.items(): + accessible_loc = set() + accessible_loc.update(key_counter.free_locations) + accessible_loc.update(key_counter.key_only_locations) + blocked_loc = key_layout.item_locations.difference(accessible_loc) + valid_rule = True + # min_keys = max(count_unique_sm_doors(key_counter.child_doors), key_counter.used_keys + 1) + min_keys = key_counter.used_keys + 1 + if len(blocked_loc) > 0 and len(key_counter.key_only_locations) < min_keys: + rule = PlacementRule() + rule.door_reference = code + rule.small_key = key_logic.small_key_name + if key_counter.big_key_opened or not big_key_progress(key_counter): + # rule.needed_keys_w_bk = key_counter.used_keys + 1 + rule.needed_keys_w_bk = min_keys + if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc): + valid_rule = False # indicates that the big key cannot be in the accessible locations + key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations)) + else: + placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) + rule.check_locations_w_bk = accessible_loc + check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + else: + if big_key_progress(key_counter) and only_sm_doors(key_counter): + create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player) + # if key_counter.used_keys + 2 > len(accessible_loc) and big_key_no_progress(key_counter): + # key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations)) + rule.bk_conditional_set = blocked_loc + # rule.needed_keys_wo_bk = key_counter.used_keys + 1 + rule.needed_keys_wo_bk = min_keys + rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc)) + if valid_rule: + key_logic.placement_rules.append(rule) + refine_placement_rules(key_layout, max_ctr) -def check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num): - avail_keys = len(worst_ctr_wo_bk.key_only_locations) - place_able_keys = min(key_layout.max_chests, len(worst_ctr_wo_bk.free_locations)) - if avail_keys + place_able_keys < alt_num: - accessible_wo_bk, post_set = set(), set() - accessible_wo_bk.update(worst_ctr_wo_bk.free_locations) - accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations) - post_set.update(post_ctr.free_locations) - post_set.update(post_ctr.key_only_locations) - key_layout.key_logic.bk_restricted.update(post_set.difference(accessible_wo_bk)) - - -def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player): +def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player): if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': - max_ctr = find_max_counter(key_layout) - blocked_others = set(max_ctr.other_locations).difference(set(worst_ctr.other_locations)) + blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations)) important_found = False for loc in blocked_others: if important_location(loc, world, player): @@ -281,13 +314,115 @@ def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, wor rule.needed_keys_w_bk -= 1 -def count_key_drops(sector): - cnt = 0 - for region in sector.regions: - for loc in region.locations: - if loc.event and 'Small Key' in loc.item.name: - cnt += 1 - return cnt +def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked): + if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations): + key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations)) + return True + return False + + +def refine_placement_rules(key_layout, max_ctr): + key_logic = key_layout.key_logic + changed = True + while changed: + changed = False + rules_to_remove = [] + for rule in key_logic.placement_rules: + if rule.check_locations_w_bk: + rule.check_locations_w_bk.difference_update(key_logic.sm_restricted) + key_onlys = rule.check_locations_w_bk.intersection(max_ctr.key_only_locations) + if len(key_onlys) > 0: + rule.check_locations_w_bk.difference_update(key_onlys) + rule.needed_keys_w_bk -= len(key_onlys) + if rule.needed_keys_w_bk == 0: + rules_to_remove.append(rule) + if len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1: + new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk + if len(new_restricted - key_logic.bk_restricted) > 0: + key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations + changed = True + if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk: + logging.getLogger('').warning('Invalid rule - what went wrong here??') + rules_to_remove.append(rule) + changed = True + if rule.bk_conditional_set is not None: + rule.bk_conditional_set.difference_update(key_logic.bk_restricted) + rule.bk_conditional_set.difference_update(max_ctr.key_only_locations) + if len(rule.bk_conditional_set) == 0: + rules_to_remove.append(rule) + if rule.check_locations_wo_bk: + rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted) + key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations) + if len(key_onlys) > 0: + rule.check_locations_wo_bk.difference_update(key_onlys) + rule.needed_keys_wo_bk -= len(key_onlys) + if rule.needed_keys_wo_bk == 0: + rules_to_remove.append(rule) + if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests: + if len(rule.bk_conditional_set) > 0: + key_logic.bk_restricted.update(rule.bk_conditional_set) + rules_to_remove.append(rule) + changed = True # impossible for bk to be here, I think + for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2): + if rule_b.bk_conditional_set and rule_a.check_locations_w_bk: + temp = rule_a + rule_a = rule_b + rule_b = temp + if rule_a.bk_conditional_set and rule_b.check_locations_w_bk: + common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk) + if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed: + key_logic.bk_restricted.update(rule_a.bk_conditional_set) + rules_to_remove.append(rule_a) + changed = True + break + equivalent_rules = [] + for rule in key_logic.placement_rules: + for rule2 in key_logic.placement_rules: + if rule != rule2: + if rule.check_locations_w_bk and rule2.check_locations_w_bk: + if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk: + rules_to_remove.append(rule) + elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk: + rules_to_remove.append(rule) + elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk: + equivalent_rules.append((rule, rule2)) + if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set: + if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk: + rules_to_remove.append(rule) + elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk: + rules_to_remove.append(rule) + elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk: + equivalent_rules.append((rule, rule2)) + if len(rules_to_remove) > 0: + key_logic.placement_rules = [x for x in key_logic.placement_rules if x not in rules_to_remove] + equivalent_rules = [x for x in equivalent_rules if x[0] not in rules_to_remove and x[1] not in rules_to_remove] + if len(equivalent_rules) > 0: + removed_rules = {} + for r1, r2 in equivalent_rules: + if r1 in removed_rules.keys(): + r1 = removed_rules[r1] + if r2 in removed_rules.keys(): + r2 = removed_rules[r2] + if r1 != r2: + r1.door_reference += ','+r2.door_reference + key_logic.placement_rules.remove(r2) + removed_rules[r2] = r1 + + +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() + rule.door_reference = code + rule.small_key = key_logic.small_key_name + rule.needed_keys_w_bk = min_keys + if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc): + # indicates that the big key cannot be in the accessible locations + key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations)) + else: + placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) + rule.check_locations_w_bk = accessible_loc + check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + key_logic.placement_rules.append(rule) def queue_sorter(queue_item): @@ -314,8 +449,8 @@ def find_bk_locked_sections(key_layout, world, player): big_chest_allowed_big_key = world.accessibility[player] != 'locations' for counter in key_counters.values(): key_layout.all_chest_locations.update(counter.free_locations) - key_layout.all_locations.update(counter.free_locations) - key_layout.all_locations.update(counter.key_only_locations) + key_layout.item_locations.update(counter.free_locations) + key_layout.item_locations.update(counter.key_only_locations) if counter.big_key_opened and counter.important_location: big_chest_allowed_big_key = False if not counter.big_key_opened: @@ -710,6 +845,16 @@ def count_unique_sm_doors(doors): return len(unique_d_set) +def big_key_progress(key_counter): + return not only_sm_doors(key_counter) or exist_big_chest(key_counter) + + +def only_sm_doors(key_counter): + for door in key_counter.child_doors: + if door.bigKey: + return False + return True + # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 @@ -738,6 +883,13 @@ def exist_relevant_big_doors(key_counter, key_layout): return False +def exist_big_chest(key_counter): + for loc in key_counter.free_locations: + if '- Big Chest' in loc.name: + return True + return False + + def count_locations_big_optional(locations, bk=False): cnt = 0 for loc in locations: @@ -989,6 +1141,13 @@ def reduce_rules(small_rules, collected, collected_alt): rule.small_key_num = collected +def set_paired_rules(key_logic, world, player): + for d_name, rule in key_logic.door_rules.items(): + door = world.get_door(d_name, player) + if door.dest.name in key_logic.door_rules.keys(): + rule.opposite = key_logic.door_rules[door.dest.name] + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1015,7 +1174,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa ttl_key_only = count_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) - if invalid_self_locking_key(state, prev_state, prev_avail, world, player): + if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False # todo: allow more key shuffles - refine placement rules # if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): @@ -1054,18 +1213,24 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return True -def invalid_self_locking_key(state, prev_state, prev_avail, world, player): +def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): if prev_state is None or state.used_smalls == prev_state.used_smalls: return False - new_locations = set(state.found_locations).difference(set(prev_state.found_locations)) + new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors)) + state_copy = state.copy() + while len(new_bk_doors) > 0: + for door in new_bk_doors: + open_a_door(door.door, state_copy, key_layout.flat_prop) + new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors)) + expand_key_state(state_copy, key_layout.flat_prop, world, player) + new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations)) important_found = False for loc in new_locations: important_found |= important_location(loc, world, player) if not important_found: return False new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors)) - new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors)) - if len(new_small_doors) > 0 or len(new_bk_doors) > 0: + if len(new_small_doors) > 0: return False return prev_avail - 1 == 0 @@ -1151,11 +1316,21 @@ def create_key_counter(state, key_layout, world, player): return key_counter -def important_location(loc, world, player): - important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden'] +imp_locations = None + + +def imp_locations_factory(world, player): + global imp_locations + 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': - important_locations.append('Hyrule Dungeon Cellblock') - return '- Prize' in loc.name or loc.name in important_locations + imp_locations.append('Hyrule Dungeon Cellblock') + return imp_locations + + +def important_location(loc, world, player): + return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) def create_odd_key_counter(door, parent_counter, key_layout, world, player): @@ -1428,7 +1603,7 @@ def validate_key_placement(key_layout, world, player): max_counter = find_max_counter(key_layout) big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations) - for counter in key_layout.key_counters.values(): + for code, counter in key_layout.key_counters.items(): if len(counter.child_doors) == 0: continue big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside @@ -1445,6 +1620,7 @@ def validate_key_placement(key_layout, world, player): # missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations? if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0): logging.getLogger('').error("Keylock - can't open locations: ") + logging.getLogger('').error("code: " + code) for i in missing_locations: logging.getLogger('').error(i) return False diff --git a/Regions.py b/Regions.py index 5a0bf298..a995086c 100644 --- a/Regions.py +++ b/Regions.py @@ -540,7 +540,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Mire 2', 'Misery Mire', None, ['Mire 2 Up Stairs', 'Mire 2 NE']), create_dungeon_region(player, 'Mire Hub', 'Misery Mire', None, ['Mire Hub SE', 'Mire Hub ES', 'Mire Hub E', 'Mire Hub NE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier']), create_dungeon_region(player, 'Mire Hub Right', 'Misery Mire', None, ['Mire Hub Right EN', 'Mire Hub Right Blue Barrier']), - create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']), + create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', None, ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']), + create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']), create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']), create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']), create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']), diff --git a/Rules.py b/Rules.py index 9cc8102d..4dce2231 100644 --- a/Rules.py +++ b/Rules.py @@ -342,6 +342,8 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player)) set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player)) set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player)) + set_rule(world.get_entrance('Mire Hub Switch Blue Barrier N', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player)) + set_rule(world.get_entrance('Mire Hub Switch Blue Barrier S', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player)) set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player)) set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player)) set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player)) @@ -1516,7 +1518,8 @@ bunny_impassible_doors = { 'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks', 'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap', 'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier', - 'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Falling Bridge WN', + 'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N', + 'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN', 'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Hidden Shooters Block Path S', 'Mire Hidden Shooters Block Path N', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', @@ -1546,7 +1549,10 @@ def add_key_logic_rules(world, player): key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): for door_name, keys in d_logic.door_rules.items(): - add_rule(world.get_entrance(door_name, player), create_advanced_key_rule(d_logic, player, keys)) + spot = world.get_entrance(door_name, player) + add_rule(spot, create_advanced_key_rule(d_logic, player, keys)) + if keys.opposite: + add_rule(spot, create_advanced_key_rule(d_logic, player, keys.opposite), 'or') for location in d_logic.bk_restricted: if location.name not in key_only_locations.keys(): forbid_item(location, d_logic.bk_name, player) From de2a3bf5c1c7c18eb9ecba190e81f861199d6be2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Mar 2020 12:24:36 -0600 Subject: [PATCH 2/3] Version change and note for later --- DungeonGenerator.py | 2 +- Main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2a1ee11c..a57e9f85 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -734,7 +734,7 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 - if location not in self.found_locations: + if location not in self.found_locations: # todo: special logic for TT Boss? self.found_locations.append(location) if not bk_Flag: self.bk_found.add(location) diff --git a/Main.py b/Main.py index 74920824..8c562078 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.0.18.6d' +__version__ = '0.0.19dev' def main(args, seed=None): From 9a65e458139f330cfe598a2f7df21f2945650ae7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Mar 2020 13:08:38 -0600 Subject: [PATCH 3/3] Refinement on BK restriction logic --- KeyDoorShuffle.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index e28a9fdc..74ef30f8 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -87,6 +87,7 @@ class PlacementRule(object): self.needed_keys_wo_bk = None self.check_locations_w_bk = None self.check_locations_wo_bk = None + self.bk_relevant = True 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 @@ -279,8 +280,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): rule.door_reference = code rule.small_key = key_logic.small_key_name if key_counter.big_key_opened or not big_key_progress(key_counter): - # rule.needed_keys_w_bk = key_counter.used_keys + 1 rule.needed_keys_w_bk = min_keys + rule.bk_relevant = key_counter.big_key_opened if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc): valid_rule = False # indicates that the big key cannot be in the accessible locations key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations)) @@ -291,10 +292,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): else: if big_key_progress(key_counter) and only_sm_doors(key_counter): create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player) - # if key_counter.used_keys + 2 > len(accessible_loc) and big_key_no_progress(key_counter): - # key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations)) rule.bk_conditional_set = blocked_loc - # rule.needed_keys_wo_bk = key_counter.used_keys + 1 rule.needed_keys_wo_bk = min_keys rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc)) if valid_rule: @@ -336,15 +334,15 @@ def refine_placement_rules(key_layout, max_ctr): rule.needed_keys_w_bk -= len(key_onlys) if rule.needed_keys_w_bk == 0: rules_to_remove.append(rule) - if len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1: + if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1: new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk if len(new_restricted - key_logic.bk_restricted) > 0: key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations changed = True - if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk: - logging.getLogger('').warning('Invalid rule - what went wrong here??') - rules_to_remove.append(rule) - changed = True + if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk: + logging.getLogger('').warning('Invalid rule - what went wrong here??') + rules_to_remove.append(rule) + changed = True if rule.bk_conditional_set is not None: rule.bk_conditional_set.difference_update(key_logic.bk_restricted) rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)