From 6671b21a869ffbc6697108ee19c5a6915044d0c2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Mar 2023 17:10:12 +0100 Subject: [PATCH] Core: Generic excluded fill (#1511) --- Fill.py | 41 +++++++++++++++++++++++++++++++++------- worlds/alttp/Dungeons.py | 11 +++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index 92b57af5..ef84a23b 100644 --- a/Fill.py +++ b/Fill.py @@ -23,15 +23,27 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], - itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, + item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False) -> None: + allow_partial: bool = False, allow_excluded: bool = False) -> None: + """ + :param world: Multiworld to be filled. + :param base_state: State assumed before fill. + :param locations: Locations to be filled with item_pool + :param item_pool: Items to fill into the locations + :param single_player_placement: if true, can speed up placement if everything belongs to a single player + :param lock: locations are set to locked as they are filled + :param swap: if true, swaps of already place items are done in the event of a dead end + :param on_place: callback that is called when a placement happens + :param allow_partial: only place what is possible. Remaining items will be in the item_pool list. + :param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations + """ unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} - for item in itempool: + for item in item_pool: reachable_items.setdefault(item.player, deque()).append(item) while any(reachable_items.values()) and locations: @@ -39,9 +51,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: items_to_place = [items.pop() for items in reachable_items.values() if items] for item in items_to_place: - itempool.remove(item) + item_pool.remove(item) maximum_exploration_state = sweep_from_pool( - base_state, itempool + unplaced_items) + base_state, item_pool + unplaced_items) has_beaten_game = world.has_beaten_game(maximum_exploration_state) @@ -111,7 +123,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: reachable_items[placed_item.player].appendleft( placed_item) - itempool.append(placed_item) + item_pool.append(placed_item) break @@ -133,6 +145,21 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: if on_place: on_place(spot_to_fill) + if allow_excluded: + # check if partial fill is the result of excluded locations, in which case retry + excluded_locations = [ + location for location in locations + if location.progress_type == location.progress_type.EXCLUDED and not location.item + ] + if excluded_locations: + for location in excluded_locations: + location.progress_type = location.progress_type.DEFAULT + fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock, + swap, on_place, allow_partial, False) + for location in excluded_locations: + if not location.item: + location.progress_type = location.progress_type.EXCLUDED + if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them if world.can_beat_game(): @@ -142,7 +169,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') - itempool.extend(unplaced_items) + item_pool.extend(unplaced_items) def remaining_fill(world: MultiWorld, diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a37ded8d..ec6862b9 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -7,6 +7,8 @@ from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops from worlds.alttp.Options import smallkey_shuffle +if typing.TYPE_CHECKING: + from .SubClasses import ALttPLocation def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): @@ -138,9 +140,10 @@ def fill_dungeons_restrictive(world): if in_dungeon_items: restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted} - locations = [location for location in get_unfilled_dungeon_locations(world) - # filter boss - if not (location.player in restricted_players and location.name in lookup_boss_drops)] + locations: typing.List["ALttPLocation"] = [ + location for location in get_unfilled_dungeon_locations(world) + # filter boss + if not (location.player in restricted_players and location.name in lookup_boss_drops)] if dungeon_specific: for location in locations: dungeon = location.parent_region.dungeon @@ -159,7 +162,7 @@ def fill_dungeons_restrictive(world): (5 if (item.player, item.name) in dungeon_specific else 0)) for item in in_dungeon_items: all_state_base.remove(item) - fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True) + fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],