From ec95ce832906579801d929fffce954e2ca6b6c45 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Wed, 19 Jan 2022 20:19:07 -0700 Subject: [PATCH] Allow locations to be prioritized for progress item placement (#189) --- BaseClasses.py | 8 +- Fill.py | 78 +++++++--- test/general/TestFill.py | 327 ++++++++++++++++++++++++++++++++++++--- worlds/generic/Rules.py | 3 + 4 files changed, 371 insertions(+), 45 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 446b1055..d49afd1c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -787,7 +787,7 @@ class Region(object): self.type = type_ self.entrances = [] self.exits = [] - self.locations = [] + self.locations: List[Location] = [] self.dungeon = None self.shop = None self.world = world @@ -908,6 +908,11 @@ class Boss(): return f"Boss({self.name})" +class LocationProgressType(Enum): + DEFAULT = 1 + PRIORITY = 2 + EXCLUDED = 3 + class Location(): # If given as integer, then this is the shop's inventory index shop_slot: Optional[int] = None @@ -919,6 +924,7 @@ class Location(): show_in_spoiler: bool = True excluded: bool = False crystal: bool = False + progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) access_rule = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) diff --git a/Fill.py b/Fill.py index eb0a7d47..2e8e5011 100644 --- a/Fill.py +++ b/Fill.py @@ -5,7 +5,7 @@ import itertools from collections import Counter, deque -from BaseClasses import CollectionState, Location, MultiWorld, Item +from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item from worlds.generic import PlandoItem from worlds.AutoWorld import call_all @@ -106,8 +106,19 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, def distribute_items_restrictive(world: MultiWorld, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: - fill_locations = world.get_unfilled_locations() - world.random.shuffle(fill_locations) + fill_locations = world.get_locations() + + world.random.shuffle(fill_locations) + + locations: dict[LocationProgressType, list[Location]] = { + type: [] for type in LocationProgressType} + + for loc in fill_locations: + locations[loc.progress_type].append(loc) + + prioritylocations = locations[LocationProgressType.PRIORITY] + defaultlocations = locations[LocationProgressType.DEFAULT] + excludedlocations = locations[LocationProgressType.EXCLUDED] # get items to distribute world.random.shuffle(world.itempool) @@ -129,21 +140,42 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None): else: restitempool.append(item) - world.random.shuffle(fill_locations) - call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations) + call_all(world, "fill_hook", progitempool, nonexcludeditempool, + localrestitempool, nonlocalrestitempool, restitempool, fill_locations) - fill_restrictive(world, world.state, fill_locations, progitempool) + locationDeficit = len(progitempool) - len(prioritylocations) + if locationDeficit > 0: + if locationDeficit > len(defaultlocations): + raise FillError( + f'Not enough locations for advancement items. There are {len(progitempool)} advancement items with {len(prioritylocations)} priority locations and {len(defaultlocations)} default locations') + prioritylocations += defaultlocations[:locationDeficit] + defaultlocations = defaultlocations[locationDeficit:] + + fill_restrictive(world, world.state, prioritylocations, progitempool) + if prioritylocations: + defaultlocations = prioritylocations + defaultlocations + + if progitempool: + fill_restrictive(world, world.state, defaultlocations, progitempool) if nonexcludeditempool: - world.random.shuffle(fill_locations) - fill_restrictive(world, world.state, fill_locations, nonexcludeditempool) # needs logical fill to not conflict with local items + world.random.shuffle(defaultlocations) + # needs logical fill to not conflict with local items + nonexcludeditempool, defaultlocations = fast_fill( + world, nonexcludeditempool, defaultlocations) + if(len(nonexcludeditempool) > 0): + raise FillError( + f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations') + + defaultlocations = defaultlocations + excludedlocations + world.random.shuffle(defaultlocations) if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds local_locations = {player: [] for player in world.player_ids} - for location in fill_locations: + for location in defaultlocations: local_locations[location.player].append(location) - for locations in local_locations.values(): - world.random.shuffle(locations) + for player_locations in local_locations.values(): + world.random.shuffle(player_locations) for player, items in localrestitempool.items(): # items already shuffled player_local_locations = local_locations[player] @@ -154,24 +186,27 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None): break spot_to_fill = player_local_locations.pop() world.push_item(spot_to_fill, item_to_place, False) - fill_locations.remove(spot_to_fill) + defaultlocations.remove(spot_to_fill) for item_to_place in nonlocalrestitempool: - for i, location in enumerate(fill_locations): + for i, location in enumerate(defaultlocations): if location.player != item_to_place.player: - world.push_item(fill_locations.pop(i), item_to_place, False) + world.push_item(defaultlocations.pop(i), item_to_place, False) break else: - logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.") + logging.warning( + f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.") - world.random.shuffle(fill_locations) + world.random.shuffle(defaultlocations) - restitempool, fill_locations = fast_fill(world, restitempool, fill_locations) + restitempool, defaultlocations = fast_fill( + world, restitempool, defaultlocations) unplaced = progitempool + restitempool - unfilled = [location.name for location in fill_locations] + unfilled = [location.name for location in defaultlocations] if unplaced or unfilled: - logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') + logging.warning( + f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]: @@ -279,7 +314,10 @@ def balance_multiworld_progression(world: MultiWorld): balancing_state.collect(location.item, True, location) player = location.item.player # only replace items that end up in another player's world - if not location.locked and player in balancing_players and location.player != player: + if(not location.locked and + player in balancing_players and + location.player != player and + location.progress_type != LocationProgressType.PRIORITY): candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 573a224f..1dcba44c 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,9 +1,9 @@ -from typing import NamedTuple, List +from typing import List import unittest from worlds.AutoWorld import World -from Fill import FillError, fill_restrictive -from BaseClasses import MultiWorld, Region, RegionType, Item, Location -from worlds.generic.Rules import set_rule +from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive +from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location +from worlds.generic.Rules import CollectionRule, set_rule def generate_multi_world(players: int = 1) -> MultiWorld: @@ -19,31 +19,87 @@ def generate_multi_world(players: int = 1) -> MultiWorld: "Menu Region Hint", player_id, multi_world) multi_world.regions.append(region) - multi_world.set_seed() + multi_world.set_seed(0) multi_world.set_default_common_options() return multi_world -class PlayerDefinition(NamedTuple): +class PlayerDefinition(object): + world: MultiWorld id: int menu: Region locations: List[Location] prog_items: List[Item] + basic_items: List[Item] + regions: List[Region] + + def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []): + self.world = world + self.id = id + self.menu = menu + self.locations = locations + self.prog_items = prog_items + self.basic_items = basic_items + self.regions = [menu] + + def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region: + region_tag = "_region" + str(len(self.regions)) + region_name = "player" + str(self.id) + region_tag + region = Region("player" + str(self.id) + region_tag, RegionType.Generic, + "Region Hint", self.id, self.world) + self.locations += generate_locations(size, + self.id, None, region, region_tag) + + entrance = Entrance(self.id, region_name + "_entrance", parent) + parent.exits.append(entrance) + entrance.connect(region) + entrance.access_rule = access_rule + + self.regions.append(region) + self.world.regions.append(region) + + return region -def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition: +def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]: + items = items.copy() + while len(items) > 0: + location = region.locations.pop(0) + region.locations.append(location) + if location.item: + return items + item = items.pop(0) + world.push_item(location, item, False) + location.event = item.advancement + + return items + + +def regionContains(region: Region, item: Item) -> bool: + for location in region.locations: + if location.item == item: + return True + + return False + + +def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition: menu = multi_world.get_region("Menu", player_id) locations = generate_locations(location_count, player_id, None, menu) prog_items = generate_items(prog_item_count, player_id, True) + multi_world.itempool += prog_items + basic_items = generate_items(basic_item_count, player_id, False) + multi_world.itempool += basic_items - return PlayerDefinition(player_id, menu, locations, prog_items) + return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items) -def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]: +def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]: locations = [] + prefix = "player" + str(player_id) + tag + "_location" for i in range(count): - name = "player" + str(player_id) + "_location" + str(i) + name = prefix + str(i) location = Location(player_id, name, address, region) locations.append(location) region.locations.append(location) @@ -52,14 +108,15 @@ def generate_locations(count: int, player_id: int, address: int = None, region: def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]: items = [] + type = "prog" if advancement else "" for i in range(count): - name = "player" + str(player_id) + "_item" + str(i) + name = "player" + str(player_id) + "_" + type + "item" + str(i) items.append(Item(name, advancement, code, player_id)) return items -class TestBase(unittest.TestCase): - def test_basic_fill_restrictive(self): +class TestFillRestrictive(unittest.TestCase): + def test_basic_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -76,7 +133,7 @@ class TestBase(unittest.TestCase): self.assertEqual([], player1.locations) self.assertEqual([], player1.prog_items) - def test_ordered_fill_restrictive(self): + def test_ordered_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) items = player1.prog_items @@ -92,7 +149,7 @@ class TestBase(unittest.TestCase): self.assertEqual(locations[0].item, items[0]) self.assertEqual(locations[1].item, items[1]) - def test_fill_restrictive_remaining_locations(self): + def test_partial_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 3, 2) @@ -106,7 +163,7 @@ class TestBase(unittest.TestCase): item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has( item0.name, player1.id)) - #forces a swap + # forces a swap set_rule(loc2, lambda state: state.has( item0.name, player1.id)) fill_restrictive(multi_world, multi_world.state, @@ -117,7 +174,7 @@ class TestBase(unittest.TestCase): self.assertEqual(1, len(player1.locations)) self.assertEqual(player1.locations[0], loc2) - def test_minimal_fill_restrictive(self): + def test_minimal_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -137,7 +194,7 @@ class TestBase(unittest.TestCase): # Unnecessary unreachable Item self.assertEqual(locations[1].item, items[0]) - def test_reversed_fill_restrictive(self): + def test_reversed_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -155,7 +212,7 @@ class TestBase(unittest.TestCase): self.assertEqual(loc0.item, item1) self.assertEqual(loc1.item, item0) - def test_multi_step_fill_restrictive(self): + def test_multi_step_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 4, 4) @@ -179,7 +236,7 @@ class TestBase(unittest.TestCase): self.assertEqual(locations[2].item, items[0]) self.assertEqual(locations[3].item, items[3]) - def test_impossible_fill_restrictive(self): + def test_impossible_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) items = player1.prog_items @@ -195,7 +252,7 @@ class TestBase(unittest.TestCase): self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, player1.locations.copy(), player1.prog_items.copy()) - def test_circular_fill_restrictive(self): + def test_circular_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 3, 3) @@ -215,7 +272,7 @@ class TestBase(unittest.TestCase): self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, player1.locations.copy(), player1.prog_items.copy()) - def test_competing_fill_restrictive(self): + def test_competing_fill(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -231,7 +288,7 @@ class TestBase(unittest.TestCase): self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, player1.locations.copy(), player1.prog_items.copy()) - def test_multiplayer_fill_restrictive(self): + def test_multiplayer_fill(self): multi_world = generate_multi_world(2) player1 = generate_player_data(multi_world, 1, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2) @@ -251,7 +308,7 @@ class TestBase(unittest.TestCase): self.assertEqual(player2.locations[0].item, player1.prog_items[0]) self.assertEqual(player2.locations[1].item, player2.prog_items[0]) - def test_multiplayer_rules_fill_restrictive(self): + def test_multiplayer_rules_fill(self): multi_world = generate_multi_world(2) player1 = generate_player_data(multi_world, 1, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2) @@ -273,3 +330,225 @@ class TestBase(unittest.TestCase): self.assertEqual(player1.locations[1].item, player2.prog_items[1]) self.assertEqual(player2.locations[0].item, player1.prog_items[0]) self.assertEqual(player2.locations[1].item, player1.prog_items[1]) + + +class TestDistributeItemsRestrictive(unittest.TestCase): + def test_basic_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + prog_items = player1.prog_items + basic_items = player1.basic_items + + distribute_items_restrictive(multi_world) + + self.assertEqual(locations[0].item, basic_items[0]) + self.assertEqual(locations[1].item, prog_items[0]) + self.assertEqual(locations[2].item, prog_items[1]) + self.assertEqual(locations[3].item, basic_items[1]) + + def test_excluded_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + + locations[1].progress_type = LocationProgressType.EXCLUDED + locations[2].progress_type = LocationProgressType.EXCLUDED + + distribute_items_restrictive(multi_world) + + self.assertFalse(locations[1].item.advancement) + self.assertFalse(locations[2].item.advancement) + + def test_non_excluded_item_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + basic_items = player1.basic_items + + locations[1].progress_type = LocationProgressType.EXCLUDED + basic_items[1].never_exclude = True + + distribute_items_restrictive(multi_world) + + self.assertEqual(locations[1].item, basic_items[0]) + + def test_too_many_excluded_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + + locations[0].progress_type = LocationProgressType.EXCLUDED + locations[1].progress_type = LocationProgressType.EXCLUDED + locations[2].progress_type = LocationProgressType.EXCLUDED + + self.assertRaises(FillError, distribute_items_restrictive, multi_world) + + def test_non_excluded_item_must_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + basic_items = player1.basic_items + + locations[1].progress_type = LocationProgressType.EXCLUDED + locations[2].progress_type = LocationProgressType.EXCLUDED + basic_items[0].never_exclude = True + basic_items[1].never_exclude = True + + self.assertRaises(FillError, distribute_items_restrictive, multi_world) + + def test_priority_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + + locations[0].progress_type = LocationProgressType.PRIORITY + locations[3].progress_type = LocationProgressType.PRIORITY + + distribute_items_restrictive(multi_world) + + self.assertTrue(locations[0].item.advancement) + self.assertTrue(locations[3].item.advancement) + + def test_excess_priority_distribute(self): + multi_world = generate_multi_world() + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + locations = player1.locations + + locations[0].progress_type = LocationProgressType.PRIORITY + locations[1].progress_type = LocationProgressType.PRIORITY + locations[2].progress_type = LocationProgressType.PRIORITY + + distribute_items_restrictive(multi_world) + + self.assertFalse(locations[3].item.advancement) + + def test_multiple_world_distribute(self): + multi_world = generate_multi_world(3) + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + player2 = generate_player_data( + multi_world, 2, 4, prog_item_count=1, basic_item_count=3) + player3 = generate_player_data( + multi_world, 3, 6, prog_item_count=4, basic_item_count=2) + + distribute_items_restrictive(multi_world) + + self.assertEqual(player1.locations[0].item, player1.prog_items[1]) + self.assertEqual(player1.locations[1].item, player3.prog_items[2]) + self.assertEqual(player1.locations[2].item, player3.prog_items[1]) + self.assertEqual(player1.locations[3].item, player2.prog_items[0]) + + self.assertEqual(player2.locations[0].item, player1.basic_items[0]) + self.assertEqual(player2.locations[1].item, player2.basic_items[1]) + self.assertEqual(player2.locations[2].item, player3.basic_items[1]) + self.assertEqual(player2.locations[3].item, player2.basic_items[0]) + + self.assertEqual(player3.locations[0].item, player1.basic_items[1]) + self.assertEqual(player3.locations[1].item, player3.prog_items[3]) + self.assertEqual(player3.locations[2].item, player1.prog_items[0]) + self.assertEqual(player3.locations[3].item, player3.basic_items[0]) + self.assertEqual(player3.locations[4].item, player3.prog_items[0]) + self.assertEqual(player3.locations[5].item, player2.basic_items[2]) + + def test_multiple_world_priority_distribute(self): + multi_world = generate_multi_world(3) + player1 = generate_player_data( + multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + player2 = generate_player_data( + multi_world, 2, 4, prog_item_count=1, basic_item_count=3) + player3 = generate_player_data( + multi_world, 3, 6, prog_item_count=4, basic_item_count=2) + + player1.locations[2].progress_type = LocationProgressType.PRIORITY + player1.locations[3].progress_type = LocationProgressType.PRIORITY + + player2.locations[1].progress_type = LocationProgressType.PRIORITY + + player3.locations[0].progress_type = LocationProgressType.PRIORITY + player3.locations[1].progress_type = LocationProgressType.PRIORITY + player3.locations[2].progress_type = LocationProgressType.PRIORITY + player3.locations[3].progress_type = LocationProgressType.PRIORITY + + distribute_items_restrictive(multi_world) + + self.assertTrue(player1.locations[2].item.advancement) + self.assertTrue(player1.locations[3].item.advancement) + self.assertTrue(player2.locations[1].item.advancement) + self.assertTrue(player3.locations[0].item.advancement) + self.assertTrue(player3.locations[1].item.advancement) + self.assertTrue(player3.locations[2].item.advancement) + self.assertTrue(player3.locations[3].item.advancement) + + +class TestBalanceMultiworldProgression(unittest.TestCase): + def assertRegionContains(self, region: Region, item: Item): + for location in region.locations: + if location.item and location.item == item: + return True + + self.fail("Expected " + region.name + " to contain " + item.name + + "\n Contains" + str(list(map(lambda location: location.item, region.locations)))) + + def setUp(self): + multi_world = generate_multi_world(2) + self.multi_world = multi_world + player1 = generate_player_data( + multi_world, 1, prog_item_count=2, basic_item_count=40) + self.player1 = player1 + player2 = generate_player_data( + multi_world, 2, prog_item_count=2, basic_item_count=40) + self.player2 = player2 + + multi_world.completion_condition[player1.id] = lambda state: state.has( + player1.prog_items[0].name, player1.id) and state.has( + player1.prog_items[1].name, player1.id) + multi_world.completion_condition[player2.id] = lambda state: state.has( + player2.prog_items[0].name, player2.id) and state.has( + player2.prog_items[1].name, player2.id) + + items = player1.basic_items + player2.basic_items + + # Sphere 1 + region = player1.generate_region(player1.menu, 20) + items = fillRegion(multi_world, region, [ + player1.prog_items[0]] + items) + + # Sphere 2 + region = player1.generate_region( + player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id)) + items = fillRegion( + multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items) + + # Sphere 3 + region = player2.generate_region( + player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id)) + items = fillRegion(multi_world, region, [ + player2.prog_items[1]] + items) + + multi_world.progression_balancing[player1.id] = True + multi_world.progression_balancing[player2.id] = True + + def test_balances_progression(self): + self.assertRegionContains( + self.player1.regions[2], self.player2.prog_items[0]) + + balance_multiworld_progression(self.multi_world) + + self.assertRegionContains( + self.player1.regions[1], self.player2.prog_items[0]) + + def test_ignores_priority_locations(self): + self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY + + balance_multiworld_progression(self.multi_world) + + self.assertRegionContains( + self.player1.regions[2], self.player2.prog_items[0]) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index dadabd89..20f8a0b2 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,5 +1,7 @@ import typing +from BaseClasses import LocationProgressType + if typing.TYPE_CHECKING: import BaseClasses @@ -31,6 +33,7 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]): else: add_item_rule(location, lambda i: not (i.advancement or i.never_exclude)) location.excluded = True + location.progress_type = LocationProgressType.EXCLUDED def set_rule(spot, rule: CollectionRule): spot.access_rule = rule