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 0e1d8336..7568bba3 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -991,6 +991,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..a3813b13 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,43 @@ 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 + 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 + 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(found_locations) + 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) + return False + + return True + diff --git a/Main.py b/Main.py index bd03c26d..7aaa0e55 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':