From f9f386fa1948f0cb7348df2690d61792caceaa18 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 15 Jul 2025 19:33:24 +0100 Subject: [PATCH] Core: Cache previous swap states to use as the base state to sweep from (#3859) The previous swap_state can often be used as the base state to create the next swap_state. This previous swap_state will already have collected all items in item_pool and is likely to have checked many locations, meaning that creating the next swap_state from it instead of from base_state is faster. From generating with extra code to raise an exception if more than 2 previous swap states were used, and using A Hat in Time and Pokemon Red/Blue yamls that often result in lots of swapping in progression fill, I could not get a single seed go through more than 2 previous swap states. A few worlds' pre-fills do often use more than 2 previous swap states, notably LADX which sometimes goes through over 20. Given a 20 player Pokemon Red/Blue multiworld that usually generates in around 16 or 17 seconds, but on a specific seed that results in 56 swaps, generation went from about 260 seconds before this patch to about 104 seconds after this patch (generated with a meta.yaml to disable progression balancing and `python -O Generate.py --skip_output`). Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index d4df9fdd..94904f4f 100644 --- a/Fill.py +++ b/Fill.py @@ -116,6 +116,13 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati else: # we filled all reachable spots. if swap: + # Keep a cache of previous safe swap states that might be usable to sweep from to produce the next + # swap state, instead of sweeping from `base_state` each time. + previous_safe_swap_state_cache: typing.Deque[CollectionState] = deque() + # Almost never are more than 2 states needed. The rare cases that do are usually highly restrictive + # single_player_placement=True pre-fills which can go through more than 10 states in some seeds. + max_swap_base_state_cache_length = 3 + # try swapping this item with previously placed items in a safe way then in an unsafe way swap_attempts = ((i, location, unsafe) for unsafe in (False, True) @@ -130,9 +137,30 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati location.item = None placed_item.location = None - swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool, - multiworld.get_filled_locations(item.player) - if single_player_placement else None) + + for previous_safe_swap_state in previous_safe_swap_state_cache: + # If a state has already checked the location of the swap, then it cannot be used. + if location not in previous_safe_swap_state.advancements: + # Previous swap states will have collected all items in `item_pool`, so the new + # `swap_state` can skip having to collect them again. + # Previous swap states will also have already checked many locations, making the sweep + # faster. + swap_state = sweep_from_pool(previous_safe_swap_state, (placed_item,) if unsafe else (), + multiworld.get_filled_locations(item.player) + if single_player_placement else None) + break + else: + # No previous swap_state was usable as a base state to sweep from, so create a new one. + swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool, + multiworld.get_filled_locations(item.player) + if single_player_placement else None) + # Unsafe states should not be added to the cache because they have collected `placed_item`. + if not unsafe: + if len(previous_safe_swap_state_cache) >= max_swap_base_state_cache_length: + # Remove the oldest cached state. + previous_safe_swap_state_cache.pop() + # Add the new state to the start of the cache. + previous_safe_swap_state_cache.appendleft(swap_state) # unsafe means swap_state assumes we can somehow collect placed_item before item_to_place # by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic # to clean that up later, so there is a chance generation fails.