From 8ad28542a478cc518f226e9c43f84cc6dbb3e622 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Wed, 5 Feb 2020 18:54:57 +1100 Subject: [PATCH 1/4] Check for key locks after placing dungeon items. --- BaseClasses.py | 3 ++- DoorShuffle.py | 1 + KeyDoorShuffle.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- Main.py | 6 ++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index deb2862c..03c40c21 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,7 +2,7 @@ import copy from enum import Enum, unique, Flag import logging import json -from collections import OrderedDict, deque +from collections import OrderedDict, deque, defaultdict from EntranceShuffle import door_addresses from _vendor.collections_extended import bag @@ -70,6 +70,7 @@ class World(object): self.inaccessible_regions = {} self.key_logic = {} self.pool_adjustment = {} + self.key_layout = defaultdict(dict) for player in range(1, players + 1): def set_player_attr(attr, val): diff --git a/DoorShuffle.py b/DoorShuffle.py index 33afa4fd..63d1c351 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -988,6 +988,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True analyze_dungeon(key_layout, world, player) builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout return True diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7919b4ee..31d8e3d6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -147,8 +147,9 @@ def analyze_dungeon(key_layout, world, player): 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) - if not rule.is_valid: - logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name) + # 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) @@ -1233,3 +1234,42 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None): assert len(setCheck) == len(rule.alternate_big_key_loc) for loc in rule.alternate_big_key_loc: assert loc.name in setCheck + + +# Soft lock stuff +def validate_key_placement(key_layout, world, player): + if world.retro[player] or world.accessibility[player] == 'none': + return True # Can't keylock in retro. Expected if beatable only. + max_counter = find_max_counter(key_layout) + keys_outside = 0 + big_key_outside = False + dungeon = world.get_dungeon(key_layout.sector.name, player) + smallkey_name = 'Small Key (%s)' % (key_layout.sector.name if key_layout.sector.name != 'Hyrule Castle' else 'Escape') + if world.keyshuffle[player]: + keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + if world.bigkeyshuffle[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(): + if len(counter.child_doors) == 0: + continue + found_keys = sum(1 for i in counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ + len(counter.key_only_locations) + keys_outside + big_found = any(i.item == dungeon.big_key for i in counter.free_locations) or big_key_outside + can_progress = (not counter.big_key_opened and big_found) or found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) + if not can_progress: + missing_locations = set(max_counter.free_locations.keys()).difference(counter.free_locations.keys()) + missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) + if world.accessibility[player] == 'items': + # Maybe ok if only locking keys + missing_key_only = [] + missing_locations = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key)] + if len(missing_locations) > 0 or len(missing_key_only) > 0: + logging.getLogger('').error("Keylock - can't open locations: ") + for i in missing_locations: + logging.getLogger('').error(i) + return False + + return True + diff --git a/Main.py b/Main.py index 80edd76b..f8dbbe6d 100644 --- a/Main.py +++ b/Main.py @@ -10,6 +10,7 @@ import zlib from BaseClasses import World, CollectionState, Item, Region, Location, Shop from Items import ItemFactory +from KeyDoorShuffle import validate_key_placement from Regions import create_regions, create_shops, mark_light_world_regions, create_dungeon_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances @@ -134,6 +135,11 @@ def main(args, seed=None): else: 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("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player)) + logger.info('Fill the world.') if args.algorithm == 'flood': From b321410e5a4b1c245f58c2d0984e63eb3c7a0e4b Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Wed, 5 Feb 2020 22:48:39 +1100 Subject: [PATCH 2/4] Keylock checks: Big Chest only available after the big key is found Bosses are always required locations Don't check for 100% locations - failure rate is too high --- KeyDoorShuffle.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 31d8e3d6..31e538fe 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1254,17 +1254,19 @@ def validate_key_placement(key_layout, world, player): for counter in key_layout.key_counters.values(): if len(counter.child_doors) == 0: continue - found_keys = sum(1 for i in counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ + 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 + found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) + found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside - big_found = any(i.item == dungeon.big_key for i in counter.free_locations) or big_key_outside - can_progress = (not counter.big_key_opened and big_found) or found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) + can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ + found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) if not can_progress: - missing_locations = set(max_counter.free_locations.keys()).difference(counter.free_locations.keys()) + missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) - if world.accessibility[player] == 'items': - # Maybe ok if only locking keys + if world.accessibility[player] == 'items' or True: # locations is busted - just check items are accessible for now + # Ok if only locking keys missing_key_only = [] - missing_locations = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key)] + missing_locations = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name] if len(missing_locations) > 0 or len(missing_key_only) > 0: logging.getLogger('').error("Keylock - can't open locations: ") for i in missing_locations: From ed3e2da663722562b3332fbb482c3a7cefbfe50f Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Wed, 5 Feb 2020 23:17:59 +1100 Subject: [PATCH 3/4] Log all missing chests, not just non-keys. --- KeyDoorShuffle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 31e538fe..4af145b1 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1262,12 +1262,9 @@ def validate_key_placement(key_layout, world, player): found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) - missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) - if world.accessibility[player] == 'items' or True: # locations is busted - just check items are accessible for now - # Ok if only locking keys - missing_key_only = [] - missing_locations = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name] - if len(missing_locations) > 0 or len(missing_key_only) > 0: + missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name] + #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: ") for i in missing_locations: logging.getLogger('').error(i) From 8fc913274685dd8b752f0c3b9fd0aa0fde45c8b5 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sat, 8 Feb 2020 11:31:50 +1100 Subject: [PATCH 4/4] Ignore impossible key counters when checking key placement. --- KeyDoorShuffle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4af145b1..a3813b13 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1239,7 +1239,7 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None): # Soft lock stuff def validate_key_placement(key_layout, world, player): if world.retro[player] or world.accessibility[player] == 'none': - return True # Can't keylock in retro. Expected if beatable only. + return True # Can't keylock in retro. Expected if beatable only. max_counter = find_max_counter(key_layout) keys_outside = 0 big_key_outside = False @@ -1255,6 +1255,8 @@ def validate_key_placement(key_layout, world, player): 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 + if counter.big_key_opened and not big_found: + continue # Can't get to this state found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside