From c933fa7e34a1c92ede81bfe25d8809aa98935131 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 4 Nov 2022 09:56:47 -0700 Subject: [PATCH] Core: optimize early items and add unit test (#1197) * optimize early items and add unit test * move sorting list init closer to sorting --- Fill.py | 45 ++++++++++++++++++++++----------- test/general/TestFill.py | 54 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/Fill.py b/Fill.py index 15af5a9e..b9888962 100644 --- a/Fill.py +++ b/Fill.py @@ -248,16 +248,10 @@ def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locat add_item_rule(location, forbid_important_item_rule) -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] = [] - usefulitempool: typing.List[Item] = [] - filleritempool: typing.List[Item] = [] - +def distribute_early_items(world: MultiWorld, + fill_locations: typing.List[Location], + itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]: + """ returns new fill_locations and itempool """ early_items_count: typing.Dict[typing.Tuple[str, int], int] = {} for player in world.player_ids: for item, count in world.early_items[player].value.items(): @@ -265,26 +259,32 @@ def distribute_items_restrictive(world: MultiWorld) -> None: if early_items_count: early_locations: typing.List[Location] = [] early_priority_locations: typing.List[Location] = [] - for loc in reversed(fill_locations): + loc_indexes_to_remove: typing.Set[int] = set() + for i, loc in enumerate(fill_locations): if loc.can_reach(world.state): if loc.progress_type == LocationProgressType.PRIORITY: early_priority_locations.append(loc) else: early_locations.append(loc) - fill_locations.remove(loc) + loc_indexes_to_remove.add(i) + fill_locations = [loc for i, loc in enumerate(fill_locations) if i not in loc_indexes_to_remove] early_prog_items: typing.List[Item] = [] early_rest_items: typing.List[Item] = [] - for item in reversed(itempool): + item_indexes_to_remove: typing.Set[int] = set() + for i, item in enumerate(itempool): if (item.name, item.player) in early_items_count: if item.advancement: early_prog_items.append(item) else: early_rest_items.append(item) - itempool.remove(item) + item_indexes_to_remove.add(i) early_items_count[(item.name, item.player)] -= 1 if early_items_count[(item.name, item.player)] == 0: del early_items_count[(item.name, item.player)] + if len(early_items_count) == 0: + break + itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove] fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True) early_locations += early_priority_locations fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True) @@ -294,8 +294,23 @@ def distribute_items_restrictive(world: MultiWorld) -> None: {len(unplaced_early_items)} items early.") itempool += unplaced_early_items - fill_locations += early_locations + fill_locations.extend(early_locations) world.random.shuffle(fill_locations) + return fill_locations, itempool + + +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) + + fill_locations, itempool = distribute_early_items(world, fill_locations, itempool) + + progitempool: typing.List[Item] = [] + usefulitempool: typing.List[Item] = [] + filleritempool: typing.List[Item] = [] for item in itempool: if item.advancement: diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 646fc3c8..c5130eed 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,7 +1,8 @@ from typing import List, Iterable import unittest from worlds.AutoWorld import World -from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive +from Fill import FillError, balance_multiworld_progression, fill_restrictive, \ + distribute_early_items, distribute_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location, \ ItemClassification from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule @@ -13,7 +14,7 @@ def generate_multi_world(players: int = 1) -> MultiWorld: for i in range(players): player_id = i+1 world = World(multi_world, player_id) - multi_world.game[player_id] = world + multi_world.game[player_id] = f"Game {player_id}" multi_world.worlds[player_id] = world multi_world.player_name[player_id] = "Test Player " + str(player_id) region = Region("Menu", RegionType.Generic, @@ -623,6 +624,55 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertEqual(item.player, item.location.player) self.assertFalse(item.location.event, False) + def test_early_items(self) -> None: + mw = generate_multi_world(2) + player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5) + player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5) + mw.early_items[1].value[player1.basic_items[0].name] = 1 + mw.early_items[2].value[player2.basic_items[2].name] = 1 + mw.early_items[2].value[player2.basic_items[3].name] = 1 + + early_items = [ + player1.basic_items[0], + player2.basic_items[2], + player2.basic_items[3], + ] + + # copied this code from the beginning of `distribute_items_restrictive` + # before `distribute_early_items` is called + fill_locations = sorted(mw.get_unfilled_locations()) + mw.random.shuffle(fill_locations) + itempool = sorted(mw.itempool) + mw.random.shuffle(itempool) + + fill_locations, itempool = distribute_early_items(mw, fill_locations, itempool) + + remaining_p1 = [item for item in itempool if item.player == 1] + remaining_p2 = [item for item in itempool if item.player == 2] + + assert len(itempool) == 7, f"number of items remaining after early_items: {len(itempool)}" + assert len(remaining_p1) == 4, f"number of p1 items after early_items: {len(remaining_p1)}" + assert len(remaining_p2) == 3, f"number of p2 items after early_items: {len(remaining_p1)}" + for i in range(5): + if i != 0: + assert player1.basic_items[i] in itempool, "non-early item to remain in itempool" + if i not in {2, 3}: + assert player2.basic_items[i] in itempool, "non-early item to remain in itempool" + for item in early_items: + assert item not in itempool, "early item to be taken out of itempool" + + assert len(fill_locations) == len(mw.get_locations()) - len(early_items), \ + f"early location count from {mw.get_locations()} to {len(fill_locations)} " \ + f"after {len(early_items)} early items" + + items_in_locations = {loc.item for loc in mw.get_locations() if loc.item} + + assert len(items_in_locations) == len(early_items), \ + f"{len(early_items)} early items in {len(items_in_locations)} locations" + + for item in early_items: + assert item in items_in_locations, "early item to be placed in location" + class TestBalanceMultiworldProgression(unittest.TestCase): def assertRegionContains(self, region: Region, item: Item) -> bool: