diff --git a/DoorShuffle.py b/DoorShuffle.py index 7568bba3..658b5e6b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -158,6 +158,7 @@ def vanilla_key_logic(world, player): world.key_logic[player] = {} analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic + log_key_logic(builder.name, key_layout.key_logic) last_key = None if world.shuffle[player] == 'vanilla': validate_vanilla_key_logic(world, player) @@ -1013,6 +1014,13 @@ def log_key_logic(d_name, key_logic): if rule.alternate_small_key is not None: for loc in rule.alternate_big_key_loc: logger.debug('---BK Loc %s', loc.name) + logger.debug('Placement rules for %s', d_name) + for rule in key_logic.placement_rules: + 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])) def build_pair_list(flat_list): @@ -1105,6 +1113,7 @@ def ncr(n, r): def reassign_key_doors(builder, world, player): logger = logging.getLogger('') + logger.debug('Key doors for %s', builder.name) proposal = builder.key_door_proposal flat_proposal = flatten_pair_list(proposal) queue = deque(find_current_key_doors(builder)) diff --git a/Fill.py b/Fill.py index b3d0948a..39a4d24e 100644 --- a/Fill.py +++ b/Fill.py @@ -201,7 +201,8 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place else: test_state = maximum_exploration_state if (not single_player_placement or location.player == item_to_place.player)\ - and location.can_fill(test_state, item_to_place, perform_access_check): + and location.can_fill(test_state, item_to_place, perform_access_check)\ + and valid_key_placement(item_to_place, location, itempool, world): spot_to_fill = location break elif item_to_place.smallkey or item_to_place.bigkey: @@ -217,11 +218,42 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) + track_outside_keys(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) spot_to_fill.event = True itempool.extend(unplaced_items) + +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]: + return True + dungeon = location.parent_region.dungeon + if dungeon: + if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name): + 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]) + return key_logic.check_placement(unplaced_keys) + else: + inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) + or (item.bigkey and not world.bigkeyshuffle[item.player])) + return not inside_dungeon_item + + +def track_outside_keys(item, location, world): + if not item.smallkey: + return + item_dungeon = item.name.split('(')[1][:-1] + if item_dungeon == 'Escape': + item_dungeon = 'Hyrule Castle' + if location.player == item.player: + loc_dungeon = location.parent_region.dungeon + if loc_dungeon and loc_dungeon.name == item_dungeon: + return # this is an inside key + world.key_logic[item.player][item_dungeon].outside_keys += 1 + + def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a3813b13..24b7dbb7 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -21,6 +21,7 @@ class KeyLayout(object): self.max_drops = None self.all_chest_locations = {} self.big_key_special = False + self.all_locations = set() # bk special? # bk required? True if big chests or big doors exists @@ -44,6 +45,14 @@ class KeyLogic(object): self.bk_chests = set() self.logic_min = {} self.logic_max = {} + self.placement_rules = [] + self.outside_keys = 0 + + def check_placement(self, unplaced_keys): + for rule in self.placement_rules: + if not rule.is_satisfiable(self.outside_keys, unplaced_keys): + return False + return True class DoorRules(object): @@ -59,6 +68,38 @@ class DoorRules(object): self.small_location = None +class PlacementRule(object): + + def __init__(self): + self.door_reference = None + self.small_key = None + self.bk_conditional_set = None # the location that means + self.needed_keys_w_bk = None + self.needed_keys_wo_bk = None + self.check_locations_w_bk = None + self.check_locations_wo_bk = None + + def is_satisfiable(self, outside_keys, unplaced_keys): + bk_blocked = False + if self.bk_conditional_set: + for loc in self.bk_conditional_set: + if loc.item and loc.item.bigkey: + bk_blocked = True + break + 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: + empty_chests += 1 + elif loc.item and loc.item.name == self.small_key: + available_keys += 1 + place_able_keys = min(empty_chests, unplaced_keys) + available_keys += place_able_keys + return available_keys >= threshold + + class KeyCounter(object): def __init__(self, max_chests): @@ -108,6 +149,8 @@ def analyze_dungeon(key_layout, world, player): find_bk_locked_sections(key_layout, world) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) + if world.retro[player] and world.mode[player] != 'standard': + return original_key_counter = find_counter({}, False, key_layout) queue = deque([(None, original_key_counter)]) @@ -155,6 +198,7 @@ def analyze_dungeon(key_layout, world, player): 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) @@ -177,6 +221,65 @@ def analyze_dungeon(key_layout, world, player): rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num +def create_placement_rule(key_layout, door, odd_ctr, current_ctr, 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) + + +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): + 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)) + important_found = False + for loc in blocked_others: + if important_location(loc, world, player): + important_found = True + break + if not important_found: + rule.needed_keys_w_bk -= 1 + def count_key_drops(sector): cnt = 0 @@ -211,6 +314,8 @@ def find_bk_locked_sections(key_layout, world): big_chest_allowed_big_key = world.accessibility != '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) if counter.big_key_opened and counter.important_location: big_chest_allowed_big_key = False if not counter.big_key_opened: @@ -241,6 +346,28 @@ def relative_empty_counter(odd_counter, key_counter): return True +def relative_empty_counter_2(odd_counter, key_counter): + if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0: + return False + if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: + return False + for child in odd_counter.child_doors: + if unique_child_door_2(child, key_counter): + return False + return True + + +def progressive_ctr(new_counter, last_counter): + if len(set(new_counter.key_only_locations).difference(last_counter.key_only_locations)) > 0: + return True + if len(set(new_counter.free_locations).difference(last_counter.free_locations)) > 0: + return True + for child in new_counter.child_doors: + if unique_child_door_2(child, last_counter): + return True + return False + + def unique_child_door(child, key_counter): if child in key_counter.child_doors or child.dest in key_counter.child_doors: return False @@ -251,6 +378,14 @@ def unique_child_door(child, key_counter): return True +def unique_child_door_2(child, key_counter): + if child in key_counter.child_doors or child.dest in key_counter.child_doors: + return False + if child in key_counter.open_doors or child.dest in key_counter.child_doors: + return False + return True + + def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? ignored_doors = {door, door.dest} if door is not None else {} finished = False @@ -280,7 +415,34 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, return last_counter -def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk): +def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible? + ignored_doors = {door, door.dest} if door is not None else {} + finished = False + opened_doors = dict(key_counter.open_doors) + bk_opened = key_counter.big_key_opened + # new_counter = key_counter + last_counter = key_counter + while not finished: + door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk, 0) + if door_set is None or len(door_set) == 0: + finished = True + continue + for new_door in door_set: + proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} + bk_open = bk_opened or new_door.bigKey + new_counter = find_counter(proposed_doors, bk_open, key_layout) + bk_open = new_counter.big_key_opened + if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter): + ignored_doors.add(new_door) + else: + last_counter = new_counter + opened_doors = proposed_doors + bk_opened = bk_open + # this means the new_door invalidates the door / leads to the same stuff + return last_counter + + +def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk, reserve=1): small_doors = [] big_doors = [] for other in key_counter.child_doors: @@ -293,7 +455,7 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk): if key_layout.big_key_special: big_key_available = key_counter.big_key_opened else: - big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0 + big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(reserve) > 0 if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)): return None return small_doors + big_doors @@ -367,7 +529,7 @@ def create_rule(key_counter, prev_counter, key_layout, world, player): def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): - if world.accessibility != 'locations': + if world.accessibility[player] != 'locations': counter = find_inverted_counter(door, parent_counter, key_layout, world, player) if not self_lock_possible(counter): return @@ -489,6 +651,27 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la # key_layout.key_logic.bk_restricted.update(unique_loc) +def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout): + if key_counter.big_key_opened: + return None, None, None + worst_counter = find_worst_counter(door, odd_ctr, key_counter, key_layout, True) + bk_rule_num = worst_counter.used_keys + 1 + bk_access_set = set() + bk_access_set.update(worst_counter.free_locations) + bk_access_set.update(worst_counter.key_only_locations) + if bk_rule_num == small_key_num and len(bk_access_set ^ accessible_set) == 0: + return None, None, None + door_open = find_next_counter(door, worst_counter, key_layout) + ignored_doors = dict_intersection(worst_counter.child_doors, door_open.child_doors) + dest_ignored = [] + for door in ignored_doors.keys(): + if door.dest not in ignored_doors: + dest_ignored.append(door.dest) + ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} + post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) + return worst_counter, post_counter, bk_rule_num + + def open_a_door(door, child_state, flat_proposal): if door.bigKey: child_state.big_key_opened = True @@ -833,6 +1016,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(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): if (not smalls_avail or not enough_small_locations(state, available_small_locations)) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): return False else: