From f15832f85ece904d29f7ea288e9a2317a46c0f9f Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Jul 2021 16:00:47 -0600 Subject: [PATCH] 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