From b21564d5aa738c0174a070434a71b7dcb1299fc2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 29 Jun 2021 16:34:28 -0600 Subject: [PATCH 01/27] Combinatoric approach revised (KLA1) Backported some fixes --- BaseClasses.py | 359 +++++++++++++++++++++++++++++++++++++++------- DoorShuffle.py | 35 +---- Dungeons.py | 15 -- Fill.py | 16 ++- KeyDoorShuffle.py | 146 +++++++++++++------ Main.py | 1 + Rules.py | 52 +++++-- Utils.py | 24 ++++ 8 files changed, 490 insertions(+), 158 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ba685ac2..54a45817 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -458,17 +458,26 @@ class World(object): class CollectionState(object): - def __init__(self, parent): - self.prog_items = Counter() + def __init__(self, parent, skip_init=False): self.world = parent - self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} - self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} - self.events = [] - self.path = {} - self.locations_checked = set() - self.stale = {player: True for player in range(1, parent.players + 1)} - for item in parent.precollected_items: - self.collect(item, True) + if not skip_init: + self.prog_items = Counter() + self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} + self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} + self.events = [] + self.path = {} + self.locations_checked = set() + self.stale = {player: True for player in range(1, parent.players + 1)} + for item in parent.precollected_items: + self.collect(item, True) + # reached vs. opened in the counter + self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)} + self.reached_doors = {player: set() for player in range(1, parent.players + 1)} + self.opened_doors = {player: set() for player in range(1, parent.players + 1)} + self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} + + self.ghost_keys = Counter() + self.dungeon_limits = None def update_reachable_regions(self, player): self.stale[player] = False @@ -479,66 +488,261 @@ class CollectionState(object): start = self.world.get_region('Menu', player) if not start in rrp: rrp[start] = CrystalBarrier.Orange - for exit in start.exits: - bc[exit] = CrystalBarrier.Orange + for conn in start.exits: + bc[conn] = CrystalBarrier.Orange queue = deque(self.blocked_connections[player].items()) + self.traverse_world(queue, rrp, bc, player) + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) + + def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items - while True: - try: - connection, crystal_state = queue.popleft() - new_region = connection.connected_region - if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): - bc.pop(connection, None) - elif connection.can_reach(self): - if new_region.type == RegionType.Dungeon: - new_crystal_state = crystal_state - for exit in new_region.exits: - door = exit.door - if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self): - new_crystal_state = CrystalBarrier.Either - break - if new_region in rrp: - new_crystal_state |= rrp[new_region] + while len(queue) > 0: + connection, crystal_state = queue.popleft() + new_region = connection.connected_region + if not self.should_visit(new_region, rrp, crystal_state, player): + bc.pop(connection, None) + elif connection.can_reach(self): + bc.pop(connection, None) + if new_region.type == RegionType.Dungeon: + new_crystal_state = crystal_state + if new_region in rrp: + new_crystal_state |= rrp[new_region] - rrp[new_region] = new_crystal_state - - for exit in new_region.exits: - door = exit.door - if door is not None and not door.blocked: + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + door = conn.door + if door is not None and not door.blocked: + if self.valid_crystal(door, new_crystal_state): door_crystal_state = door.crystal if door.crystal else new_crystal_state - bc[exit] = door_crystal_state - queue.append((exit, door_crystal_state)) - elif door is None: - queue.append((exit, new_crystal_state)) + bc[conn] = door_crystal_state + queue.append((conn, door_crystal_state)) + elif door is None: + # note: no door in dungeon indicates what exactly? (always traversable)? + queue.append((conn, new_crystal_state)) + else: + new_crystal_state = CrystalBarrier.Orange + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + bc[conn] = new_crystal_state + queue.append((conn, new_crystal_state)) + + self.path[new_region] = (new_region.name, self.path.get(connection, None)) + + # Retry connections if the new region can unblock them + if new_region.name in indirect_connections: + new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + if new_entrance in bc and new_entrance.parent_region in rrp: + new_crystal_state = rrp[new_entrance.parent_region] + if (new_entrance, new_crystal_state) not in queue: + queue.append((new_entrance, new_crystal_state)) + # else those connections that are not accessible yet + if self.is_small_door(connection) and not self.world.retro[player]: # todo: retro + door = connection.door + dungeon_name = connection.parent_region.dungeon.name # todo: universal + key_logic = self.world.key_logic[player][dungeon_name] + if door.name not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door.name) + if key_logic.sm_doors[door]: + self.reached_doors[player].add(key_logic.sm_doors[door].name) + if not connection.can_reach(self): + checklist = self.dungeons_to_check[player][dungeon_name] + checklist[connection.name] = (connection, crystal_state) + elif door.name not in self.opened_doors[player]: + opened_doors = self.opened_doors[player] + door = connection.door + if door.name not in opened_doors: + self.door_counter[player][1][dungeon_name] += 1 + opened_doors.add(door.name) + key_logic = self.world.key_logic[player][dungeon_name] + if key_logic.sm_doors[door]: + opened_doors.add(key_logic.sm_doors[door].name) + + def should_visit(self, new_region, rrp, crystal_state, player): + if not new_region: + return False + if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player): + return False + if new_region not in rrp: + return True + if new_region.type != RegionType.Dungeon: + return False + return (rrp[new_region] & crystal_state) != crystal_state + + def possibly_connected_to_dungeon(self, new_region, player): + if new_region.dungeon: + return new_region.dungeon.name in self.dungeon_limits + else: + return new_region.name in self.world.inaccessible_regions[player] + + @staticmethod + def valid_crystal(door, new_crystal_state): + return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either + or new_crystal_state == door.crystal) + + def check_key_doors_in_dungeons(self, rrp, player): + for dungeon_name, checklist in self.dungeons_to_check[player].items(): + init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) + key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal + remaining_keys = key_total - self.door_counter[player][1][dungeon_name] + if not init_door_candidates or remaining_keys == 0: + continue + dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()} + + def valid_d_door(x): + return x in dungeon_doors + + child_states = deque() + child_states.append(self) + visited_opened_doors = set() + visited_opened_doors.add(frozenset(self.opened_doors[player])) + terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set() + while not done: + terminal_states.clear() + while len(child_states) > 0: + next_child = child_states.popleft() + door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) + if door_candidates: + for chosen_door in door_candidates: + child_state = next_child.copy() + child_queue = deque() + child_state.door_counter[player][1][dungeon_name] += 1 + if isinstance(chosen_door, tuple): + child_state.opened_doors[player].add(chosen_door[0]) + child_state.opened_doors[player].add(chosen_door[1]) + if chosen_door[0] in checklist: + child_queue.append(checklist[chosen_door[0]]) + if chosen_door[1] in checklist: + child_queue.append(checklist[chosen_door[1]]) + else: + child_state.opened_doors[player].add(chosen_door) + if chosen_door in checklist: + child_queue.append(checklist[chosen_door]) + if child_state.opened_doors[player] not in visited_opened_doors: + done = False + while not done: + rrp_ = child_state.reachable_regions[player] + bc_ = child_state.blocked_connections[player] + self.dungeon_limits = [dungeon_name] + child_state.traverse_world(child_queue, rrp_, bc_, player) + new_events = child_state.sweep_for_events_once() + child_state.stale[player] = False + if new_events: + for conn in bc_: + if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: + child_queue.append((conn, bc_[conn])) + done = not new_events + visited_opened_doors.add(frozenset(child_state.opened_doors[player])) + child_states.append(child_state) else: - new_crystal_state = CrystalBarrier.Orange - rrp[new_region] = new_crystal_state - bc.pop(connection, None) - for exit in new_region.exits: - bc[exit] = new_crystal_state - queue.append((exit, new_crystal_state)) + terminal_states.append(next_child) + common_regions, common_doors, first = {}, set(), True + for term_state in terminal_states: + t_rrp = term_state.reachable_regions[player] + if first: + first = False + common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + else: + cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() + if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} + common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + done = len(child_states) == 0 - self.path[new_region] = (new_region.name, self.path.get(connection, None)) + terminal_queue = deque() + for door in common_doors: + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 - # Retry connections if the new region can unblock them - if new_region.name in indirect_connections: - new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) - if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp: - queue.append((new_entrance, rrp[new_entrance.parent_region])) - except IndexError: - break + self.dungeon_limits = [dungeon_name] + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + rrp = self.reachable_regions[player] + missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} + for k in missing_regions: + rrp[k] = missing_regions[k] + checklist.clear() + + @staticmethod + def comb_crys(a, b): + return a if a == b or a != CrystalBarrier.Either else b + + @staticmethod + def crys_agree(a, b): + return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either + + def find_door_pair(self, player, dungeon_name, name): + for door in self.world.key_logic[player][dungeon_name].sm_doors.keys(): + if door.name == name: + paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door] + return paired_door.name if paired_door else None + return None + + @staticmethod + def should_explore_child_state(state, dungeon_name, player): + small_key_name = dungeon_keys[dungeon_name] # todo: universal + key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)] + remaining_keys = key_total - state.door_counter[player][1][dungeon_name] + unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name] + if remaining_keys > 0 and unopened_doors > 0: + key_logic = state.world.key_logic[player][dungeon_name] # todo: universal + door_candidates, skip = [], set() + for door, paired in key_logic.sm_doors.items(): + if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]: + if door.name not in skip: + if paired: + door_candidates.append((door.name, paired.name)) + skip.add(paired.name) + else: + door_candidates.append(door.name) + return door_candidates + return None + + @staticmethod + def print_rrp(rrp): + logger = logging.getLogger('') + logger.debug('RRP Checking') + for region, packet in rrp.items(): + new_crystal_state, logic, path = packet + logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})') + for i in range(0, len(logic)): + logger.debug(f'{logic[i]}') + logger.debug(f'{",".join(str(x) for x in path[i])}') def copy(self): - ret = CollectionState(self.world) + ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) + ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)} + ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1])) + for player in range(1, self.world.players + 1)} + ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)} + ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)} + # todo: verify if this isn't copied deep enough + ret.dungeons_to_check = { + player: defaultdict(dict, {name: copy.copy(checklist) + for name, checklist in self.dungeons_to_check[player].items()}) + for player in range(1, self.world.players + 1)} + ret.ghost_keys = self.ghost_keys.copy() return ret def can_reach(self, spot, resolution_hint=None, player=None): @@ -556,6 +760,19 @@ class CollectionState(object): return spot.can_reach(self) + def sweep_for_events_once(self, key_only=False, locations=None): + if locations is None: + locations = self.world.get_filled_locations() + checked_locations = set([l for l in locations if l in self.locations_checked]) + reachable_events = [location for location in locations if location.event and + (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + and location.can_reach(self)] + reachable_events = self._do_not_flood_the_keys(reachable_events) + for event in reachable_events: + if event not in checked_locations: + self.events.append((event.name, event.player)) + self.collect(event.item, True, event) + return len(reachable_events) > len(checked_locations) def sweep_for_events(self, key_only=False, locations=None): # this may need improvement @@ -603,6 +820,13 @@ class CollectionState(object): or not self.location_can_be_flooded(flood_location)) return True + @staticmethod + def is_small_door(connection): + return connection and connection.door and connection.door.smallKey + + def is_door_open(self, door_name, player): + return door_name in self.opened_doors[player] + @staticmethod def location_can_be_flooded(location): return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove'] @@ -1806,6 +2030,15 @@ class Item(object): def compass(self): return self.type == 'Compass' + @property + def dungeon(self): + if not self.smallkey and not self.bigkey and not self.map and not self.compass: + return None + item_dungeon = self.name.split('(')[1][:-1] + if item_dungeon == 'Escape': + item_dungeon = 'Hyrule Castle' + return item_dungeon + def __str__(self): return str(self.__unicode__()) @@ -2196,6 +2429,21 @@ dungeon_names = [ 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower' ] +dungeon_keys = { + 'Hyrule Castle': 'Small Key (Escape)', + 'Eastern Palace': 'Small Key (Eastern Palace)', + 'Desert Palace': 'Small Key (Desert Palace)', + 'Tower of Hera': 'Small Key (Tower of Hera)', + 'Agahnims Tower': 'Small Key (Agahnims Tower)', + 'Palace of Darkness': 'Small Key (Palace of Darkness)', + 'Swamp Palace': 'Small Key (Swamp Palace)', + 'Skull Woods': 'Small Key (Skull Woods)', + 'Thieves Town': 'Small Key (Thieves Town)', + 'Ice Palace': 'Small Key (Ice Palace)', + 'Misery Mire': 'Small Key (Misery Mire)', + 'Turtle Rock': 'Small Key (Turtle Rock)', + 'Ganons Tower': 'Small Key (Ganons Tower)' +} class PotItem(FastEnum): Nothing = 0x0 @@ -2346,3 +2594,10 @@ class Settings(object): args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.shufflepots[p] = True if settings[7] & 0x4 else False + + +@unique +class KeyRuleType(Enum): + WorstCase = 0 + AllowSmall = 1 + Lock = 2 diff --git a/DoorShuffle.py b/DoorShuffle.py index ee38f00c..53676cb9 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1,22 +1,21 @@ import random from collections import defaultdict, deque import logging -import operator as op import time from enum import unique, Flag from typing import DefaultDict, Dict, List -from functools import reduce -from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo +from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts -from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints +from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException -from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout +from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout +from Utils import ncr, kth_combination def link_doors(world, player): @@ -212,8 +211,8 @@ def vanilla_key_logic(world, 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) - if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: - validate_vanilla_key_logic(world, player) + # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + # validate_vanilla_key_logic(world, player) # some useful functions @@ -1576,28 +1575,6 @@ def find_key_door_candidates(region, checked, world, player): return candidates, checked_doors -def kth_combination(k, l, r): - if r == 0: - return [] - elif len(l) == r: - return l - else: - i = ncr(len(l)-1, r-1) - if k < i: - return l[0:1] + kth_combination(k, l[1:], r-1) - else: - return kth_combination(k-i, l[1:], r) - - -def ncr(n, r): - if r == 0: - return 1 - r = min(r, n-r) - numerator = reduce(op.mul, range(n, n-r, -1), 1) - denominator = reduce(op.mul, range(1, r+1), 1) - return numerator / denominator - - def reassign_key_doors(builder, world, player): logger = logging.getLogger('') logger.debug('Key doors for %s', builder.name) diff --git a/Dungeons.py b/Dungeons.py index d5297400..6f0f2197 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,21 +375,6 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } -dungeon_keys = { - 'Hyrule Castle': 'Small Key (Escape)', - 'Eastern Palace': 'Small Key (Eastern Palace)', - 'Desert Palace': 'Small Key (Desert Palace)', - 'Tower of Hera': 'Small Key (Tower of Hera)', - 'Agahnims Tower': 'Small Key (Agahnims Tower)', - 'Palace of Darkness': 'Small Key (Palace of Darkness)', - 'Swamp Palace': 'Small Key (Swamp Palace)', - 'Skull Woods': 'Small Key (Skull Woods)', - 'Thieves Town': 'Small Key (Thieves Town)', - 'Ice Palace': 'Small Key (Ice Palace)', - 'Misery Mire': 'Small Key (Misery Mire)', - 'Turtle Rock': 'Small Key (Turtle Rock)', - 'Ganons Tower': 'Small Key (Ganons Tower)' -} dungeon_bigs = { 'Hyrule Castle': 'Big Key (Escape)', diff --git a/Fill.py b/Fill.py index c1488113..9c914726 100644 --- a/Fill.py +++ b/Fill.py @@ -199,6 +199,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None + valid_locations = [] for location in locations: if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place @@ -209,11 +210,16 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = 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 valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location - break - elif item_to_place.smallkey or item_to_place.bigkey: + # todo: optimization: break instead of cataloging all valid locations + if not spot_to_fill: + spot_to_fill = location + valid_locations.append(location) + + if item_to_place.smallkey or item_to_place.bigkey: location.item = None + logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations') + if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) @@ -250,9 +256,7 @@ def valid_key_placement(item, location, itempool, world): 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' + item_dungeon = item.dungeon if location.player == item.player: loc_dungeon = location.parent_region.dungeon if loc_dungeon and loc_dungeon.name == item_dungeon: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 549a739c..857ee479 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs +from Dungeons import dungeon_bigs from DungeonGenerator import ExplorationState, special_big_key_doors @@ -25,6 +25,7 @@ class KeyLayout(object): self.all_locations = set() self.item_locations = set() + self.found_doors = set() # bk special? # bk required? True if big chests or big doors exists @@ -54,6 +55,7 @@ class KeyLogic(object): self.location_rules = {} self.outside_keys = 0 self.dungeon = dungeon_name + self.sm_doors = {} def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: @@ -65,6 +67,15 @@ class KeyLogic(object): return False return True + def reset(self): + self.door_rules.clear() + self.bk_restricted.clear() + self.bk_locked.clear() + self.sm_restricted.clear() + self.bk_doors.clear() + self.bk_chests.clear() + self.placement_rules.clear() + class DoorRules(object): @@ -79,6 +90,8 @@ class DoorRules(object): self.small_location = None self.opposite = None + self.new_rules = {} # keyed by type, or type+lock_item -> number + class LocationRule(object): def __init__(self): @@ -209,8 +222,19 @@ def calc_max_chests(builder, key_layout, world, player): def analyze_dungeon(key_layout, world, player): + key_layout.key_logic.reset() key_layout.key_counters = create_key_counters(key_layout, world, player) key_logic = key_layout.key_logic + for door in key_layout.proposal: + if isinstance(door, tuple): + key_logic.sm_doors[door[0]] = door[1] + key_logic.sm_doors[door[1]] = door[0] + else: + if door.dest and door.type != DoorType.SpiralStairs: + key_logic.sm_doors[door] = door.dest + key_logic.sm_doors[door.dest] = door + else: + key_logic.sm_doors[door] = None find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) @@ -247,8 +271,9 @@ def analyze_dungeon(key_layout, world, player): 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) + best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) + rule = create_rule(best_counter, key_counter, world, player) + create_worst_case_rule(rule, best_counter, 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 @@ -258,7 +283,8 @@ def analyze_dungeon(key_layout, world, player): if ctr_id not in visited_cid: queue.append((child, next_counter)) visited_cid.add(ctr_id) - check_rules(original_key_counter, key_layout, world, player) + # todo: why is this commented out? + # 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 for rule in key_logic.door_rules.values(): @@ -294,7 +320,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): 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) + # 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) @@ -320,6 +346,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe rule.needed_keys_w_bk -= 1 +# this rule is suspect - commented out usages for now 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)) @@ -478,7 +505,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a 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) + # 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) @@ -538,6 +565,8 @@ def relative_empty_counter(odd_counter, key_counter): return False if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: return False + if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0: + return False # important only if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0: return False @@ -594,33 +623,50 @@ def unique_child_door_2(child, key_counter): 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 - 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) - 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 - # this means the new_door invalidates the door / leads to the same stuff - if not empty_flag and relative_empty_counter(odd_counter, new_counter): - ignored_doors.add(new_door) - elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): - last_counter = new_counter - opened_doors = proposed_doors - bk_opened = bk_open - else: - ignored_doors.add(new_door) - return last_counter +# 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 +# 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) +# 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 +# # this means the new_door invalidates the door / leads to the same stuff +# if not empty_flag and relative_empty_counter(odd_counter, new_counter): +# ignored_doors.add(new_door) +# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): +# last_counter = new_counter +# opened_doors = proposed_doors +# bk_opened = bk_open +# else: +# ignored_doors.add(new_door) +# return last_counter + + +def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag): + best, best_ctr, locations = 0, None, 0 + for code, counter in key_layout.key_counters.items(): + if door not in counter.open_doors: + if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations): + if not skip_bk or not counter.big_key_opened: + if empty_flag or not relative_empty_counter(odd_counter, counter): + best = counter.used_keys + best_ctr = counter + locations = count_locations(counter) + return best_ctr + + +def count_locations(ctr): + return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations) def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible? @@ -717,7 +763,7 @@ def calc_avail_keys(key_counter, world, player): return raw_avail - key_counter.used_keys -def create_rule(key_counter, prev_counter, key_layout, world, player): +def create_rule(key_counter, prev_counter, world, player): # prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) chest_keys = available_chest_small_keys(key_counter, world, player) @@ -736,6 +782,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player): return DoorRules(rule_num, is_valid) +def create_worst_case_rule(rules, key_counter, world, player): + required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys + rules.new_rules[KeyRuleType.WorstCase] = required_keys + + def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): if world.accessibility[player] != 'locations': counter = find_inverted_counter(door, parent_counter, key_layout, world, player) @@ -845,16 +896,16 @@ def big_key_drop_available(key_counter): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player): if key_counter.big_key_opened: return - best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag) - bk_rule = create_rule(best_counter, key_counter, key_layout, world, player) + best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag) + bk_rule = create_rule(best_counter, key_counter, world, player) if bk_rule.small_key_num >= rule.small_key_num: return door_open = find_next_counter(door, best_counter, key_layout) ignored_doors = dict_intersection(best_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) + for d in ignored_doors.keys(): + if d.dest not in ignored_doors: + dest_ignored.append(d.dest) ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) @@ -862,8 +913,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la if len(unique_loc) > 0: # and bk_rule.is_valid rule.alternate_small_key = bk_rule.small_key_num rule.alternate_big_key_loc.update(unique_loc) - # elif not bk_rule.is_valid: - # key_layout.key_logic.bk_restricted.update(unique_loc) + if not door.bigKey: + rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1 def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout): @@ -935,6 +986,7 @@ def only_sm_doors(key_counter): return False return True + # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 @@ -1197,7 +1249,7 @@ def check_rules_deep(original_counter, key_layout, world, player): elif not door.bigKey: can_open = True if can_open: - can_progress = smalls_opened or not big_maybe_not_found + can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened next_counter = find_next_counter(door, counter, key_layout) c_id = cid(next_counter, key_layout) if c_id not in completed: @@ -1381,6 +1433,7 @@ def cnt_avail_big_locations(ttl_locations, state, world, player): def create_key_counters(key_layout, world, player): key_counters = {} + key_layout.found_doors.clear() flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': @@ -1403,6 +1456,9 @@ def create_key_counters(key_layout, world, player): while len(queue) > 0: next_key_counter, parent_state = queue.popleft() for door in next_key_counter.child_doors: + key_layout.found_doors.add(door) + if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: + key_layout.found_doors.add(door.dest) child_state = parent_state.copy() if door.bigKey or door.name in special_big_key_doors: key_layout.key_logic.bk_doors.add(door) @@ -1520,11 +1576,11 @@ def find_counter_hint(opened_doors, bk_hint, key_layout): def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout) if max_counter is None: raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout) return max_counter diff --git a/Main.py b/Main.py index 9600fa8f..74d4c77e 100644 --- a/Main.py +++ b/Main.py @@ -448,6 +448,7 @@ def copy_world(world): # these need to be modified properly by set_rules new_location.access_rule = lambda state: True new_location.item_rule = lambda state: True + new_location.forced_item = location.forced_item # copy remaining itempool. No item in itempool should have an assigned location for item in world.itempool: diff --git a/Rules.py b/Rules.py index 3762b9bb..b30780ed 100644 --- a/Rules.py +++ b/Rules.py @@ -3,7 +3,7 @@ import logging from collections import deque import OverworldGlitchRules -from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier +from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -1939,14 +1939,10 @@ bunny_impassible_doors = { 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(): - spot = world.get_entrance(door_name, player) - if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot): - rule = create_advanced_key_rule(d_logic, player, keys) - if keys.opposite: - rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite)) - add_rule(spot, rule) - + for door_name, rule in d_logic.door_rules.items(): + add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) + if rule.allow_small: + set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -1954,8 +1950,9 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.small_key_name, player) for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) - for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1: + for chest in d_logic.bk_chests: + add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: @@ -1963,6 +1960,39 @@ def add_key_logic_rules(world, player): add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) +def allow_self_locking_small(logic, player): + return lambda state, item: item.player == player and logic.small_key_name == item.name + + +def eval_small_key_door_main(state, door_name, dungeon, player): + if state.is_door_open(door_name, player): + return True + key_logic = state.world.key_logic[player][dungeon] + door_rule = key_logic.door_rules[door_name] + door_openable = False + for ruleType, number in door_rule.new_rules.items(): + if door_openable: + return True + if ruleType == KeyRuleType.WorstCase: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + elif ruleType == KeyRuleType.AllowSmall: + if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name: + return True # always okay if allow small is on + elif isinstance(ruleType, tuple): + lock, lock_item = ruleType + # this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't + for loc in door_rule.alternate_big_key_loc: + spot = state.world.get_location(loc, player) + if spot.item and spot.item.name == lock_item: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + break + return door_openable + + +def eval_small_key_door(door_name, dungeon, player): + return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) + + def retro_in_hc(spot): return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False diff --git a/Utils.py b/Utils.py index cf3db3fb..b8cf5497 100644 --- a/Utils.py +++ b/Utils.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import os import re +import operator as op import subprocess import sys import xml.etree.ElementTree as ET from collections import defaultdict +from functools import reduce def int16_as_bytes(value): @@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap return "New Rom Hash: " + basemd5.hexdigest() +def kth_combination(k, l, r): + if r == 0: + return [] + elif len(l) == r: + return l + else: + i = ncr(len(l)-1, r-1) + if k < i: + return l[0:1] + kth_combination(k, l[1:], r-1) + else: + return kth_combination(k-i, l[1:], r) + + +def ncr(n, r): + if r == 0: + return 1 + r = min(r, n-r) + numerator = reduce(op.mul, range(n, n-r, -1), 1) + denominator = reduce(op.mul, range(1, r+1), 1) + return numerator / denominator + + entrance_offsets = { 'Sanctuary': 0x2, 'HC West': 0x3, From f15832f85ece904d29f7ea288e9a2317a46c0f9f Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Jul 2021 16:00:47 -0600 Subject: [PATCH 02/27] Refinement - Missing blocked_connections added - appropriate checklists Optimization - Exp Cache - which is somewhat problematic with the playthrough logic --- BaseClasses.py | 239 ++++++++++++++++++++++++++++++++++------------ Fill.py | 10 +- KeyDoorShuffle.py | 1 + Main.py | 6 ++ Rules.py | 3 +- 5 files changed, 187 insertions(+), 72 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 54a45817..cab3c5c7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -138,6 +138,8 @@ class World(object): set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) + set_player_attr('exp_cache', defaultdict(dict)) + def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -392,6 +394,10 @@ class World(object): def clear_location_cache(self): self._cached_locations = None + def clear_exp_cache(self): + for p in range(1, self.players + 1): + self.exp_cache[p].clear() + def get_unfilled_locations(self, player=None): return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None] @@ -478,6 +484,7 @@ class CollectionState(object): self.ghost_keys = Counter() self.dungeon_limits = None + # self.trace = None def update_reachable_regions(self, player): self.stale[player] = False @@ -588,6 +595,8 @@ class CollectionState(object): def check_key_doors_in_dungeons(self, rrp, player): for dungeon_name, checklist in self.dungeons_to_check[player].items(): + if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): + continue init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal remaining_keys = key_total - self.door_counter[player][1][dungeon_name] @@ -602,61 +611,64 @@ class CollectionState(object): child_states.append(self) visited_opened_doors = set() visited_opened_doors.add(frozenset(self.opened_doors[player])) - terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set() - while not done: - terminal_states.clear() - while len(child_states) > 0: - next_child = child_states.popleft() - door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) - if door_candidates: - for chosen_door in door_candidates: - child_state = next_child.copy() - child_queue = deque() - child_state.door_counter[player][1][dungeon_name] += 1 - if isinstance(chosen_door, tuple): - child_state.opened_doors[player].add(chosen_door[0]) - child_state.opened_doors[player].add(chosen_door[1]) - if chosen_door[0] in checklist: - child_queue.append(checklist[chosen_door[0]]) - if chosen_door[1] in checklist: - child_queue.append(checklist[chosen_door[1]]) - else: - child_state.opened_doors[player].add(chosen_door) - if chosen_door in checklist: - child_queue.append(checklist[chosen_door]) + terminal_states, common_regions, common_bc, common_doors = [], {}, {}, set() + while len(child_states) > 0: + next_child = child_states.popleft() + door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) + child_checklist = next_child.dungeons_to_check[player][dungeon_name] + if door_candidates: + for chosen_door in door_candidates: + child_state = next_child.copy() + child_queue = deque() + child_state.door_counter[player][1][dungeon_name] += 1 + if isinstance(chosen_door, tuple): + child_state.opened_doors[player].add(chosen_door[0]) + child_state.opened_doors[player].add(chosen_door[1]) + if chosen_door[0] in child_checklist: + child_queue.append(child_checklist[chosen_door[0]]) + if chosen_door[1] in child_checklist: + child_queue.append(child_checklist[chosen_door[1]]) + else: + child_state.opened_doors[player].add(chosen_door) + if chosen_door in child_checklist: + child_queue.append(child_checklist[chosen_door]) + if child_state.opened_doors[player] not in visited_opened_doors: + done = False + while not done: + rrp_ = child_state.reachable_regions[player] + bc_ = child_state.blocked_connections[player] + self.dungeon_limits = [dungeon_name] + child_state.traverse_world(child_queue, rrp_, bc_, player) + new_events = child_state.sweep_for_events_once() + child_state.stale[player] = False + if new_events: + for conn in bc_: + if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: + child_queue.append((conn, bc_[conn])) + done = not new_events if child_state.opened_doors[player] not in visited_opened_doors: - done = False - while not done: - rrp_ = child_state.reachable_regions[player] - bc_ = child_state.blocked_connections[player] - self.dungeon_limits = [dungeon_name] - child_state.traverse_world(child_queue, rrp_, bc_, player) - new_events = child_state.sweep_for_events_once() - child_state.stale[player] = False - if new_events: - for conn in bc_: - if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: - child_queue.append((conn, bc_[conn])) - done = not new_events visited_opened_doors.add(frozenset(child_state.opened_doors[player])) child_states.append(child_state) - else: - terminal_states.append(next_child) - common_regions, common_doors, first = {}, set(), True - for term_state in terminal_states: - t_rrp = term_state.reachable_regions[player] - if first: - first = False - common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} - common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] - if valid_d_door(x)} - else: - cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} - common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() - if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} - common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] - if valid_d_door(x)} - done = len(child_states) == 0 + else: + terminal_states.append(next_child) + common_regions, common_bc, common_doors, first = {}, {}, set(), True + bc = self.blocked_connections[player] + for term_state in terminal_states: + t_rrp = term_state.reachable_regions[player] + t_bc = term_state.blocked_connections[player] + if first: + first = False + common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_bc = {x: y for x, y in t_bc.items() if x not in bc} + common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + else: + cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() + if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} + common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc}) + common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} terminal_queue = deque() for door in common_doors: @@ -669,6 +681,9 @@ class CollectionState(object): self.dungeon_limits = [dungeon_name] rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) self.traverse_world(terminal_queue, rrp_, bc_, player) self.dungeon_limits = None @@ -676,6 +691,13 @@ class CollectionState(object): missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} for k in missing_regions: rrp[k] = missing_regions[k] + missing_bc = {} + for blocked, crystal in common_bc.items(): + if blocked not in bc and self.should_visit(blocked.connected_region, rrp, crystal, player): + missing_bc[blocked] = crystal + for k in missing_bc: + bc[k] = missing_bc[k] + self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, missing_regions, missing_bc) checklist.clear() @staticmethod @@ -737,7 +759,6 @@ class CollectionState(object): for player in range(1, self.world.players + 1)} ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)} ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)} - # todo: verify if this isn't copied deep enough ret.dungeons_to_check = { player: defaultdict(dict, {name: copy.copy(checklist) for name, checklist in self.dungeons_to_check[player].items()}) @@ -745,6 +766,97 @@ class CollectionState(object): ret.ghost_keys = self.ghost_keys.copy() return ret + def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): + bc = self.blocked_connections[player] + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + if dungeon_name in ec and exp_key in ec[dungeon_name]: + # apply + cnt, miss, common_doors, missing_regions, missing_bc = ec[dungeon_name][exp_key] + terminal_queue = deque() + for door in common_doors: + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 + + self.dungeon_limits = [dungeon_name] + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + + for k in missing_regions: + rrp[k] = missing_regions[k] + for k in missing_bc: + bc[k] = missing_bc[k] + + return True + return False + + def record_dungeon_exploration(self, player, dungeon_name, checklist, common_doors, missing_regions, missing_bc): + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + count = 1 + misses = 0 + # if exp_key in ec: + # if dungeon_name in ec[exp_key]: + # cnt, miss, old_common, old_missing, old_bc, trace = ec[exp_key][dungeon_name] + # if old_common == common_doors and old_missing == missing_regions and old_bc == missing_bc: + # count = cnt + 1 + # else: + # misses = miss + 1 + ec[dungeon_name][exp_key] = (count, misses, common_doors, missing_regions, missing_bc) + + def reduce_prog_items(self, player, dungeon_name): + # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc + # static logic rules needed most likely + # todo: universal smalls where needed + life_count, bottle_count = 0, 0 + reduced = Counter() + for item, cnt in self.prog_items.items(): + item_name, item_player = item + if item_player == player and self.check_if_progressive(item_name): + if item_name.startswith('Bottle'): + bottle_count += cnt + elif item_name.startswith(('Small Key', 'Big Key')): + d_name = 'Escape' if dungeon_name == 'Hyrule Castle' else dungeon_name + if d_name in item_name: + reduced[item] = cnt + elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: + if 'Container' in item_name: + life_count += 1 + elif 'Piece of Heart' == item_name: + life_count += .25 + else: + reduced[item] = cnt + if bottle_count > 0: + reduced[('Bottle', player)] = 1 + if life_count >= 1: + reduced[('Heart Container', player)] = 1 + return frozenset(reduced.items()) + + @staticmethod + def check_if_progressive(item_name): + return (item_name in + ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', + 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', + 'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod', + 'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword', + 'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant', + 'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', + 'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield', + 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', + 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', + 'Magic Upgrade (1/4)'] + or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + def can_reach(self, spot, resolution_hint=None, player=None): try: spot_type = spot.spot_type @@ -760,13 +872,10 @@ class CollectionState(object): return spot.can_reach(self) - def sweep_for_events_once(self, key_only=False, locations=None): - if locations is None: - locations = self.world.get_filled_locations() + def sweep_for_events_once(self): + locations = self.world.get_filled_locations() checked_locations = set([l for l in locations if l in self.locations_checked]) - reachable_events = [location for location in locations if location.event and - (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) - and location.can_reach(self)] + reachable_events = [location for location in locations if location.event and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) for event in reachable_events: if event not in checked_locations: @@ -1958,9 +2067,7 @@ class Location(object): return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) def can_reach(self, state): - if self.parent_region.can_reach(state) and self.access_rule(state): - return True - return False + return self.parent_region.can_reach(state) and self.access_rule(state) def forced_big_key(self): if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player: @@ -1987,6 +2094,12 @@ class Location(object): world = self.parent_region.world if self.parent_region and self.parent_region.world else None return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' + def __eq__(self, other): + return self.name == other.name and self.player == other.player + + def __hash__(self): + return hash((self.name, self.player)) + class Item(object): @@ -2597,7 +2710,7 @@ class Settings(object): @unique -class KeyRuleType(Enum): +class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 Lock = 2 diff --git a/Fill.py b/Fill.py index 9c914726..bb608cdb 100644 --- a/Fill.py +++ b/Fill.py @@ -199,7 +199,6 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None - valid_locations = [] for location in locations: if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place @@ -210,16 +209,11 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = 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 valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - # todo: optimization: break instead of cataloging all valid locations - if not spot_to_fill: spot_to_fill = location - valid_locations.append(location) - + break if item_to_place.smallkey or item_to_place.bigkey: location.item = None - logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations') - if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) @@ -691,7 +685,7 @@ def balance_money_progression(world): for player in solvent: wallet[player] -= sphere_costs[player] for location in locked_by_money[player]: - if location == 'Kiki': + if isinstance(location, str) and location == 'Kiki': kiki_paid[player] = True else: state.collect(location.item, True, location) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 857ee479..5732d8cd 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -795,6 +795,7 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: rule.allow_small = True rule.small_location = next(iter(counter.free_locations)) + rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1 def find_inverted_counter(door, parent_counter, key_layout, world, player): diff --git a/Main.py b/Main.py index 74d4c77e..c72bcef6 100644 --- a/Main.py +++ b/Main.py @@ -389,6 +389,8 @@ def copy_world(world): ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() + ret.exp_cache = world.exp_cache.copy() + for player in range(1, world.players + 1): if world.mode[player] != 'inverted': create_regions(ret, player) @@ -557,10 +559,14 @@ def create_playthrough(world): logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None + # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic + world.clear_exp_cache() if world.can_beat_game(state_cache[num]): + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.append(location) else: # still required, got to keep it around + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end diff --git a/Rules.py b/Rules.py index b30780ed..d8796dab 100644 --- a/Rules.py +++ b/Rules.py @@ -1976,7 +1976,8 @@ def eval_small_key_door_main(state, door_name, dungeon, player): if ruleType == KeyRuleType.WorstCase: door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) elif ruleType == KeyRuleType.AllowSmall: - if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name: + if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name + and door_rule.small_location.item.player == player): return True # always okay if allow small is on elif isinstance(ruleType, tuple): lock, lock_item = ruleType From 7fde95e5d7ee4a6339c8dd15e0d370935d3fb15f Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Jul 2021 16:13:02 -0600 Subject: [PATCH 03/27] Small refinements for key logic --- BaseClasses.py | 35 +++++++++++++++------------------- Fill.py | 4 ++-- Main.py | 2 +- Rules.py | 6 ------ resources/app/cli/lang/en.json | 2 +- 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index cab3c5c7..2c41fbfb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -689,15 +689,20 @@ class CollectionState(object): rrp = self.reachable_regions[player] missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} + paths = {} for k in missing_regions: rrp[k] = missing_regions[k] + possible_path = terminal_states[0].path[k] + self.path[k] = paths[k] = possible_path missing_bc = {} for blocked, crystal in common_bc.items(): - if blocked not in bc and self.should_visit(blocked.connected_region, rrp, crystal, player): + if (blocked not in bc and blocked.parent_region in rrp + and self.should_visit(blocked.connected_region, rrp, crystal, player)): missing_bc[blocked] = crystal for k in missing_bc: bc[k] = missing_bc[k] - self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, missing_regions, missing_bc) + self.record_dungeon_exploration(player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths) checklist.clear() @staticmethod @@ -773,7 +778,7 @@ class CollectionState(object): exp_key = (prog_set, frozenset(checklist)) if dungeon_name in ec and exp_key in ec[dungeon_name]: # apply - cnt, miss, common_doors, missing_regions, missing_bc = ec[dungeon_name][exp_key] + common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() for door in common_doors: self.opened_doors[player].add(door) @@ -793,42 +798,32 @@ class CollectionState(object): for k in missing_regions: rrp[k] = missing_regions[k] + for r, path in paths.items(): + self.path[r] = path for k in missing_bc: bc[k] = missing_bc[k] return True return False - def record_dungeon_exploration(self, player, dungeon_name, checklist, common_doors, missing_regions, missing_bc): + def record_dungeon_exploration(self, player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths): ec = self.world.exp_cache[player] prog_set = self.reduce_prog_items(player, dungeon_name) exp_key = (prog_set, frozenset(checklist)) - count = 1 - misses = 0 - # if exp_key in ec: - # if dungeon_name in ec[exp_key]: - # cnt, miss, old_common, old_missing, old_bc, trace = ec[exp_key][dungeon_name] - # if old_common == common_doors and old_missing == missing_regions and old_bc == missing_bc: - # count = cnt + 1 - # else: - # misses = miss + 1 - ec[dungeon_name][exp_key] = (count, misses, common_doors, missing_regions, missing_bc) + ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths) def reduce_prog_items(self, player, dungeon_name): # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc - # static logic rules needed most likely + # cross dungeon requirements may be necessary for keysanity - which invalidates the above # todo: universal smalls where needed life_count, bottle_count = 0, 0 reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item if item_player == player and self.check_if_progressive(item_name): - if item_name.startswith('Bottle'): + if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt - elif item_name.startswith(('Small Key', 'Big Key')): - d_name = 'Escape' if dungeon_name == 'Hyrule Castle' else dungeon_name - if d_name in item_name: - reduced[item] = cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: if 'Container' in item_name: life_count += 1 diff --git a/Fill.py b/Fill.py index bb608cdb..ee13e990 100644 --- a/Fill.py +++ b/Fill.py @@ -209,8 +209,8 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = 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 valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location - break + spot_to_fill = location + break if item_to_place.smallkey or item_to_place.bigkey: location.item = None diff --git a/Main.py b/Main.py index c72bcef6..78fcaec8 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0.8-u' +__version__ = '0.4.1.0-u' class EnemizerError(RuntimeError): diff --git a/Rules.py b/Rules.py index d8796dab..3cd18b2b 100644 --- a/Rules.py +++ b/Rules.py @@ -1941,8 +1941,6 @@ def add_key_logic_rules(world, player): for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) - if rule.allow_small: - set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -1960,10 +1958,6 @@ def add_key_logic_rules(world, player): add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) -def allow_self_locking_small(logic, player): - return lambda state, item: item.player == player and logic.small_key_name == item.name - - def eval_small_key_door_main(state, door_name, dungeon, player): if state.is_door_open(door_name, player): return True diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 145667ef..fa7c355e 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -2,7 +2,7 @@ "cli": { "yes": "Yes", "no": "No", - "app.title": "ALttP Door Randomizer Version %s - Seed: %d, Code: %s", + "app.title": "ALttP Door Randomizer Version %s : --seed %d --code %s", "version": "Version", "seed": "Seed", "player": "Player", From 96e4ba3e2ed5bffafa2641e86ed5b665b465c23d Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 6 Jul 2021 13:20:28 -0700 Subject: [PATCH 04/27] Insanity: blacksmith/bomb shop placement shouldn't preclude another multi-exit cave from exiting there --- EntranceShuffle.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e78b35b8..e111ddff 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -963,7 +963,6 @@ def link_entrances(world, player): blacksmith_hut = blacksmith_doors.pop() connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) - exit_pool.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] @@ -971,7 +970,6 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) doors.remove(bomb_shop) - exit_pool.remove(bomb_shop) # handle remaining caves for cave in caves: @@ -1804,7 +1802,6 @@ def link_inverted_entrances(world, player): blacksmith_hut = blacksmith_doors.pop() connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) - exit_pool.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] @@ -1812,7 +1809,6 @@ def link_inverted_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) doors.remove(bomb_shop) - exit_pool.remove(bomb_shop) # handle remaining caves for cave in caves: From 8c2b6f3bd2165489353ac4e26c460b9107292e4c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 12 Jul 2021 15:54:38 -0700 Subject: [PATCH 05/27] Can't have a key at blind unless you can crack the floor and deliver the maiden --- KeyDoorShuffle.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5732d8cd..89fa307d 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1035,14 +1035,6 @@ def filter_big_chest(locations): return [x for x in locations if '- Big Chest' not in x.name] -def count_locations_exclude_logic(locations, key_logic): - cnt = 0 - for loc in locations: - if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc): - cnt += 1 - return cnt - - def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked @@ -1051,18 +1043,36 @@ def prize_or_event(loc): return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] -def count_free_locations(state): +def boss_unavail(loc, world, player): + # todo: ambrosia + # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name + return False + + +def blind_boss_unavail(loc, state, world, player): + if loc.name == "Thieves' Town - Boss": + # todo: check attic + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) + return False + + +def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if not prize_or_event(loc) and not loc.forced_item: + if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) + and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt -def count_locations_exclude_big_chest(state): +def count_locations_exclude_big_chest(state, world, player): cnt = 0 for loc in state.found_locations: - if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc): + if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt @@ -1340,7 +1350,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not smalls_avail and num_bigs == 0: return True # I think that's the end # todo: fix state to separate out these types - ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state) + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) From 9ef24a610d1defea451c5a94716b0508ace0ebef Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 13 Jul 2021 10:38:14 -0700 Subject: [PATCH 06/27] Bug with key flooding (events that would flood the key are not unresolved) Last ditch placement efforts for when things get stuck (crossed standard mostly) --- BaseClasses.py | 1 + Fill.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index c4257fa1..7a3fe9e8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -505,6 +505,7 @@ class CollectionState(object): unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) if len(unresolved_events) == 0: self.check_key_doors_in_dungeons(rrp, player) diff --git a/Fill.py b/Fill.py index d2480df8..2a578ff7 100644 --- a/Fill.py +++ b/Fill.py @@ -221,7 +221,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if world.accessibility[item_to_place.player] != 'none': logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) continue - raise FillError('No more spots to place %s' % item_to_place) + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, single_player_placement) + if spot_to_fill is None: + 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) @@ -258,6 +261,69 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): + def location_preference(loc): + if not loc.item.advancement: + return 1 + if loc.item.type and loc.item.type != 'Sword': + if loc.item.type in ['Map', 'Compass']: + return 2 + else: + return 3 + return 4 + + possible_swaps = [x for x in state.locations_checked + if x.item.type not in ['Event', 'Crystal']] + swap_locations = sorted(possible_swaps, key=location_preference) + + for location in swap_locations: + old_item = location.item + new_pool = list(itempool) + [old_item] + new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, + keys_in_itempool, single_player_placement) + if new_spot: + new_spot.item = item_to_place + swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + if swap_spot: + logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') + world.push_item(swap_spot, old_item, False) + locations.remove(swap_spot) + locations.append(new_spot) + return new_spot + return None + + +def find_spot_for_item(item_to_place, locations, world, base_state, pool, + keys_in_itempool=None, single_player_placement=False): + def sweep_from_pool(): + new_state = base_state.copy() + for item in pool: + new_state.collect(item, True) + new_state.sweep_for_events() + return new_state + for location in locations: + maximum_exploration_state = sweep_from_pool() + perform_access_check = True + if world.accessibility[item_to_place.player] == 'none': + perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state) + + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + location.item = item_to_place + test_state = maximum_exploration_state.copy() + test_state.stale[item_to_place.player] = True + 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 valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = None + return None + + 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: From b56f8d4136259ce1566a3be83a24f6aeaed391b4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 15 Jul 2021 08:03:33 -0700 Subject: [PATCH 07/27] Std+Retro: key logic for escape and exclude some bow logic sectors Bug with reachable doors --- BaseClasses.py | 48 +++++++++++++++++++++++++++++++-------------- DoorShuffle.py | 6 +++--- DungeonGenerator.py | 23 +++++++++++++++++++++- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7a3fe9e8..01179702 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -482,8 +482,6 @@ class CollectionState(object): self.reached_doors = {player: set() for player in range(1, parent.players + 1)} self.opened_doors = {player: set() for player in range(1, parent.players + 1)} self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} - - self.ghost_keys = Counter() self.dungeon_limits = None # self.trace = None @@ -551,9 +549,9 @@ class CollectionState(object): if (new_entrance, new_crystal_state) not in queue: queue.append((new_entrance, new_crystal_state)) # else those connections that are not accessible yet - if self.is_small_door(connection) and not self.world.retro[player]: # todo: retro + if self.is_small_door(connection): door = connection.door - dungeon_name = connection.parent_region.dungeon.name # todo: universal + dungeon_name = connection.parent_region.dungeon.name key_logic = self.world.key_logic[player][dungeon_name] if door.name not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 @@ -561,7 +559,8 @@ class CollectionState(object): if key_logic.sm_doors[door]: self.reached_doors[player].add(key_logic.sm_doors[door].name) if not connection.can_reach(self): - checklist = self.dungeons_to_check[player][dungeon_name] + checklist_key = 'Universal' if self.world.retro[player] else dungeon_name + checklist = self.dungeons_to_check[player][checklist_key] checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: opened_doors = self.opened_doors[player] @@ -639,7 +638,7 @@ class CollectionState(object): while not done: rrp_ = child_state.reachable_regions[player] bc_ = child_state.blocked_connections[player] - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) child_state.traverse_world(child_queue, rrp_, bc_, player) new_events = child_state.sweep_for_events_once() child_state.stale[player] = False @@ -674,13 +673,19 @@ class CollectionState(object): terminal_queue = deque() for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) - if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + if pair not in self.opened_doors[player]: self.door_counter[player][1][dungeon_name] += 1 - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] for block, crystal in bc_.items(): @@ -722,14 +727,20 @@ class CollectionState(object): return paired_door.name if paired_door else None return None + def set_dungeon_limits(self, player, dungeon_name): + if self.world.retro[player] and self.world.mode[player] == 'standard': + self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower'] + else: + self.dungeon_limits = [dungeon_name] + @staticmethod def should_explore_child_state(state, dungeon_name, player): - small_key_name = dungeon_keys[dungeon_name] # todo: universal - key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)] + small_key_name = dungeon_keys[dungeon_name] + key_total = state.prog_items[(small_key_name, player)] remaining_keys = key_total - state.door_counter[player][1][dungeon_name] unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name] if remaining_keys > 0 and unopened_doors > 0: - key_logic = state.world.key_logic[player][dungeon_name] # todo: universal + key_logic = state.world.key_logic[player][dungeon_name] door_candidates, skip = [], set() for door, paired in key_logic.sm_doors.items(): if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]: @@ -770,7 +781,6 @@ class CollectionState(object): player: defaultdict(dict, {name: copy.copy(checklist) for name, checklist in self.dungeons_to_check[player].items()}) for player in range(1, self.world.players + 1)} - ret.ghost_keys = self.ghost_keys.copy() return ret def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): @@ -783,13 +793,19 @@ class CollectionState(object): common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) - if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + if pair not in self.opened_doors[player]: self.door_counter[player][1][dungeon_name] += 1 - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] for block, crystal in bc_.items(): @@ -1812,6 +1828,7 @@ class Sector(object): self.entrance_sector = None self.destination_entrance = False self.equations = None + self.item_logic = set() def region_set(self): if self.r_name_set is None: @@ -2552,7 +2569,8 @@ dungeon_keys = { 'Ice Palace': 'Small Key (Ice Palace)', 'Misery Mire': 'Small Key (Misery Mire)', 'Turtle Rock': 'Small Key (Turtle Rock)', - 'Ganons Tower': 'Small Key (Ganons Tower)' + 'Ganons Tower': 'Small Key (Ganons Tower)', + 'Universal': 'Small Key (Universal)' } class PotItem(FastEnum): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ce078f0..5231fc91 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1101,9 +1101,9 @@ def assign_cross_keys(dungeon_builders, world, player): logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining) # Last Step: Adjust Small Key Dungeon Pool - if not world.retro[player]: - for name, builder in dungeon_builders.items(): - reassign_key_doors(builder, world, player) + for name, builder in dungeon_builders.items(): + reassign_key_doors(builder, world, player) + if not world.retro[player]: log_key_logic(builder.name, world.key_logic[player][builder.name]) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) dungeon = world.get_dungeon(name, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index c8599748..9d76ddb5 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1290,13 +1290,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) + bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard' free_location_sectors = {} crystal_switches = {} crystal_barriers = {} polarized_sectors = {} neutral_sectors = {} for sector in candidate_sectors: - if sector.chest_locations > 0: + if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC + bow_sectors[sector] = None + elif sector.chest_locations > 0: free_location_sectors[sector] = None elif sector.c_switch: crystal_switches[sector] = None @@ -1306,6 +1309,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, neutral_sectors[sector] = None else: polarized_sectors[sector] = None + if bow_sectors: + assign_bow_sectors(dungeon_map, bow_sectors, global_pole) assign_location_sectors(dungeon_map, free_location_sectors, global_pole) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) @@ -1471,6 +1476,9 @@ def define_sector_features(sectors): sector.blue_barrier = True if door.bigKey: sector.bk_required = True + if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2', + 'Eastern Single Eyegore', 'Eastern Duo Eyegores']: + sector.item_logic.add('Bow') def assign_sector(sector, dungeon, candidate_sectors, global_pole): @@ -1521,6 +1529,19 @@ def find_sector(r_name, sectors): return None +def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): + sector_list = list(bow_sectors) + random.shuffle(sector_list) + population = [] + for name in dungeon_map: + if name != 'Hyrule Castle': + population.append(name) + choices = random.choices(population, k=len(sector_list)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + assign_sector(sector_list[i], builder, bow_sectors, global_pole) + + def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): valid = False choices = None From df0218c7a199f06524f41ba94808d79568479a63 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 22 Jul 2021 16:09:28 -0700 Subject: [PATCH 08/27] last ditch swap fixes --- Fill.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index 2a578ff7..29226afd 100644 --- a/Fill.py +++ b/Fill.py @@ -273,8 +273,11 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite return 3 return 4 - possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal']] + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + else: + possible_swaps = [x for x in state.locations_checked + if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) for location in swap_locations: @@ -283,6 +286,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, keys_in_itempool, single_player_placement) if new_spot: + restore_item = new_spot.item new_spot.item = item_to_place swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, keys_in_itempool, single_player_placement) @@ -292,6 +296,9 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite locations.remove(swap_spot) locations.append(new_spot) return new_spot + else: + new_spot.item = restore_item + return None From 8c1e746556495f8135d8c7315feddee487ce3a27 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 22 Jul 2021 16:26:08 -0700 Subject: [PATCH 09/27] Inverted pathing fix (Castle S&Q may be required) --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 5231fc91..264b2e5a 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1835,7 +1835,7 @@ def find_accessible_entrances(world, player, builder): elif world.mode[player] != 'inverted': start_regions = ['Links House', 'Sanctuary'] else: - start_regions = ['Inverted Links House', 'Inverted Dark Sanctuary'] + start_regions = ['Inverted Links House', 'Inverted Dark Sanctuary', 'Hyrule Castle Ledge'] regs = convert_regions(start_regions, world, player) visited_regions = set() visited_entrances = [] From 46bfcb4dde7b1d8cb3fa9e23530a2f538d6d0f9f Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 23 Jul 2021 08:39:31 -0700 Subject: [PATCH 10/27] Aga Tower shouldn't choose inverted links house if not shuffled --- EntranceShuffle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e111ddff..cd8df010 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1446,7 +1446,7 @@ def link_inverted_entrances(world, player): # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] + aga_doors = [i for i in all_entrances_aga if world.shufflelinks[player] or i != 'Inverted Links House'] random.shuffle(aga_doors) aga_door = aga_doors.pop() @@ -1589,8 +1589,9 @@ def link_inverted_entrances(world, player): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_door = random.choice(entrances) - + aga_choices = [x for x in entrances if world.shufflelinks[player] or x != 'Inverted Links House'] + aga_door = random.choice(aga_choices) + if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) From 8ed6b3fad27752955dbe6edfabf4a49866099408 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 23 Jul 2021 12:37:41 -0700 Subject: [PATCH 11/27] Exception for DP back on basic,intensity level 1 (simply can't make a passage) --- DoorShuffle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DoorShuffle.py b/DoorShuffle.py index 264b2e5a..02f54069 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -502,6 +502,9 @@ def analyze_portals(world, player): raise Exception('please inspect this case') if len(reachable_portals) == 1: info.sole_entrance = reachable_portals[0] + if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace': + if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back': + info.required_passage.clear() # can't make a passage at this intensity level, something else must exit info_map[dungeon] = info for dungeon, info in info_map.items(): From 65c583c082cf7951eb3113fd47230b9021392938 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 16:00:05 -0600 Subject: [PATCH 12/27] Initial work on dungeon prize logic in key layouts --- DungeonGenerator.py | 10 ++++ Dungeons.py | 16 ++++++ KeyDoorShuffle.py | 137 +++++++++++++++++++++++++++++++++----------- 3 files changed, 129 insertions(+), 34 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 9d76ddb5..86be2a04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -807,6 +807,10 @@ class ExplorationState(object): self.dungeon = dungeon self.pinball_used = False + self.prize_door_set = {} + self.prize_doors = [] + self.prize_doors_opened = False + def copy(self): ret = ExplorationState(dungeon=self.dungeon) ret.unattached_doors = list(self.unattached_doors) @@ -833,6 +837,10 @@ class ExplorationState(object): ret.non_door_entrances = list(self.non_door_entrances) ret.dungeon = self.dungeon ret.pinball_used = self.pinball_used + + ret.prize_door_set = dict(self.prize_door_set) + ret.prize_doors = list(self.prize_doors) + ret.prize_doors_opened = self.prize_doors_opened return ret def next_avail_door(self): @@ -868,6 +876,8 @@ class ExplorationState(object): if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) + if '- Prize' in location.name: + self.prize_received = True def flooded_key_check(self, location): if location.name not in flooded_keys.keys(): diff --git a/Dungeons.py b/Dungeons.py index 6f0f2197..eca190c1 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -392,6 +392,22 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } +dungeon_prize = { + 'Hyrule Castle': None, + 'Eastern Palace': 'Eastern Palace - Prize', + 'Desert Palace': 'Desert Palace - Prize', + 'Tower of Hera': 'Tower of Hera - Prize', + 'Agahnims Tower': None, + 'Palace of Darkness': 'Palace of Darkness - Prize', + 'Swamp Palace': 'Swamp Palace - Prize', + 'Skull Woods': 'Skull Woods - Prize', + 'Thieves Town': 'Thieves Town - Prize', + 'Ice Palace': 'Ice Palace - Prize', + 'Misery Mire': 'Misery Mire - Prize', + 'Turtle Rock': 'Turtle Rock - Prize', + 'Ganons Tower': None +} + dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 89fa307d..0ecb98e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType, dungeon_keys, KeyRuleType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_bigs +from Dungeons import dungeon_bigs, dungeon_prize from DungeonGenerator import ExplorationState, special_big_key_doors @@ -26,6 +26,7 @@ class KeyLayout(object): self.item_locations = set() self.found_doors = set() + self.prize_relevant = False # bk special? # bk required? True if big chests or big doors exists @@ -36,6 +37,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() + self.prize_relevant = False class KeyLogic(object): @@ -183,6 +185,8 @@ class KeyCounter(object): self.important_location = False self.other_locations = {} self.important_locations = {} + self.prize_doors_opened = False + self.prize_received = False def used_smalls_loc(self, reserve=0): return max(self.used_keys + reserve - len(self.key_only_locations), 0) @@ -242,7 +246,7 @@ def analyze_dungeon(key_layout, world, player): if world.retro[player] and world.mode[player] != 'standard': return - original_key_counter = find_counter({}, False, key_layout) + original_key_counter = find_counter({}, False, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -270,15 +274,18 @@ def analyze_dungeon(key_layout, world, player): child_queue.append((child, odd_counter, empty_flag)) while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() - if not child.bigKey and child not in doors_completed: + prize_flag = key_counter.prize_doors_opened + if child in key_layout.flat_prop and child not in doors_completed: best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) rule = create_rule(best_counter, key_counter, world, player) create_worst_case_rule(rule, best_counter, 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 + elif not child.bigKey and child not in doors_completed: + prize_flag = True doors_completed.add(child) - next_counter = find_next_counter(child, key_counter, key_layout) + next_counter = find_next_counter(child, key_counter, key_layout, prize_flag) ctr_id = cid(next_counter, key_layout) if ctr_id not in visited_cid: queue.append((child, next_counter)) @@ -300,6 +307,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): + if key_counter.prize_received and not key_counter.prize_doors_opened: + continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) accessible_loc.update(key_counter.key_only_locations) @@ -684,7 +693,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # 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) + new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened) 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) @@ -736,7 +745,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, for new_child in new_children: proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened) if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player): wasted_keys += 1 if new_avail - wasted_keys < old_avail: @@ -744,10 +753,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, return False -def find_next_counter(new_door, old_counter, key_layout): +def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = old_counter.big_key_opened or new_door.bigKey - return find_counter(proposed_doors, bk_open, key_layout) + prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened + return find_counter(proposed_doors, bk_open, key_layout, prize_flag) def check_special_locations(locations): @@ -835,7 +845,7 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -856,7 +866,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened) changed = True return counter @@ -939,12 +949,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c return worst_counter, post_counter, bk_rule_num -def open_a_door(door, child_state, flat_proposal): +def open_a_door(door, child_state, flat_proposal, world, player): if door.bigKey or door.name in special_big_key_doors: child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) child_state.big_doors.clear() + elif door in child_state.prize_door_set: + child_state.prize_doors_opened = True + for exp_door in child_state.prize_doors: + new_region = exp_door.door.entrance.parent_region + child_state.visit_region(new_region, key_checks=True) + child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player) + child_state.prize_doors.clear() else: child_state.opened_doors.append(door) doors_to_open = [x for x in child_state.small_doors if x.door == door] @@ -1002,7 +1019,7 @@ def count_unique_small_doors(key_counter, proposal): def exist_relevant_big_doors(key_counter, key_layout): - bk_counter = find_counter(key_counter.open_doors, True, key_layout, False) + bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False) if bk_counter is not None: diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) if len(diff) > 0: @@ -1338,8 +1355,13 @@ def validate_key_layout(key_layout, world, player): state.key_locations = key_layout.max_chests state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player) @@ -1370,7 +1392,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() - open_a_door(exp_door.door, state_copy, flat_proposal) + open_a_door(exp_door.door, state_copy, flat_proposal, world, player) state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 @@ -1385,7 +1407,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return False if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)): state_copy = state.copy() - open_a_door(state.big_doors[0].door, state_copy, flat_proposal) + open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 code = state_id(state_copy, flat_proposal) @@ -1397,6 +1419,18 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa valid = checked_states[code] if not valid: return False + if not state.prize_doors_opened and key_layout.prize_relevant: + state_copy = state.copy() + open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) + code = state_id(state_copy, flat_proposal) + if code not in checked_states.keys(): + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, + state, available_small_locations, world, player) + checked_states[code] = valid + else: + valid = checked_states[code] + if not valid: + return False return True @@ -1407,7 +1441,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p 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) + open_a_door(door.door, state_copy, key_layout.flat_prop, world, player) 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)) @@ -1461,8 +1495,14 @@ def create_key_counters(key_layout, world, player): state.big_key_special = True special_region = region for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) code = state_id(state, key_layout.flat_prop) key_counters[code] = create_key_counter(state, key_layout, world, player) @@ -1478,7 +1518,7 @@ def create_key_counters(key_layout, world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): - open_a_door(door, child_state, flat_proposal) + open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) if code not in key_counters.keys(): @@ -1488,9 +1528,19 @@ def create_key_counters(key_layout, world, player): return key_counters +def find_outside_connection(region): + portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None) + if portal: + dungeon_entrance = next(x for x in portal.parent_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region) + return dungeon_entrance, portal_entrance.door + return None, None + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) - key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) + key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) for loc in state.found_locations: if important_location(loc, world, player): key_counter.important_location = True @@ -1507,6 +1557,10 @@ def create_key_counter(state, key_layout, world, player): key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.big_key_opened = state.big_key_opened + if len(state.prize_door_set) > 0 and state.prize_doors_opened: + key_counter.prize_doors_opened = True + if any(x for x in key_counter.important_locations if '- Prize' in x.name): + key_counter.prize_received = True return key_counter @@ -1557,11 +1611,13 @@ def state_id(state, flat_proposal): s_id = '1' if state.big_key_opened else '0' for d in flat_proposal: s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' return s_id -def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): - counter = find_counter_hint(opened_doors, bk_hint, key_layout) +def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): + counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: return counter more_doors = [] @@ -1570,43 +1626,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): if door.dest not in opened_doors.keys(): more_doors.append(door.dest) if len(more_doors) > len(opened_doors.keys()): - counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout) + counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag) if counter is not None: return counter if raise_on_error: - raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) + raise Exception(f'Unable to find door permutation. Init CID: {cid}') return None -def find_counter_hint(opened_doors, bk_hint, key_layout): - cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop) +def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag): + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] if not bk_hint: - cid = counter_id(opened_doors, True, key_layout.flat_prop) + cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] return None def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True) if max_counter is None: raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: - max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True) return max_counter -def counter_id(opened_doors, bk_unlocked, flat_proposal): +def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag): s_id = '1' if bk_unlocked else '0' for d in flat_proposal: s_id += '1' if d in opened_doors.keys() else '0' + if prize_relevant: + s_id += '1' if prize_flag else '0' return s_id def cid(counter, key_layout): - return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop) + return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop, + key_layout.prize_relevant, counter.prize_doors_opened) # class SoftLockException(Exception): @@ -1816,8 +1876,17 @@ def validate_key_placement(key_layout, world, player): 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 + if key_layout.prize_relevant: + found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + if not found_prize: + prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + # todo: pyramid fairy only care about crystals 5 & 6 + found_prize = 'Crystal' not in prize_loc.item.name + else: + found_prize = False 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) + found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \ + (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) 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.name != bigkey_name) or "- Boss" in l.name] From 8a361e9672dee24e20b7e3d2dfe0f5c2e4d633a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Aug 2021 12:39:48 -0600 Subject: [PATCH 13/27] Various refinements and fixes to key logic and generation --- DoorShuffle.py | 4 ++++ DungeonGenerator.py | 6 +++--- Dungeons.py | 3 --- Fill.py | 4 ++-- KeyDoorShuffle.py | 31 ++++++++++++++++++++++++------- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 02f54069..89e7a6d6 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1823,6 +1823,10 @@ def find_inaccessible_regions(world, player): if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) + if world.mode[player] == 'inverted': + ledge = world.get_region('Hyrule Castle Ledge', 1) + if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): + world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') logger.debug('Inaccessible Regions:') for r in world.inaccessible_regions[player]: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 86be2a04..47922be5 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1113,9 +1113,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player): def valid_region_to_explore(region, name, world, player): if region is None: return False - return (region.type == RegionType.Dungeon and region.dungeon.name in name)\ - or region.name in world.inaccessible_regions[player]\ - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard') + return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name) + or region.name in world.inaccessible_regions[player] + or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) def get_doors(world, region, player): diff --git a/Dungeons.py b/Dungeons.py index eca190c1..e3b76c28 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -393,11 +393,9 @@ dungeon_bigs = { } dungeon_prize = { - 'Hyrule Castle': None, 'Eastern Palace': 'Eastern Palace - Prize', 'Desert Palace': 'Desert Palace - Prize', 'Tower of Hera': 'Tower of Hera - Prize', - 'Agahnims Tower': None, 'Palace of Darkness': 'Palace of Darkness - Prize', 'Swamp Palace': 'Swamp Palace - Prize', 'Skull Woods': 'Skull Woods - Prize', @@ -405,7 +403,6 @@ dungeon_prize = { 'Ice Palace': 'Ice Palace - Prize', 'Misery Mire': 'Misery Mire - Prize', 'Turtle Rock': 'Turtle Rock - Prize', - 'Ganons Tower': None } dungeon_hints = { diff --git a/Fill.py b/Fill.py index 29226afd..d75cf080 100644 --- a/Fill.py +++ b/Fill.py @@ -722,7 +722,7 @@ def balance_money_progression(world): if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: wallet[player] += income rooms_visited[player].add(room) - if checked_locations: + if checked_locations or len(unchecked_locations) == 0: if world.has_beaten_game(state): done = True continue @@ -732,7 +732,7 @@ def balance_money_progression(world): solvent = set() insolvent = set() for player in range(1, world.players+1): - if wallet[player] >= sphere_costs[player] > 0: + if wallet[player] >= sphere_costs[player] >= 0: solvent.add(player) if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 0ecb98e4..4c591304 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -307,7 +307,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): - if key_counter.prize_received and not key_counter.prize_doors_opened: + if skip_key_counter_due_to_prize(key_layout, key_counter): continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) @@ -343,6 +343,10 @@ def create_exhaustive_placement_rules(key_layout, world, player): refine_location_rules(key_layout) +def skip_key_counter_due_to_prize(key_layout, key_counter): + return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened + + def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player): if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations)) @@ -1356,7 +1360,8 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + if (key_layout.prize_relevant and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance else: @@ -1396,7 +1401,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1410,7 +1415,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1422,7 +1427,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1496,7 +1501,9 @@ def create_key_counters(key_layout, world, player): special_region = region for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] + and key_layout.key_logic.dungeon in dungeon_prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1616,6 +1623,16 @@ def state_id(state, flat_proposal): return s_id +def validate_id(state, flat_proposal): + s_id = '1' if state.big_key_opened else '0' + for d in flat_proposal: + s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' + s_id += str(state.used_locations) + return s_id + + def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: @@ -1878,7 +1895,7 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize: + if not found_prize and key_layout.sector.name in dungeon_prize: prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name From 50296915a718a9b629c225368451bbfe9c1e3e46 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Aug 2021 12:40:29 -0600 Subject: [PATCH 14/27] Uncertain key rule change (keeping my eye on it) --- KeyDoorShuffle.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4c591304..e7bcae11 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -61,7 +61,7 @@ class KeyLogic(object): 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): + if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -149,13 +149,20 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys): + def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc): 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 + else: + def loc_has_bk(l): + return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) + + bk_found = any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) + if not bk_found: + return True 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 From aeb910e2746bac2c4b950398e5da68afd10e2949 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 08:43:47 -0600 Subject: [PATCH 15/27] Various fixes: Full boss shuffle samples without replacement Vanilla doors simply unmarks ugly smalls Money balance: handle multiworld solvent players better Perf for item pool adjustment Add loglevel to Mystery --- Bosses.py | 4 ++-- DoorShuffle.py | 9 +++++++++ Fill.py | 2 +- ItemList.py | 8 ++++---- Mystery.py | 2 ++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Bosses.py b/Bosses.py index 253717bd..2718431e 100644 --- a/Bosses.py +++ b/Bosses.py @@ -176,8 +176,8 @@ def place_bosses(world, player): if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] - else: # all bosses present, the three duplicates chosen at random - bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)] + else: # all bosses present, the three duplicates chosen at random + bosses = all_bosses + random.sample(placeable_bosses, 3) logging.getLogger('').debug('Bosses chosen %s', bosses) diff --git a/DoorShuffle.py b/DoorShuffle.py index d5b69126..ca4b9139 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -101,6 +101,8 @@ def link_doors_main(world, player): connect_portal(portal, world, player) if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) + else: + unmark_ugly_smalls(world, player) if world.doorShuffle[player] == 'vanilla': for entrance, ext in open_edges: connect_two_way(world, entrance, ext, player) @@ -316,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player): y.dest = x +def unmark_ugly_smalls(world, player): + for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S', + 'TR Lava Escape SE', 'GT Hidden Spikes SE']: + door = world.get_door(d, player) + door.smallKey = False + + def fix_big_key_doors_with_ugly_smalls(world, player): remove_ugly_small_key_doors(world, player) unpair_big_key_doors(world, player) diff --git a/Fill.py b/Fill.py index a6ef6cf4..04278d43 100644 --- a/Fill.py +++ b/Fill.py @@ -736,7 +736,7 @@ def balance_money_progression(world): solvent.add(player) if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) - if len(solvent) == 0: + if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) difference = sphere_costs[target_player]-wallet[target_player] logger.debug(f'Money balancing needed: Player {target_player} short {difference}') diff --git a/ItemList.py b/ItemList.py index e79392b8..eaa78365 100644 --- a/ItemList.py +++ b/ItemList.py @@ -347,11 +347,11 @@ def generate_itempool(world, player): # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0): - [item for item in items if item.name == 'Boss Heart Container'][0].advancement = True + next(item for item in items if item.name == 'Boss Heart Container').advancement = True elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4): - adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4] - for hp in adv_heart_pieces: - hp.advancement = True + adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') + for i in range(4): + next(adv_heart_pieces).advancement = True beeweights = {'0': {None: 100}, '1': {None: 75, 'trap': 25}, diff --git a/Mystery.py b/Mystery.py index 306b5c27..3c62e72a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -31,6 +31,7 @@ def main(): parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') + parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) for player in range(1, multiargs.multi + 1): parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) args = parser.parse_args() @@ -63,6 +64,7 @@ def main(): erargs.race = True erargs.outputname = seedname erargs.outputpath = args.outputpath + erargs.loglevel = args.loglevel if args.rom: erargs.rom = args.rom From 24b01cfcc7c48639c48ecd76a2a8290945a21fc6 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 09:26:41 -0600 Subject: [PATCH 16/27] Performance optimizations --- BaseClasses.py | 4 +++ EntranceShuffle.py | 7 ++-- Fill.py | 79 +++++++++++++++++++++++++++------------------- Main.py | 17 +++++----- Rom.py | 3 +- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b9f33267..087bda1b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -434,8 +434,12 @@ class World(object): def can_beat_game(self, starting_state=None): if starting_state: + if self.has_beaten_game(starting_state): + return True state = starting_state.copy() else: + if self.has_beaten_game(self.state): + return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 1c1a6ab8..7e5dcdd3 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2120,10 +2120,11 @@ def connect_doors(world, doors, targets, player): """This works inplace""" random.shuffle(doors) random.shuffle(targets) - while doors: - door = doors.pop() - target = targets.pop() + placing = min(len(doors), len(targets)) + for door, target in zip(doors, targets): connect_entrance(world, door, target, player) + doors[:] = doors[placing:] + targets[:] = targets[placing:] def skull_woods_shuffle(world, player): diff --git a/Fill.py b/Fill.py index 04278d43..d1905daf 100644 --- a/Fill.py +++ b/Fill.py @@ -1,4 +1,6 @@ import RaceRandom as random +import collections +import itertools import logging from BaseClasses import CollectionState @@ -491,9 +493,8 @@ def sell_keys(world, player): def balance_multiworld_progression(world): state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {} for player in range(1, world.players + 1): @@ -501,7 +502,7 @@ def balance_multiworld_progression(world): def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) - return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] + return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -512,38 +513,42 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] - if balancing_players is not None and len(balancing_players) > 0: + balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold} + if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() - candidate_items = [] + candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) - if location.item.player in balancing_players and not location.locked: - candidate_items.append(location) + player = location.item.player + if player in balancing_players and not location.locked and location.player != player: + candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): + if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') - unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] + unlocked_locations = collections.defaultdict(set) + for l in unchecked_locations: + if l not in balancing_unchecked_locations: + unlocked_locations[l.player].add(l) items_to_replace = [] for player in balancing_players: - locations_to_test = [l for l in unlocked_locations if l.player == player] - # only replace items that end up in another player's world - items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player] + locations_to_test = unlocked_locations[player] + items_to_test = candidate_items[player] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() - for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]: + for location in itertools.chain((l for l in items_to_replace if l.item.player == player), + items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events(locations=locations_to_test) @@ -557,33 +562,43 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + world.random.shuffle(replacement_locations) + items_to_replace.sort() + world.random.shuffle(items_to_replace) while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() + for new_location in replacement_locations: + if (new_location.can_fill(state, old_location.item, False) and + old_location.can_fill(state, new_location.item, False)): + replacement_locations.remove(new_location) + new_location.item, old_location.item = old_location.item, new_location.item + if world.shopsanity[new_location.player]: + check_shop_swap(new_location) + if world.shopsanity[old_location.player]: + check_shop_swap(old_location) + new_location.event, old_location.event = True, False + logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " + f"displacing {old_location.item} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") - while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)): - replacement_locations.insert(0, new_location) - new_location = replacement_locations.pop() - - new_location.item, old_location.item = old_location.item, new_location.item - if world.shopsanity[new_location.player]: - check_shop_swap(new_location) - if world.shopsanity[old_location.player]: - check_shop_swap(old_location) - new_location.event, old_location.event = True, False - state.collect(new_location.item, True, new_location) - replaced_items = True if replaced_items: - for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]): + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} + for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) for location in sphere_locations: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) - checked_locations.extend(sphere_locations) + checked_locations |= sphere_locations if world.has_beaten_game(state): break diff --git a/Main.py b/Main.py index b43d099a..719d0cc5 100644 --- a/Main.py +++ b/Main.py @@ -541,11 +541,11 @@ def create_playthrough(world): while sphere_candidates: state.sweep_for_events(key_only=True) - sphere = [] + sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location) and state.not_flooding_a_key(world, location): - sphere.append(location) + sphere.add(location) for location in sphere: sphere_candidates.remove(location) @@ -566,7 +566,7 @@ def create_playthrough(world): # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): - to_delete = [] + to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) @@ -576,15 +576,14 @@ def create_playthrough(world): world.clear_exp_cache() if world.can_beat_game(state_cache[num]): # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') - to_delete.append(location) + to_delete.add(location) else: # still required, got to keep it around # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end - for location in to_delete: - sphere.remove(location) + sphere -= to_delete # second phase, sphere 0 for item in [i for i in world.precollected_items if i.advancement]: @@ -600,7 +599,7 @@ def create_playthrough(world): # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres - required_locations = [item for sphere in collection_spheres for item in sphere] + required_locations = {item for sphere in collection_spheres for item in sphere} state = CollectionState(world) collection_spheres = [] while required_locations: @@ -637,7 +636,7 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - for _, path in dict(old_world.spoiler.paths).items(): + for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) @@ -645,6 +644,6 @@ def create_playthrough(world): old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])]) + old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} diff --git a/Rom.py b/Rom.py index 8d86a16e..564126fb 100644 --- a/Rom.py +++ b/Rom.py @@ -102,8 +102,7 @@ class LocalRom(object): self.buffer[address] = value def write_bytes(self, startaddress, values): - for i, value in enumerate(values): - self.write_byte(startaddress + i, value) + self.buffer[startaddress:startaddress + len(values)] = values def write_to_file(self, file): with open(file, 'wb') as outfile: From e10c765bc28a91b031920bddfe410e003550cfea Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 12:16:29 -0600 Subject: [PATCH 17/27] Key logic --- KeyDoorShuffle.py | 69 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index e7bcae11..7d550d1b 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -275,7 +275,7 @@ def analyze_dungeon(key_layout, world, player): # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): - if not child.bigKey or not key_layout.big_key_special or big_avail: + if can_open_door_by_counter(child, key_counter, key_layout, world, player): odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) empty_flag = empty_counter(odd_counter) child_queue.append((child, odd_counter, empty_flag)) @@ -460,7 +460,8 @@ def refine_placement_rules(key_layout, max_ctr): 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: + common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) + if (common_needed - common_locs) * 2 > key_layout.max_chests: key_logic.bk_restricted.update(rule_a.bk_conditional_set) rules_to_remove.append(rule_a) changed = True @@ -821,7 +822,7 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe def find_inverted_counter(door, parent_counter, key_layout, world, player): # open all doors in counter - counter = open_all_counter(parent_counter, key_layout, door=door) + counter = open_all_counter(parent_counter, key_layout, world, player, door=door) max_counter = find_max_counter(key_layout) # find the difference inverted_counter = KeyCounter(key_layout.max_chests) @@ -837,7 +838,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player): return inverted_counter -def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): +def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False): changed = True counter = parent_counter proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) @@ -849,13 +850,11 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): if skipBk: if not child.bigKey: doors_to_open[child] = None - elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened: + elif can_open_door_by_counter(child, counter, key_layout, world, player): doors_to_open[child] = None if len(doors_to_open.keys()) > 0: proposed_doors = {**proposed_doors, **doors_to_open} - bk_hint = counter.big_key_opened - for d in doors_to_open.keys(): - bk_hint = bk_hint or d.bigKey + bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys()) counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -1485,12 +1484,28 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player): return state.key_locations - state.used_smalls +def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): + if not world.keyshuffle[player] and not world.retro[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) + return layout.max_chests + len(counter.key_only_locations) - counter.used_keys + + def cnt_avail_big_locations(ttl_locations, state, world, player): if not world.bigkeyshuffle[player]: return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 return 1 if not state.big_key_special else 0 +def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player): + if not world.bigkeyshuffle[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj + return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0 + return 1 if not layout.big_key_special else 0 + + def create_key_counters(key_layout, world, player): key_counters = {} key_layout.found_doors.clear() @@ -1500,12 +1515,11 @@ def create_key_counters(key_layout, world, player): state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) else: state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num - state.big_key_special, special_region = False, None + state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: if location.forced_big_key(): state.big_key_special = True - special_region = region for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and @@ -1531,7 +1545,7 @@ def create_key_counters(key_layout, world, player): if door.bigKey or door.name in special_big_key_doors: key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): + if can_open_door(door, child_state, world, player): open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) @@ -1552,6 +1566,39 @@ def find_outside_connection(region): return None, None +def can_open_door(door, state, world, player): + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) + if door.smallKey: + ttl_small_key_only = count_small_key_only_locations(state) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + return available_small_locations > 0 + elif door.bigKey: + available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) + found_forced_bk = state.found_forced_bk() + return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk) + else: + return True + + +def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): + if counter.big_key_opened: + ttl_locations = len(counter.free_locations) + else: + ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name]) + + if door.smallKey: + # ttl_small_key_only = len(counter.key_only_locations) + return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 + elif door.bigKey: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special + else: + return True + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) From 4b29408911b61ec6ee4e04aaff7dcc13763868ca Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 13:49:46 -0600 Subject: [PATCH 18/27] Perf enhancement fixes --- Fill.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Fill.py b/Fill.py index d1905daf..9adb0916 100644 --- a/Fill.py +++ b/Fill.py @@ -564,10 +564,11 @@ def balance_multiworld_progression(world): replaced_items = False # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere - replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) - world.random.shuffle(replacement_locations) - items_to_replace.sort() - world.random.shuffle(items_to_replace) + replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked), + key=lambda loc: (loc.name, loc.player)) + random.shuffle(replacement_locations) + items_to_replace.sort(key=lambda item: (item.name, item.player)) + random.shuffle(items_to_replace) while replacement_locations and items_to_replace: old_location = items_to_replace.pop() for new_location in replacement_locations: From d7e326f71dba53d5d2acd0a5ad5b9128e2d18dea Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 4 Aug 2021 12:08:17 -0600 Subject: [PATCH 19/27] Key checking in dungeons limited to single player for speed Spoiler traversal fixes and speed Bug with boss shuffle spoiler in MW fixed Perf enhancement fix for pot shuffle --- BaseClasses.py | 16 ++++++++++------ Main.py | 7 +++++-- Rom.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 087bda1b..d6e0c5e2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -518,7 +518,8 @@ class CollectionState(object): connection, crystal_state = queue.popleft() new_region = connection.connected_region if not self.should_visit(new_region, rrp, crystal_state, player): - bc.pop(connection, None) + if not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): + bc.pop(connection, None) elif connection.can_reach(self): bc.pop(connection, None) if new_region.type == RegionType.Dungeon: @@ -643,9 +644,12 @@ class CollectionState(object): while not done: rrp_ = child_state.reachable_regions[player] bc_ = child_state.blocked_connections[player] - self.set_dungeon_limits(player, dungeon_name) + child_state.set_dungeon_limits(player, dungeon_name) + child_queue.extend([(x, y) for x, y in bc_.items() + if child_state.possibly_connected_to_dungeon(x.parent_region, + player)]) child_state.traverse_world(child_queue, rrp_, bc_, player) - new_events = child_state.sweep_for_events_once() + new_events = child_state.sweep_for_events_once(player) child_state.stale[player] = False if new_events: for conn in bc_: @@ -890,8 +894,8 @@ class CollectionState(object): return spot.can_reach(self) - def sweep_for_events_once(self): - locations = self.world.get_filled_locations() + def sweep_for_events_once(self, player): + locations = self.world.get_filled_locations(player) checked_locations = set([l for l in locations if l in self.locations_checked]) reachable_events = [location for location in locations if location.event and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) @@ -2520,7 +2524,7 @@ class Spoiler(object): for player in range(1, self.world.players + 1): if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[player] if self.world.players > 1 else self.bosses + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) diff --git a/Main.py b/Main.py index 719d0cc5..d8967f1f 100644 --- a/Main.py +++ b/Main.py @@ -573,7 +573,7 @@ def create_playthrough(world): old_item = location.item location.item = None # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic - world.clear_exp_cache() + # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) @@ -615,7 +615,10 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations)) if not sphere: - raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) + if world.has_beaten_game(state): + required_locations.clear() + else: + raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) # store the required locations for statistical analysis old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] diff --git a/Rom.py b/Rom.py index 564126fb..f74a385e 100644 --- a/Rom.py +++ b/Rom.py @@ -2795,7 +2795,7 @@ def write_pots_to_rom(rom, pot_contents): pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing] if len(pots) > 0: write_int16(rom, pot_item_room_table_lookup + 2*i, n) - rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots))) + rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots)))) n += 3*len(pots) + 2 rom.write_bytes(n - 2, [0xFF,0xFF]) else: From feb9177871b6015a7b7ee7f32159902dcc934722 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 4 Aug 2021 13:46:30 -0600 Subject: [PATCH 20/27] Ignore certain rules when bk isn't available in contradiction checking --- KeyDoorShuffle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7d550d1b..5dff33ef 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -127,6 +127,10 @@ class PlacementRule(object): 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 + if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply + return False + if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply + 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 From 7623de396c20063ce1a39306ef66dbd39d322f25 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 13:39:32 -0600 Subject: [PATCH 21/27] Fixed some bk special handling --- DungeonGenerator.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5174762e..3c64df89 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): proposed_map = {} doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) paths = determine_paths_for_dungeon(world, player, all_regions, builder.name) dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) @@ -106,14 +107,15 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) proposed_map = {} choices_master = [[]] depth = 0 @@ -187,8 +189,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon return proposed_map -def determine_if_bk_needed(sector, split_dungeon, world, player): - if not split_dungeon: +def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player): + if not split_dungeon or bk_special: for region in sector.regions: for ext in region.exits: door = world.check_for_door(ext.name, player) @@ -197,8 +199,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def check_for_special(sector): - for region in sector.regions: +def check_for_special(regions): + for region in regions: for loc in region.locations: if loc.forced_big_key(): return True @@ -417,6 +419,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a # origin has no more hooks, but not all doors have been proposed if not world.bigkeyshuffle[player]: possible_bks = len(dungeon['Origin'].possible_bk_locations) + if bk_special and check_for_special(dungeon['Origin'].visited_regions): + possible_bks = 1 true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed] if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect): return False @@ -450,7 +454,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special) for piece in dungeon.values(): all_visited.update(piece.visited_regions) - if not bk_possible and len(piece.possible_bk_locations) > 0: + if ((not bk_possible and len(piece.possible_bk_locations) > 0) or + (bk_special and check_for_special(piece.visited_regions))): bk_possible = True if len(all_regions.difference(all_visited)) > 0: return False From fd24c016e253dcb02efe1c654b438d56a30282b2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 15:59:01 -0600 Subject: [PATCH 22/27] Total keys fix for key logic (Basic reductions do not affect amount of keys placed while crossed do) --- DoorShuffle.py | 4 +++- DungeonGenerator.py | 1 + KeyDoorShuffle.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ca4b9139..3ed21895 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1082,6 +1082,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Step 3: Initial valid combination find - reduce flex if needed for name, builder in dungeon_builders.items(): suggested = builder.key_doors_num - builder.key_drop_cnt + builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt if actual_chest_keys < suggested: @@ -1098,6 +1099,7 @@ def assign_cross_keys(dungeon_builders, world, player): name = builder.name logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name) builder.key_doors_num += 1 + builder.total_keys = builder.key_doors_num result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False) if result: remaining -= 1 @@ -1391,7 +1393,7 @@ def shuffle_key_doors(builder, world, player): skips.append(world.get_door(dp.door_a, player)) break num_key_doors += 1 - builder.key_doors_num = num_key_doors + builder.key_doors_num = builder.total_keys = num_key_doors find_small_key_door_candidates(builder, start_regions, world, player) find_valid_combination(builder, start_regions, world, player) reassign_key_doors(builder, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 3c64df89..98908868 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1190,6 +1190,7 @@ class DungeonBuilder(object): self.pre_open_stonewalls = set() # used by stonewall system self.candidates = None + self.total_keys = None self.key_doors_num = None self.combo_size = None self.flex = 0 diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7829d92b..84ff1b38 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1519,7 +1519,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = default_key_counts[key_layout.sector.name] else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.key_doors_num - builder.key_drop_cnt + state.key_locations = builder.total_keys - builder.key_drop_cnt state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: From 678e5eb091ae02459a8be279e92502f7161044e0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 13 Aug 2021 14:41:04 -0600 Subject: [PATCH 23/27] Multi-entrance crystal doesn't propagate between dungeons Multi-entrance: don't use unreachable entrances for reachable stats --- DungeonGenerator.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 98908868..170233ce 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -102,8 +102,15 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon excluded = {} for region in entrance_regions: portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) - if portal and portal.destination: - excluded[region] = None + if portal: + if portal.destination: + excluded[region] = None + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if access_region.name in world.inaccessible_regions[player]: + excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() @@ -855,6 +862,8 @@ class ExplorationState(object): return exp_door def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): + if region.type != RegionType.Dungeon: + self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: if region not in self.visited_blue: self.visited_blue.append(region) From 54004118f5f874881b4a1820741f35f20e0a6b49 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 13 Aug 2021 14:41:35 -0600 Subject: [PATCH 24/27] Minor bug during exploration --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index d6e0c5e2..91384e46 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -518,7 +518,7 @@ class CollectionState(object): connection, crystal_state = queue.popleft() new_region = connection.connected_region if not self.should_visit(new_region, rrp, crystal_state, player): - if not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): + if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): bc.pop(connection, None) elif connection.can_reach(self): bc.pop(connection, None) From b53a005545d456c719e809a5bd37a8adacab1c09 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 16 Aug 2021 15:28:20 -0600 Subject: [PATCH 25/27] Fix for last ditch problems Special bk adjustments Exception for self locking key doors in key lock checker --- BaseClasses.py | 5 ++++- Fill.py | 8 ++++++-- KeyDoorShuffle.py | 26 +++++++++++++++++++++++++- Main.py | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 91384e46..84ff319d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -432,7 +432,7 @@ class World(object): else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state=None): + def can_beat_game(self, starting_state=None, log_error=False): if starting_state: if self.has_beaten_game(starting_state): return True @@ -456,6 +456,9 @@ class World(object): if not sphere: # ran out of places and did not finish yet, quit + if log_error: + missing_locations = ", ".join([x.name for x in prog_locations]) + logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}') return False for location in sphere: diff --git a/Fill.py b/Fill.py index 9adb0916..c42b6251 100644 --- a/Fill.py +++ b/Fill.py @@ -295,12 +295,14 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite if swap_spot: logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') world.push_item(swap_spot, old_item, False) + swap_spot.event = True locations.remove(swap_spot) locations.append(new_spot) return new_spot else: new_spot.item = restore_item - + else: + location.item = old_item return None @@ -315,10 +317,12 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, for location in locations: maximum_exploration_state = sweep_from_pool() perform_access_check = True + old_item = None if world.accessibility[item_to_place.player] == 'none': perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state) if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + old_item = location.item location.item = item_to_place test_state = maximum_exploration_state.copy() test_state.stale[item_to_place.player] = True @@ -329,7 +333,7 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): return location if item_to_place.smallkey or item_to_place.bigkey: - location.item = None + location.item = old_item return None diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 84ff1b38..4319d42b 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -116,6 +116,7 @@ class PlacementRule(object): self.needed_keys_w_bk = None self.needed_keys_wo_bk = None self.check_locations_w_bk = None + self.special_bk_avail = False self.check_locations_wo_bk = None self.bk_relevant = True self.key_reduced = False @@ -164,7 +165,10 @@ class PlacementRule(object): def loc_has_bk(l): return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) - bk_found = any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) + # todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid + # but sometimes it certainly does + # check threshold vs len(check_loc) maybe to determine bk isn't relevant? + bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) if not bk_found: return True check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk @@ -258,6 +262,8 @@ def analyze_dungeon(key_layout, world, player): return original_key_counter = find_counter({}, False, key_layout, False) + if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None: + original_key_counter = find_counter({}, True, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -340,6 +346,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc + if key_layout.big_key_special: + rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) else: if big_key_progress(key_counter) and only_sm_doors(key_counter): @@ -1359,6 +1367,13 @@ def check_bk_special(regions, world, player): return False +def forced_big_key_avail(locations): + for loc in locations: + if loc.forced_big_key(): + return loc + return None + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1962,6 +1977,7 @@ def validate_key_placement(key_layout, world, player): found_prize = False 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) or \ + self_locked_child_door(key_layout, counter) or \ (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) @@ -1976,3 +1992,11 @@ def validate_key_placement(key_layout, world, player): return True + +def self_locked_child_door(key_layout, counter): + if len(counter.child_doors) == 1: + door = next(iter(counter.child_doors.keys())) + return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small + return False + + diff --git a/Main.py b/Main.py index d8967f1f..34d35b25 100644 --- a/Main.py +++ b/Main.py @@ -244,7 +244,7 @@ def main(args, seed=None, fish=None): balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom - if not world.can_beat_game(): + if not world.can_beat_game(log_error=True): raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) for player in range(1, world.players+1): From 25bc968b6281500fdaaaa6d1bf401631ee7807e1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Aug 2021 16:15:05 -0600 Subject: [PATCH 26/27] Key counts for vanilla fixed --- Dungeons.py | 7 ------- KeyDoorShuffle.py | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Dungeons.py b/Dungeons.py index 75699434..73f53794 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,13 +375,6 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } -default_key_counts = { - 'Hyrule Castle': 1, 'Eastern Palace': 0, 'Desert Palace': 1, - 'Tower of Hera': 1, 'Agahnims Tower': 2, 'Palace of Darkness': 6, - 'Swamp Palace': 1, 'Skull Woods': 3, 'Thieves Town': 1, - 'Ice Palace': 2, 'Misery Mire': 3, 'Turtle Rock': 4, 'Ganons Tower': 4 -} - dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4319d42b..5cf88dc0 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize, default_key_counts +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize from DungeonGenerator import ExplorationState, special_big_key_doors @@ -1531,7 +1531,8 @@ def create_key_counters(key_layout, world, player): flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': - state.key_locations = default_key_counts[key_layout.sector.name] + builder = world.dungeon_layouts[player][key_layout.sector.name] + state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] state.key_locations = builder.total_keys - builder.key_drop_cnt From 4e91e627e1888ed24683755c72e6053d32709fe9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Aug 2021 16:21:42 -0600 Subject: [PATCH 27/27] Split dungeon refinement Prize relevance refinement --- DungeonGenerator.py | 30 +++++++++++++++++++++++++++++- KeyDoorShuffle.py | 9 +++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 170233ce..948115f3 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3320,6 +3320,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): possible_regions.add(portal.door.entrance.parent_region.name) if builder.name in dungeon_drops.keys(): possible_regions.update(dungeon_drops[builder.name]) + independents = find_independent_entrances(possible_regions, world, player) for name, split_build in builder.split_dungeon_map.items(): name_bits = name.split(" ") orig_name = " ".join(name_bits[:-1]) @@ -3332,7 +3333,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): if r_name not in entrance_regions: entrance_regions.append(r_name) # entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid] - proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player) + split = any(x for x in independents if x not in entrance_regions) + proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player) # record split proposals builder.valid_proposal[name] = proposal builder.exception_list = list(sector_list) @@ -3347,6 +3349,32 @@ def check_for_valid_layout(builder, sector_list, builder_info): return len(unreached_doors) == 0, unreached_doors +def find_independent_entrances(entrance_regions, world, player): + independents = set() + for region in entrance_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None) + if portal: + if portal.destination: + continue + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if access_region.name in world.inaccessible_regions[player]: + continue + else: + r = world.get_region(region, player) + access_region = next(x.parent_region for x in r.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop') + if access_region.name == 'Sewer Drop': + access_region = next(x.parent_region for x in access_region.entrances) + if access_region.name in world.inaccessible_regions[player]: + continue + independents.add(region) + return independents + + def resolve_equations(builder, sector_list): unreached_doors = defaultdict(list) equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0} diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5cf88dc0..cf18f4f4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1385,10 +1385,12 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (key_layout.prize_relevant and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']): + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] + and key_layout.key_logic.dungeon in dungeon_prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1419,6 +1421,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_done and bk_done: return False else: + # todo: pretty sure you should OR these paths together, maybe when there's one location and it can + # either be small or big key if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() @@ -1449,6 +1453,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa valid = checked_states[code] if not valid: return False + # todo: feel like you only open these if the boss is available??? if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)