diff --git a/BaseClasses.py b/BaseClasses.py index ec04d679..c2da8e91 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1423,10 +1423,10 @@ class Region(object): or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) or (item.compass and not self.world.compassshuffle[item.player])) - sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' - if sewer_hack or inside_dungeon_item: + # not all small keys to escape must be in escape + # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' + if inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player - return True def __str__(self): diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2140520e..8231833b 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -1002,7 +1009,8 @@ class ExplorationState(object): if self.can_traverse(door): if door.controller: door = door.controller - if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened: + if (door in big_key_door_proposal + or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened: if not self.in_door_list(door, self.big_doors): self.append_door_to_list(door, self.big_doors) elif door.req_event is not None and door.req_event not in self.events: @@ -3554,7 +3562,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) return True, {}, package - except (GenerationException, NeutralizingException, OtherGenException): + except (GenerationException, NeutralizingException, OtherGenException) as e: + logging.getLogger('').info(f'Bailing on this layout for', e) builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: diff --git a/Fill.py b/Fill.py index 321e1a8e..239d8e8e 100644 --- a/Fill.py +++ b/Fill.py @@ -143,21 +143,24 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl key_pool, world): 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 + location.event = True test_state = max_exp_state.copy() test_state.stale[item_to_place.player] = True else: test_state = max_exp_state if not single_player_placement or location.player == item_to_place.player: + test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): - if valid_key_placement(item_to_place, location, key_pool, world): + if valid_key_placement(item_to_place, location, key_pool, test_state, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = None + location.event = False return None -def valid_key_placement(item, location, key_pool, world): +def valid_key_placement(item, location, key_pool, collection_state, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player @@ -174,7 +177,15 @@ def valid_key_placement(item, location, key_pool, world): prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] wild_keys = world.keyshuffle[item.player] != 'none' - return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) + if wild_keys: + reached_keys = {x for x in collection_state.locations_checked + if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player} + else: + reached_keys = set() # will be calculated using key logic in a moment + self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small + and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) + return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -205,6 +216,7 @@ def track_outside_keys(item, location, world): if loc_dungeon and loc_dungeon.name == item_dungeon: return # this is an inside key world.key_logic[item.player][item_dungeon].outside_keys += 1 + world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location) def track_dungeon_items(item, location, world): @@ -345,7 +357,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, 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): + and valid_key_placement(item_to_place, location, + pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, + test_state, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = old_item @@ -424,10 +438,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) fill_locations.reverse() - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots - # todo: crossed - progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' - and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0) + # Make sure the escape keys ire placed first in standard to prevent running out of spots + def std_item_sort(item): + if world.mode[item.player] == 'standard': + if item.name == 'Small Key (Escape)': + return 1 + if item.name == 'Big Key (Escape)': + return 2 + return 0 + + progitempool.sort(key=std_item_sort) key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7b5e011f..2a39f38c 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,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_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -59,13 +59,16 @@ class KeyLogic(object): self.placement_rules = [] self.location_rules = {} self.outside_keys = 0 + self.outside_keys_locations = set() self.dungeon = dungeon_name self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys, + big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys, + unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -159,7 +162,8 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys, + big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: @@ -186,10 +190,11 @@ class PlacementRule(object): 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 - available_keys = outside_keys + available_keys = len(outside_keys_locations) # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk + threshold -= self_locking_keys if not wild_keys: empty_chests = 0 for loc in check_locations: @@ -200,7 +205,8 @@ class PlacementRule(object): place_able_keys = min(empty_chests, unplaced_keys) available_keys += place_able_keys else: - available_keys += unplaced_keys + available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys + available_keys += unplaced_keys # small keys not yet placed return available_keys >= threshold @@ -1002,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c def open_a_door(door, child_state, flat_proposal, world, player): - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): 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])) @@ -1485,7 +1491,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, 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_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False @@ -1615,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player): key_layout.prize_can_lock = True -def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if world.keyshuffle[player] == 'none': +def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player): + std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) return state.key_locations - state.used_smalls def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if world.keyshuffle[player] == 'none': + std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: 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) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - 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 @@ -1683,10 +1696,10 @@ def create_key_counters(key_layout, world, player): 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: + if door.bigKey or door.name in get_special_big_key_doors(world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if can_open_door(door, child_state, world, player): + if can_open_door(door, child_state, key_layout, 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) @@ -1707,14 +1720,15 @@ def find_outside_connection(region): return None, None -def can_open_door(door, state, world, player): +def can_open_door(door, state, key_layout, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, 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) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) return available_small_locations > 0 elif door.bigKey: available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) diff --git a/Rom.py b/Rom.py index 54d801bd..3e353b3a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '4eeafc915838a7c9c5eff7a1b53d4709' +RANDOMIZERBASEHASH = '6f64fcea052e37b39d6b4bb24ae2f548' class JsonRom(object): diff --git a/Rules.py b/Rules.py index 4bf9cd83..a550a9d4 100644 --- a/Rules.py +++ b/Rules.py @@ -1288,7 +1288,7 @@ def standard_rules(world, player): # zelda should be saved before agahnim is in play add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) - # too restrictive for crossed? + # uncle can't have keys generally because unplaced items aren't used here def uncle_item_rule(item): copy_state = CollectionState(world) copy_state.collect(item) diff --git a/data/base2current.bps b/data/base2current.bps index 3afa29d0..93e4cfd2 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index d0b21d89..8052b728 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -320,7 +320,10 @@ def determine_paths_for_dungeon(world, player, all_regions, name): paths.append('Hyrule Dungeon Cellblock') paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room')) entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South') + # todo: in non-er, we can use the other portals too paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name)) + paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name, + 'Hyrule Dungeon Cellblock'])) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') elif 'Thieves Attic Window' in all_r_names: @@ -434,6 +437,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -674,7 +684,7 @@ class ExplorationState(object): if door in key_door_proposal and door not in self.opened_doors: if not self.in_door_list(door, self.small_doors): self.append_door_to_list(door, self.small_doors) - elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened: + elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened: if not self.in_door_list(door, self.big_doors): self.append_door_to_list(door, self.big_doors) elif door.req_event is not None and door.req_event not in self.events: