From 9daa64741baa0a5e305f3d7919fffaea61333577 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 16 Sep 2022 20:06:25 -0400 Subject: [PATCH] New, smarter fast_fill function (#646) Co-authored-by: Fabian Dill --- Fill.py | 150 ++++++++++++++++++++++----------------- test/general/TestFill.py | 8 +-- worlds/AutoWorld.py | 6 +- worlds/alttp/__init__.py | 18 +---- worlds/sa2b/__init__.py | 3 +- worlds/sm/__init__.py | 3 +- 6 files changed, 97 insertions(+), 91 deletions(-) diff --git a/Fill.py b/Fill.py index e44c80e7..c62eaabd 100644 --- a/Fill.py +++ b/Fill.py @@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: itempool.extend(unplaced_items) +def remaining_fill(world: MultiWorld, + locations: typing.List[Location], + itempool: typing.List[Item]) -> None: + unplaced_items: typing.List[Item] = [] + placements: typing.List[Location] = [] + swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() + while locations and itempool: + item_to_place = itempool.pop() + spot_to_fill: typing.Optional[Location] = None + + for i, location in enumerate(locations): + if location.item_rule(item_to_place): + # popping by index is faster than removing by content, + spot_to_fill = locations.pop(i) + # skipping a scan for the element + break + + else: + # we filled all reachable spots. + # try swapping this item with previously placed items + + for (i, location) in enumerate(placements): + placed_item = location.item + # Unplaceable items can sometimes be swapped infinitely. Limit the + # number of times we will swap an individual item to prevent this + + if swapped_items[placed_item.player, + placed_item.name] > 1: + continue + + location.item = None + placed_item.location = None + if location.item_rule(item_to_place): + # Add this item to the existing placement, and + # add the old item to the back of the queue + spot_to_fill = placements.pop(i) + + swapped_items[placed_item.player, + placed_item.name] += 1 + + itempool.append(placed_item) + + break + + # Item can't be placed here, restore original item + location.item = placed_item + placed_item.location = location + + if spot_to_fill is None: + # Can't place this item, move on to the next + unplaced_items.append(item_to_place) + continue + + world.push_item(spot_to_fill, item_to_place, False) + placements.append(spot_to_fill) + + if unplaced_items and locations: + # There are leftover unplaceable items and locations that won't accept them + 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) + + +def fast_fill(world: MultiWorld, + item_pool: typing.List[Item], + fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: + placing = min(len(item_pool), len(fill_locations)) + for item, location in zip(item_pool, fill_locations): + world.push_item(location, item, False) + return item_pool[placing:], fill_locations[placing:] + + def distribute_items_restrictive(world: MultiWorld) -> None: fill_locations = sorted(world.get_unfilled_locations()) world.random.shuffle(fill_locations) - # get items to distribute itempool = sorted(world.itempool) world.random.shuffle(itempool) progitempool: typing.List[Item] = [] - nonexcludeditempool: typing.List[Item] = [] - localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)} - nonlocalrestitempool: typing.List[Item] = [] - restitempool: typing.List[Item] = [] + usefulitempool: typing.List[Item] = [] + filleritempool: typing.List[Item] = [] for item in itempool: if item.advancement: progitempool.append(item) - elif item.useful: # this only gets nonprogression items which should not appear in excluded locations - nonexcludeditempool.append(item) - elif item.name in world.local_items[item.player].value: - localrestitempool[item.player].append(item) - elif item.name in world.non_local_items[item.player].value: - nonlocalrestitempool.append(item) + elif item.useful: + usefulitempool.append(item) else: - restitempool.append(item) + filleritempool.append(item) - call_all(world, "fill_hook", progitempool, nonexcludeditempool, - localrestitempool, nonlocalrestitempool, restitempool, fill_locations) + call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations) locations: typing.Dict[LocationProgressType, typing.List[Location]] = { loc_type: [] for loc_type in LocationProgressType} @@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') - if nonexcludeditempool: - world.random.shuffle(defaultlocations) - # needs logical fill to not conflict with local items - fill_restrictive( - world, world.state, defaultlocations, nonexcludeditempool) - if nonexcludeditempool: - raise FillError( - f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations') + remaining_fill(world, excludedlocations, filleritempool) + if excludedlocations: + raise FillError( + f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") - defaultlocations = defaultlocations + excludedlocations - world.random.shuffle(defaultlocations) + restitempool = usefulitempool + filleritempool - if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds - local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids} - for location in defaultlocations: - local_locations[location.player].append(location) - for player_locations in local_locations.values(): - world.random.shuffle(player_locations) + remaining_fill(world, defaultlocations, restitempool) - for player, items in localrestitempool.items(): # items already shuffled - player_local_locations = local_locations[player] - for item_to_place in items: - if not player_local_locations: - logging.warning(f"Ran out of local locations for player {player}, " - f"cannot place {item_to_place}.") - break - spot_to_fill = player_local_locations.pop() - world.push_item(spot_to_fill, item_to_place, False) - defaultlocations.remove(spot_to_fill) - - for item_to_place in nonlocalrestitempool: - for i, location in enumerate(defaultlocations): - if location.player != item_to_place.player: - world.push_item(defaultlocations.pop(i), item_to_place, False) - break - else: - raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. " - f"Too many non-local items for too few remaining locations.") - - world.random.shuffle(defaultlocations) - - restitempool, defaultlocations = fast_fill( - world, restitempool, defaultlocations) - unplaced = progitempool + restitempool + unplaced = restitempool unfilled = defaultlocations if unplaced or unfilled: @@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None: logging.info(f'Per-Player counts: {print_data})') -def fast_fill(world: MultiWorld, - item_pool: typing.List[Item], - fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: - placing = min(len(item_pool), len(fill_locations)) - for item, location in zip(item_pool, fill_locations): - world.push_item(location, item, False) - return item_pool[placing:], fill_locations[placing:] - - def flood_items(world: MultiWorld) -> None: # get items to distribute world.random.shuffle(world.itempool) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 189aafaf..8ce5b3b2 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase): distribute_items_restrictive(multi_world) - self.assertEqual(locations[0].item, basic_items[0]) + self.assertEqual(locations[0].item, basic_items[1]) self.assertFalse(locations[0].event) self.assertEqual(locations[1].item, prog_items[0]) self.assertTrue(locations[1].event) self.assertEqual(locations[2].item, prog_items[1]) self.assertTrue(locations[2].event) - self.assertEqual(locations[3].item, basic_items[1]) + self.assertEqual(locations[3].item, basic_items[0]) self.assertFalse(locations[3].event) def test_excluded_distribute(self): @@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase): removed_item: list[Item] = [] removed_location: list[Location] = [] - def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations): - removed_item.append(restitempool.pop(0)) + def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations): + removed_item.append(filleritempool.pop(0)) removed_location.append(fill_locations.pop(0)) multi_world.worlds[player1.id].fill_hook = fill_hook diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 8d9a1b08..959bc858 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -221,10 +221,8 @@ class World(metaclass=AutoWorldRegister): @classmethod def fill_hook(cls, progitempool: List["Item"], - nonexcludeditempool: List["Item"], - localrestitempool: Dict[int, List["Item"]], - nonlocalrestitempool: Dict[int, List["Item"]], - restitempool: List["Item"], + usefulitempool: List["Item"], + filleritempool: List["Item"], fill_locations: List["Location"]) -> None: """Special method that gets called as part of distribute_items_restrictive (main fill). This gets called once per present world type.""" diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index abb1f0a9..cb66ac4f 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -424,8 +424,7 @@ class ALTTPWorld(World): return ALttPItem(name, self.player, **item_init_table[name]) @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): trash_counts = {} standard_keyshuffle_players = set() for player in world.get_game_players("A Link to the Past"): @@ -472,26 +471,15 @@ class ALTTPWorld(World): for player, trash_count in trash_counts.items(): gtower_locations = locations_mapping[player] world.random.shuffle(gtower_locations) - localrest = localrestitempool[player] - if localrest: - gt_item_pool = restitempool + localrest - world.random.shuffle(gt_item_pool) - else: - gt_item_pool = restitempool.copy() - while gtower_locations and gt_item_pool and trash_count > 0: + while gtower_locations and filleritempool and trash_count > 0: spot_to_fill = gtower_locations.pop() - item_to_place = gt_item_pool.pop() + item_to_place = filleritempool.pop() if spot_to_fill.item_rule(item_to_place): - if item_to_place in localrest: - localrest.remove(item_to_place) - else: - restitempool.remove(item_to_place) world.push_item(spot_to_fill, item_to_place, False) fill_locations.remove(spot_to_fill) # very slow, unfortunately trash_count -= 1 - def get_filler_item_name(self) -> str: if self.world.goal[self.player] == "icerodhunt": item = "Nothing" diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index e9b75bbe..ea248095 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -282,8 +282,7 @@ class SA2BWorld(World): spoiler_handle.writelines(text) @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): if world.get_game_players("Sonic Adventure 2 Battle"): progitempool.sort( key=lambda item: 0 if (item.name != 'Emblem') else 1) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index fbf3825e..0bf12ca7 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -660,8 +660,7 @@ class SMWorld(World): loc.address = loc.item.code = None @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): if world.get_game_players("Super Metroid"): progitempool.sort( key=lambda item: 1 if (item.name == 'Morph Ball') else 0)