From c811c72edaf1afe2df66e66b6f1e1c1945e6f767 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:30:42 -0500 Subject: [PATCH 01/51] working? --- Fill.py | 277 +++++++++++++++++++++++++--------------------------- Generate.py | 2 - Options.py | 92 +++++++++++++++++ 3 files changed, 223 insertions(+), 148 deletions(-) diff --git a/Fill.py b/Fill.py index 2d6257eae30a..b1cb0a53b8a6 100644 --- a/Fill.py +++ b/Fill.py @@ -5,7 +5,7 @@ from collections import Counter, deque from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld -from Options import Accessibility +from Options import Accessibility, PlandoItem from worlds.AutoWorld import call_all from worlds.generic.Rules import add_item_rule @@ -82,7 +82,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati # if minimal accessibility, only check whether location is reachable if game not beatable if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal: perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state, - item_to_place.player) \ + item_to_place.player) \ if single_player_placement else not has_beaten_game else: perform_access_check = True @@ -198,10 +198,10 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati # There are leftover unplaceable items and locations that won't accept them if multiworld.can_beat_game(): logging.warning( - f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})') + f"Not all items placed. Game beatable anyway. (Could not place {unplaced_items})") else: - 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)}') + 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)}") item_pool.extend(unplaced_items) @@ -213,7 +213,7 @@ def remaining_fill(multiworld: MultiWorld, unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() - total = min(len(itempool), len(locations)) + total = min(len(itempool), len(locations)) placed = 0 while locations and itempool: item_to_place = itempool.pop() @@ -273,8 +273,8 @@ def remaining_fill(multiworld: MultiWorld, 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)}') + 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) @@ -290,8 +290,10 @@ def fast_fill(multiworld: MultiWorld, def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]): maximum_exploration_state = sweep_from_pool(state, pool) - minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"} - unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and + minimal_players = {player for player in multiworld.player_ids if + multiworld.worlds[player].options.accessibility == "minimal"} + unreachable_locations = [location for location in multiworld.get_locations() if + location.player in minimal_players and not location.can_reach(maximum_exploration_state)] for location in unreachable_locations: if (location.item is not None and location.item.advancement and location.address is not None and not @@ -313,7 +315,7 @@ def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] if unreachable_locations: def forbid_important_item_rule(item: Item): - return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal') + return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != "minimal") for location in unreachable_locations: add_item_rule(location, forbid_important_item_rule) @@ -447,7 +449,8 @@ def mark_for_locking(location: Location): if prioritylocations: # "priority fill" - fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, + on_place=mark_for_locking, name="Priority") accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations @@ -457,7 +460,7 @@ def mark_for_locking(location: Location): fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression") if progitempool: raise FillError( - f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') + f"Not enough locations for progress items. There are {len(progitempool)} more items than locations") accessibility_corrections(multiworld, multiworld.state, defaultlocations) for location in lock_later: @@ -481,13 +484,13 @@ def mark_for_locking(location: Location): if unplaced or unfilled: logging.warning( - f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') + f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}") items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item) locations_counter = Counter(location.player for location in multiworld.get_locations()) items_counter.update(item.player for item in unplaced) locations_counter.update(location.player for location in unfilled) print_data = {"items": items_counter, "locations": locations_counter} - logging.info(f'Per-Player counts: {print_data})') + logging.info(f"Per-Player counts: {print_data})") def flood_items(multiworld: MultiWorld) -> None: @@ -535,7 +538,7 @@ def flood_items(multiworld: MultiWorld) -> None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: - raise FillError('No more progress items left to place.') + raise FillError("No more progress items left to place.") # find item to replace with progress item location_list = multiworld.get_reachable_locations() @@ -564,9 +567,9 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: if multiworld.worlds[player].options.progression_balancing > 0 } if not balanceable_players: - logging.info('Skipping multiworld progression balancing.') + logging.info("Skipping multiworld progression balancing.") else: - logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') + logging.info(f"Balancing multiworld progression for {len(balanceable_players)} Players.") logging.debug(balanceable_players) state: CollectionState = CollectionState(multiworld) checked_locations: typing.Set[Location] = set() @@ -665,7 +668,7 @@ def item_percentage(player: int, num: int) -> float: if player in threshold_percentages): break elif not balancing_sphere: - raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') + raise RuntimeError("Not all required items reachable. Something went terribly wrong here.") # Gather a set of locations which we can swap items into unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set) for l in unchecked_locations: @@ -681,8 +684,8 @@ def item_percentage(player: int, num: int) -> float: testing = items_to_test.pop() reducing_state = state.copy() for location in itertools.chain(( - l for l in items_to_replace - if l.item.player == player + l for l in items_to_replace + if l.item.player == player ), items_to_test): reducing_state.collect(location.item, True, location) @@ -758,13 +761,13 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: def distribute_planned(multiworld: MultiWorld) -> None: def warn(warning: str, force: typing.Union[bool, str]) -> None: - if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']: - logging.warning(f'{warning}') + if force in [True, "fail", "failure", "none", False, "warn", "warning"]: + logging.warning(f"{warning}") else: - logging.debug(f'{warning}') + logging.debug(f"{warning}") def failed(warning: str, force: typing.Union[bool, str]) -> None: - if force in [True, 'fail', 'failure']: + if force in [True, "fail", "failure"]: raise Exception(warning) else: warn(warning, force) @@ -782,23 +785,18 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: world_name_lookup = multiworld.world_name_lookup - block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] + block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any]] plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] player_ids = set(multiworld.player_ids) for player in player_ids: - for block in multiworld.plando_items[player]: - block['player'] = player - if 'force' not in block: - block['force'] = 'silent' - if 'from_pool' not in block: - block['from_pool'] = True - elif not isinstance(block['from_pool'], bool): - from_pool_type = type(block['from_pool']) - raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.') - if 'world' not in block: - target_world = False - else: - target_world = block['world'] + for block in multiworld.worlds[player].options.plando_items: + new_block = {"player": player} + if not isinstance(block.from_pool, bool): + from_pool_type = type(block.from_pool) + raise Exception(f"Plando 'from_pool' has to be boolean, not {from_pool_type} for player {player}.") + new_block["from_pool"] = block.from_pool + new_block["force"] = block.force + target_world = block.world if target_world is False or multiworld.players == 1: # target own world worlds: typing.Set[int] = {player} @@ -811,36 +809,25 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: for listed_world in target_world: if listed_world not in world_name_lookup: failed(f"Cannot place item to {target_world}'s world as that world does not exist.", - block['force']) + block.force) continue worlds.add(world_name_lookup[listed_world]) elif type(target_world) == int: # target world by slot number if target_world not in range(1, multiworld.players + 1): failed( f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})", - block['force']) + block.force) continue worlds = {target_world} else: # target world by slot name if target_world not in world_name_lookup: failed(f"Cannot place item to {target_world}'s world as that world does not exist.", - block['force']) + block.force) continue worlds = {world_name_lookup[target_world]} - block['world'] = worlds - - items: block_value = [] - if "items" in block: - items = block["items"] - if 'count' not in block: - block['count'] = False - elif "item" in block: - items = block["item"] - if 'count' not in block: - block['count'] = 1 - else: - failed("You must specify at least one item to place items with plando.", block['force']) - continue + new_block["world"] = worlds + + items: block_value = block.items if isinstance(items, dict): item_list: typing.List[str] = [] for key, value in items.items(): @@ -850,22 +837,12 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: items = item_list if isinstance(items, str): items = [items] - block['items'] = items + new_block["items"] = items - locations: block_value = [] - if 'location' in block: - locations = block['location'] # just allow 'location' to keep old yamls compatible - elif 'locations' in block: - locations = block['locations'] + locations: block_value = block.locations if isinstance(locations, str): locations = [locations] - if isinstance(locations, dict): - location_list = [] - for key, value in locations.items(): - location_list += [key] * value - locations = location_list - if "early_locations" in locations: locations.remove("early_locations") for target_player in worlds: @@ -874,93 +851,101 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: locations.remove("non_early_locations") for target_player in worlds: locations += non_early_locations[target_player] - - block['locations'] = list(dict.fromkeys(locations)) - - if not block['count']: - block['count'] = (min(len(block['items']), len(block['locations'])) if - len(block['locations']) > 0 else len(block['items'])) - if isinstance(block['count'], int): - block['count'] = {'min': block['count'], 'max': block['count']} - if 'min' not in block['count']: - block['count']['min'] = 0 - if 'max' not in block['count']: - block['count']['max'] = (min(len(block['items']), len(block['locations'])) if - len(block['locations']) > 0 else len(block['items'])) - if block['count']['max'] > len(block['items']): - count = block['count'] - failed(f"Plando count {count} greater than items specified", block['force']) - block['count'] = len(block['items']) - if block['count']['max'] > len(block['locations']) > 0: - count = block['count'] - failed(f"Plando count {count} greater than locations specified", block['force']) - block['count'] = len(block['locations']) - block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max']) - - if block['count']['target'] > 0: - plando_blocks.append(block) + for target_player in worlds: + for group in multiworld.worlds[target_player].location_name_groups: + if group in locations: + locations.extend(multiworld.worlds[target_player].location_name_groups[group]) + + new_block["locations"] = list(dict.fromkeys(locations)) + + new_block["count"] = block.count + if not new_block["count"]: + new_block["count"] = (min(len(new_block["items"]), len(new_block["locations"])) if + len(new_block["locations"]) > 0 else len(new_block["items"])) + if isinstance(new_block["count"], int): + new_block["count"] = {"min": new_block["count"], "max": new_block["count"]} + if "min" not in new_block["count"]: + new_block["count"]["min"] = 0 + if "max" not in new_block["count"]: + new_block["count"]["max"] = (min(len(new_block["items"]), len(new_block["locations"])) if + len(new_block["locations"]) > 0 else len(new_block["items"])) + if new_block["count"]["max"] > len(new_block["items"]): + count = new_block["count"] + failed(f"Plando count {count} greater than items specified", block.force) + new_block["count"] = len(new_block["items"]) + if new_block["count"]["max"] > len(new_block["locations"]) > 0: + count = new_block["count"] + failed(f"Plando count {count} greater than locations specified", block.force) + new_block["count"] = len(new_block["locations"]) + new_block["count"]["target"] = multiworld.random.randint(new_block["count"]["min"], + new_block["count"]["max"]) + + if new_block["count"]["target"] > 0: + plando_blocks.append(new_block) # shuffle, but then sort blocks by number of locations minus number of items, # so less-flexible blocks get priority multiworld.random.shuffle(plando_blocks) - plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target'] - if len(block['locations']) > 0 - else len(multiworld.get_unfilled_locations(player)) - block['count']['target'])) - + plando_blocks.sort(key=lambda block: (len(block["locations"]) - block["count"]["target"] + if len(block["locations"]) > 0 + else len(multiworld.get_unfilled_locations(player)) + - block["count"]["target"])) for placement in plando_blocks: - player = placement['player'] + player = placement["player"] try: - worlds = placement['world'] - locations = placement['locations'] - items = placement['items'] - maxcount = placement['count']['target'] - from_pool = placement['from_pool'] + worlds = placement["world"] + locations = placement["locations"] + items = placement["items"] + maxcount = placement["count"]["target"] + from_pool = placement["from_pool"] candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds))) - multiworld.random.shuffle(candidates) - multiworld.random.shuffle(items) - count = 0 - err: typing.List[str] = [] - successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] - for item_name in items: - item = multiworld.worlds[player].create_item(item_name) - for location in reversed(candidates): - if (location.address is None) == (item.code is None): # either both None or both not None - if not location.item: - if location.item_rule(item): - if location.can_fill(multiworld.state, item, False): - successful_pairs.append((item, location)) - candidates.remove(location) - count = count + 1 - break - else: - err.append(f"Can't place item at {location} due to fill condition not met.") - else: - err.append(f"{item_name} not allowed at {location}.") - else: - err.append(f"Cannot place {item_name} into already filled location {location}.") + if any(location.address is None for location in candidates) \ + and not all(location.address is None for location in candidates): + failed(f"Plando block for player {player} ({multiworld.player_name[player]}) contains both " + f"event locations and non-event locations. " + f"Event locations: {[location for location in candidates if location.address is None]}, " + f"Non-event locations: {[location for location in candidates if location.address is not None]}", + placement["force"]) + continue + item_candidates = [] + if from_pool: + instances = [item for item in multiworld.itempool if item.player == player and item.name in items] + for item in multiworld.random.sample(items, maxcount): + candidate = next((i for i in instances if i.name == item), None) + if candidate is None: + warn(f"Could not remove {item} from pool for {multiworld.player_name[player]} as " + f"it's already missing from it", placement["force"]) + candidate = multiworld.worlds[player].create_item(item) else: - err.append(f"Mismatch between {item_name} and {location}, only one is an event.") - if count == maxcount: - break - if count < placement['count']['min']: - m = placement['count']['min'] - failed( - f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", - placement['force']) - for (item, location) in successful_pairs: - multiworld.push_item(location, item, collect=False) - location.event = True # flag location to be checked during fill - location.locked = True - logging.debug(f"Plando placed {item} at {location}") - if from_pool: - try: - multiworld.itempool.remove(item) - except ValueError: - warn( - f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", - placement['force']) - + multiworld.itempool.remove(candidate) + instances.remove(candidate) + item_candidates.append(candidate) + else: + item_candidates = [multiworld.worlds[player].create_item(item) + for item in multiworld.random.sample(items, maxcount)] + if any(item.code is None for item in item_candidates) \ + and not all(item.code is None for item in item_candidates): + failed(f"Plando block for player {player} ({multiworld.player_name[player]}) contains both " + f"event items and non-event items. " + f"Event items: {[item for item in item_candidates if item.code is None]}, " + f"Non-event items: {[item for item in item_candidates if item.code is not None]}", + placement["force"]) + continue + allstate = multiworld.get_all_state(False) + mincount = placement["count"]["min"] + allowed_margin = len(item_candidates) - mincount + fill_restrictive(multiworld, allstate, candidates, item_candidates, lock=True, + allow_partial=True, name="Plando Main Fill") + + if len(item_candidates) > allowed_margin: + failed(f"Could not place {len(item_candidates)} " + f"of {mincount + allowed_margin} item(s) " + f"for {multiworld.player_name[player]}, " + f"remaining items: {item_candidates}", + placement["force"]) + if from_pool: + multiworld.itempool.extend([item for item in item_candidates if item.code is not None]) except Exception as e: raise Exception( f"Error running plando for player {player} ({multiworld.player_name[player]})") from e diff --git a/Generate.py b/Generate.py index 56979334b547..4b40bfdeddbf 100644 --- a/Generate.py +++ b/Generate.py @@ -482,8 +482,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) - if PlandoOptions.items in plando_options: - ret.plando_items = game_weights.get("plando_items", []) if ret.game == "A Link to the Past": roll_alttp_settings(ret, game_weights, plando_options) if PlandoOptions.connections in plando_options: diff --git a/Options.py b/Options.py index e1ae33914332..d0052412d773 100644 --- a/Options.py +++ b/Options.py @@ -1115,6 +1115,97 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P link.setdefault("link_replacement", None) +class PlandoItem(typing.NamedTuple): + items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] + locations: typing.Union[typing.List[str]] + world: typing.Union[int, str, bool, None, typing.Iterable[str], typing.Set[int]] = False + from_pool: bool = True + force: typing.Union[bool, typing.Literal["silent"]] = "silent" + count: typing.Union[int, bool, typing.Dict[str, int]] = False + percentage: int = 100 + player: int = -1 # present for use later + + +class PlandoItems(Option[typing.List[PlandoItem]]): + default = () + supports_weighting = False + display_name = "Plando Items" + + def __init__(self, value: typing.Iterable[PlandoItem]) -> None: + self.value = list(deepcopy(value)) + super().__init__() + + @classmethod + def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: + if not isinstance(data, typing.Iterable): + raise Exception(f"Cannot create plando connections from non-Iterable type, got {type(data)}") + + value: typing.List[PlandoItem] = [] + for item in data: + if isinstance(item, typing.Mapping): + percentage = item.get("percentage", 100) + if random.random() < float(percentage / 100): + count = item.get("count", False) + items = item.get("items", []) + if not items: + items = item.get("item", None) # explcitly throw an error here if not present + if not items: + raise Exception("You must specify at least one item to place items with plando.") + items = [items] + locations = item.get("locations", []) + if not locations: + locations = item.get("location", None) + if not locations: + raise Exception("You must specify at least one location to place items with plando.") + locations = [locations] + world = item.get("world", False) + from_pool = item.get("from_pool", True) + force = item.get("force", "silent") + value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) + elif isinstance(item, PlandoItem): + if random.random() < float(item.percentage / 100): + value.append(item) + else: + raise Exception(f"Cannot create plando item from non-Dict type, got {type(item)}.") + return cls(value) + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if not (PlandoOptions.items & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando items module is turned off, " + f"so items for {player_name} will be ignored.") + else: + # filter down item groups + for plando in self.value: + items_copy = plando.items.copy() + if isinstance(plando.items, dict): + for item in items_copy: + if item in world.item_name_groups: + value = plando.items.pop(item) + plando.items.update({key: value for key in world.item_name_groups[item]}) + else: + assert isinstance(plando.items, list) # pycharm can't figure out the hinting without the hint + for item in items_copy: + if item in world.item_name_groups: + plando.items.remove(item) + plando.items.extend(sorted(world.item_name_groups[item])) + + @classmethod + def get_option_name(cls, value: typing.List[PlandoItem]) -> str: + return ", ".join(["%s-%s" % (item.items, item.locations) for item in value]) #TODO: see what a better way to display would be + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoItem: + return self.value.__getitem__(index) + + def __iter__(self) -> typing.Iterator[PlandoItem]: + yield from self.value + + def __len__(self) -> int: + return len(self.value) + + @dataclass class PerGameCommonOptions(CommonOptions): local_items: LocalItems @@ -1125,6 +1216,7 @@ class PerGameCommonOptions(CommonOptions): exclude_locations: ExcludeLocations priority_locations: PriorityLocations item_links: ItemLinks + plando_items: PlandoItems @dataclass From 3027f37281ff357d138711ed12b65ab41d4e00be Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:05:34 -0500 Subject: [PATCH 02/51] Add docstring, removed unused --- Fill.py | 4 ++-- Options.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index b1cb0a53b8a6..603c3905530a 100644 --- a/Fill.py +++ b/Fill.py @@ -888,8 +888,8 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: multiworld.random.shuffle(plando_blocks) plando_blocks.sort(key=lambda block: (len(block["locations"]) - block["count"]["target"] if len(block["locations"]) > 0 - else len(multiworld.get_unfilled_locations(player)) - - block["count"]["target"])) + else len(multiworld.get_unfilled_locations(player)) - + block["count"]["target"])) for placement in plando_blocks: player = placement["player"] try: diff --git a/Options.py b/Options.py index d0052412d773..b2fde3ed0fc3 100644 --- a/Options.py +++ b/Options.py @@ -1123,10 +1123,10 @@ class PlandoItem(typing.NamedTuple): force: typing.Union[bool, typing.Literal["silent"]] = "silent" count: typing.Union[int, bool, typing.Dict[str, int]] = False percentage: int = 100 - player: int = -1 # present for use later class PlandoItems(Option[typing.List[PlandoItem]]): + """Generic items plando.""" default = () supports_weighting = False display_name = "Plando Items" From b583ebb8cbcdd70d49a4047ba86d9ea4b1485a88 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 02:02:07 -0500 Subject: [PATCH 03/51] fix ladx test the test is still busted, but now it actually plandos the items --- worlds/ladx/test/testShop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py index 91d504d521b4..24ab70f1ad7c 100644 --- a/worlds/ladx/test/testShop.py +++ b/worlds/ladx/test/testShop.py @@ -1,6 +1,7 @@ from typing import Optional from Fill import distribute_planned +from Options import PlandoItems from test.general import setup_solo_multiworld from worlds.AutoWorld import call_all from . import LADXTestBase @@ -19,13 +20,13 @@ class PlandoTest(LADXTestBase): ], }], } - + def world_setup(self, seed: Optional[int] = None) -> None: self.multiworld = setup_solo_multiworld( LinksAwakeningWorld, ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") ) - self.multiworld.plando_items[1] = self.options["plando_items"] + self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"]) distribute_planned(self.multiworld) call_all(self.multiworld, "pre_fill") From dbb83469889a384840555884fab3aabcb2c30e9d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:23:18 -0500 Subject: [PATCH 04/51] Update Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index b2fde3ed0fc3..0a18988ae168 100644 --- a/Options.py +++ b/Options.py @@ -1138,7 +1138,7 @@ def __init__(self, value: typing.Iterable[PlandoItem]) -> None: @classmethod def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: if not isinstance(data, typing.Iterable): - raise Exception(f"Cannot create plando connections from non-Iterable type, got {type(data)}") + raise Exception(f"Cannot create plando items from non-Iterable type, got {type(data)}") value: typing.List[PlandoItem] = [] for item in data: From 702a9a4d17f725cb8cb1597aa259cd65de64f7d2 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 19:34:31 -0500 Subject: [PATCH 05/51] support locations is None --- Options.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index 0a18988ae168..274b1f986cba 100644 --- a/Options.py +++ b/Options.py @@ -1117,7 +1117,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class PlandoItem(typing.NamedTuple): items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] - locations: typing.Union[typing.List[str]] + locations: typing.Optional[typing.List[str]] world: typing.Union[int, str, bool, None, typing.Iterable[str], typing.Set[int]] = False from_pool: bool = True force: typing.Union[bool, typing.Literal["silent"]] = "silent" @@ -1155,9 +1155,8 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: locations = item.get("locations", []) if not locations: locations = item.get("location", None) - if not locations: - raise Exception("You must specify at least one location to place items with plando.") - locations = [locations] + if locations: + locations = [locations] world = item.get("world", False) from_pool = item.get("from_pool", True) force = item.get("force", "silent") From 905b700c53a7e716258b98e9b499e0c13aeb420e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 19:47:54 -0500 Subject: [PATCH 06/51] account for present but empty plando items for warning --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index 274b1f986cba..7f55085292d9 100644 --- a/Options.py +++ b/Options.py @@ -1169,6 +1169,8 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: return cls(value) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + if not self.value: + return from BaseClasses import PlandoOptions if not (PlandoOptions.items & plando_options): # plando is disabled but plando options were given so overwrite the options From 2e00b07c5c288a4dbcfa2acdfde5933c9060f832 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 30 Mar 2024 01:35:09 -0500 Subject: [PATCH 07/51] Update Fill.py --- Fill.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Fill.py b/Fill.py index 603c3905530a..5bee30eb2e5d 100644 --- a/Fill.py +++ b/Fill.py @@ -790,7 +790,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: player_ids = set(multiworld.player_ids) for player in player_ids: for block in multiworld.worlds[player].options.plando_items: - new_block = {"player": player} + new_block: typing.Dict[str, typing.Any] = {"player": player} if not isinstance(block.from_pool, bool): from_pool_type = type(block.from_pool) raise Exception(f"Plando 'from_pool' has to be boolean, not {from_pool_type} for player {player}.") @@ -870,13 +870,17 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: new_block["count"]["max"] = (min(len(new_block["items"]), len(new_block["locations"])) if len(new_block["locations"]) > 0 else len(new_block["items"])) if new_block["count"]["max"] > len(new_block["items"]): - count = new_block["count"] + count = new_block["count"]["max"] failed(f"Plando count {count} greater than items specified", block.force) - new_block["count"] = len(new_block["items"]) + new_block["count"]["max"] = len(new_block["items"]) + if new_block["count"]["min"] > len(new_block["items"]): + new_block["count"]["min"] = len(new_block["items"]) if new_block["count"]["max"] > len(new_block["locations"]) > 0: - count = new_block["count"] + count = new_block["count"]["max"] failed(f"Plando count {count} greater than locations specified", block.force) - new_block["count"] = len(new_block["locations"]) + new_block["count"]["max"] = len(new_block["locations"]) + if new_block["count"]["min"] > len(new_block["locations"]): + new_block["count"]["min"] = len(new_block["locations"]) new_block["count"]["target"] = multiworld.random.randint(new_block["count"]["min"], new_block["count"]["max"]) From 6bc8701faf3ef791986acf63d300c69ff260d0c3 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 11 Aug 2024 00:24:19 -0500 Subject: [PATCH 08/51] rewrite candidates, add compat test (limited) --- Fill.py | 15 ++++++--------- test/general/test_implemented.py | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Fill.py b/Fill.py index 220760835745..cd09ff7f4758 100644 --- a/Fill.py +++ b/Fill.py @@ -954,15 +954,6 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: maxcount = placement["count"]["target"] from_pool = placement["from_pool"] - candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds))) - if any(location.address is None for location in candidates) \ - and not all(location.address is None for location in candidates): - failed(f"Plando block for player {player} ({multiworld.player_name[player]}) contains both " - f"event locations and non-event locations. " - f"Event locations: {[location for location in candidates if location.address is None]}, " - f"Non-event locations: {[location for location in candidates if location.address is not None]}", - placement["force"]) - continue item_candidates = [] if from_pool: instances = [item for item in multiworld.itempool if item.player == player and item.name in items] @@ -987,6 +978,12 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: f"Non-event items: {[item for item in item_candidates if item.code is not None]}", placement["force"]) continue + else: + is_real = item_candidates[0].code is not None + candidates = [candidate for candidate in multiworld.get_unfilled_locations_for_players(locations, + sorted(worlds)) + if bool(candidate.address) == is_real] + multiworld.random.shuffle(candidates) allstate = multiworld.get_all_state(False) mincount = placement["count"]["min"] allowed_margin = len(item_candidates) - mincount diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index e76d539451ea..873d5f807a37 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -1,7 +1,8 @@ import unittest -from Fill import distribute_items_restrictive +from Fill import distribute_items_restrictive, distribute_planned from NetUtils import encode +from Options import PlandoItem from worlds.AutoWorld import AutoWorldRegister, call_all from worlds import failed_world_loads from . import setup_solo_multiworld @@ -52,3 +53,19 @@ def test_slot_data(self): def test_no_failed_world_loads(self): if failed_world_loads: self.fail(f"The following worlds failed to load: {failed_world_loads}") + + def test_prefill_items(self): + """Test that every world can reach every location from allstate before pre_fill.""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"): + with self.subTest(gamename): + multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items", + "set_rules", "generate_basic")) + allstate = multiworld.get_all_state(False) + locations = multiworld.get_locations() + reachable = multiworld.get_reachable_locations(allstate) + unreachable = [location for location in locations if location not in reachable] + + self.assertTrue(not unreachable, + f"Locations were not reachable with all state before prefill: " + f"{unreachable}") From e96a752ff4c18b8a2a2fe5971d6d7c36e70552dc Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:12:13 -0500 Subject: [PATCH 09/51] fix alttp --- worlds/alttp/__init__.py | 43 +++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3cdbb1cb458a..f0e8d28e2b3d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -497,20 +497,20 @@ def collect_item(self, state: CollectionState, item: Item, remove=False): def pre_fill(self): from Fill import fill_restrictive, FillError attempts = 5 - world = self.multiworld - player = self.player - all_state = world.get_all_state(use_cache=True) + all_state = self.multiworld.get_all_state(use_cache=True).copy() crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']] - crystal_locations = [world.get_location('Turtle Rock - Prize', player), - world.get_location('Eastern Palace - Prize', player), - world.get_location('Desert Palace - Prize', player), - world.get_location('Tower of Hera - Prize', player), - world.get_location('Palace of Darkness - Prize', player), - world.get_location('Thieves\' Town - Prize', player), - world.get_location('Skull Woods - Prize', player), - world.get_location('Swamp Palace - Prize', player), - world.get_location('Ice Palace - Prize', player), - world.get_location('Misery Mire - Prize', player)] + for crystal in crystals: + all_state.remove(crystal) + crystal_locations = [self.multiworld.get_location('Turtle Rock - Prize', self.player), + self.multiworld.get_location('Eastern Palace - Prize', self.player), + self.multiworld.get_location('Desert Palace - Prize', self.player), + self.multiworld.get_location('Tower of Hera - Prize', self.player), + self.multiworld.get_location('Palace of Darkness - Prize', self.player), + self.multiworld.get_location('Thieves\' Town - Prize', self.player), + self.multiworld.get_location('Skull Woods - Prize', self.player), + self.multiworld.get_location('Swamp Palace - Prize', self.player), + self.multiworld.get_location('Ice Palace - Prize', self.player), + self.multiworld.get_location('Misery Mire - Prize', self.player)] placed_prizes = {loc.item.name for loc in crystal_locations if loc.item} unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes] empty_crystal_locations = [loc for loc in crystal_locations if not loc.item] @@ -518,8 +518,8 @@ def pre_fill(self): try: prizepool = unplaced_prizes.copy() prize_locs = empty_crystal_locations.copy() - world.random.shuffle(prize_locs) - fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True, + self.multiworld.random.shuffle(prize_locs) + fill_restrictive(self.multiworld, all_state, prize_locs, prizepool, True, lock=True, name="LttP Dungeon Prizes") except FillError as e: lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, @@ -530,10 +530,10 @@ def pre_fill(self): break else: raise FillError('Unable to place dungeon prizes') - if world.mode[player] == 'standard' and world.small_key_shuffle[player] \ - and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \ - world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons: - world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1 + if self.multiworld.mode[self.player] == 'standard' and self.multiworld.small_key_shuffle[self.player] \ + and self.multiworld.small_key_shuffle[self.player] != small_key_shuffle.option_universal and \ + self.multiworld.small_key_shuffle[self.player] != small_key_shuffle.option_own_dungeons: + self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 @classmethod def stage_pre_fill(cls, world): @@ -805,12 +805,15 @@ def get_filler_item_name(self) -> str: return GetBeemizerItem(self.multiworld, self.player, item) def get_pre_fill_items(self): - res = [] + res = [self.create_item(name) for name in ('Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', + 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', + 'Crystal 6')] if self.dungeon_local_item_names: for dungeon in self.dungeons.values(): for item in dungeon.all_items: if item.name in self.dungeon_local_item_names: res.append(item) + return res def fill_slot_data(self): From 091504b100ea04e75aedb39518a79e8bfeaeff68 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:33:19 -0500 Subject: [PATCH 10/51] add get_all_state arg, fix kh2 --- BaseClasses.py | 11 ++++++----- worlds/kh2/__init__.py | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 715732589b67..1aecaf976661 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -424,7 +424,7 @@ def get_entrance(self, entrance_name: str, player: int) -> Entrance: def get_location(self, location_name: str, player: int) -> Location: return self.regions.location_cache[player][location_name] - def get_all_state(self, use_cache: bool) -> CollectionState: + def get_all_state(self, use_cache: bool, collect_pre_fill_items: bool = True) -> CollectionState: cached = getattr(self, "_all_state", None) if use_cache and cached: return cached.copy() @@ -433,10 +433,11 @@ def get_all_state(self, use_cache: bool) -> CollectionState: for item in self.itempool: self.worlds[item.player].collect(ret, item) - for player in self.player_ids: - subworld = self.worlds[player] - for item in subworld.get_pre_fill_items(): - subworld.collect(ret, item) + if collect_pre_fill_items: + for player in self.player_ids: + subworld = self.worlds[player] + for item in subworld.get_pre_fill_items(): + subworld.collect(ret, item) ret.sweep_for_advancements() if use_cache: diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index faf0bed88567..b8da9da0fcc3 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -420,10 +420,14 @@ def keyblade_pre_fill(self): Fills keyblade slots with abilities determined on player's setting """ keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()] - state = self.multiworld.get_all_state(False) + state = self.multiworld.get_all_state(False, True) keyblade_ability_pool_copy = self.keyblade_ability_pool.copy() fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True, allow_excluded=True) + def get_pre_fill_items(self) -> List["Item"]: + return [self.create_item(item) for item in [*DonaldAbility_Table.keys(), *GoofyAbility_Table.keys(), + *SupportAbility_Table.keys()]] + def starting_invo_verify(self): """ Making sure the player doesn't put too many abilities in their starting inventory. From d8e9041e32174ac574bc6d6778d264bdaad6a501 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:41:21 -0500 Subject: [PATCH 11/51] fix blasphemous and hylics --- worlds/blasphemous/__init__.py | 16 +++++++++++++++- worlds/hylics2/__init__.py | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index b110c316da48..5d7a504002d7 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -214,7 +214,21 @@ def pre_fill(self): if self.options.thorn_shuffle == "local_only": self.options.local_items.value.add("Thorn Upgrade") - + + def get_pre_fill_items(self): + pool = [self.create_item(item) for item in unrandomized_dict.values()] + + if self.options.thorn_shuffle in ("vanilla", "local_only"): + pool.extend(self.create_item("Thorn Upgrade") for _ in thorn_set) + + if self.options.start_wheel: + pool.append(self.create_item("The Young Mason's Wheel")) + + if not self.options.skill_randomizer: + pool.extend(self.create_item(item) for item in skill_dict.values()) + + return pool + def place_items_from_set(self, location_set: Set[str], name: str): for loc in location_set: diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index 18bcb0edc143..572dfad46996 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -127,6 +127,9 @@ def pre_fill(self): tv = tvs.pop() self.get_location(tv).place_locked_item(self.create_item(gesture)) + def get_pre_fill_items(self) -> List["Item"]: + if self.options.gesture_shuffle: + return [self.create_item(gesture["name"]) for gesture in Items.gesture_item_table.values()] def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { From 6196c8592a655a3252ac345831085462557c1e3f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:55:07 -0500 Subject: [PATCH 12/51] fix emerald and incorrect kh2 --- worlds/kh2/__init__.py | 2 +- worlds/pokemon_emerald/__init__.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index b8da9da0fcc3..89d2dc3d4823 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -420,7 +420,7 @@ def keyblade_pre_fill(self): Fills keyblade slots with abilities determined on player's setting """ keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()] - state = self.multiworld.get_all_state(False, True) + state = self.multiworld.get_all_state(False, False) keyblade_ability_pool_copy = self.keyblade_ability_pool.copy() fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True, allow_excluded=True) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f572f..905c076375cf 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -489,7 +489,7 @@ def pre_fill(self) -> None: if location.progress_type == LocationProgressType.EXCLUDED \ else location.progress_type - collection_state = self.multiworld.get_all_state(False) + collection_state = self.multiworld.get_all_state(False, False) # If HM shuffle is on, HMs are not placed and not in the pool, so # `get_all_state` did not contain them. Collect them manually for @@ -548,7 +548,7 @@ def pre_fill(self) -> None: if location.progress_type == LocationProgressType.EXCLUDED \ else location.progress_type - collection_state = self.multiworld.get_all_state(False) + collection_state = self.multiworld.get_all_state(False, False) # In specific very constrained conditions, fill_restrictive may run # out of swaps before it finds a valid solution if it gets unlucky. @@ -568,6 +568,16 @@ def pre_fill(self) -> None: logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.") continue + def get_pre_fill_items(self) -> List[PokemonEmeraldItem]: + pool = [] + if self.options.badges == RandomizeBadges.option_shuffle: + pool.extend(badge for _, badge in self.badge_shuffle_info) + + if self.options.hms == RandomizeHms.option_shuffle: + pool.extend(hm for _, hm in self.hm_shuffle_info) + + return pool + def generate_output(self, output_directory: str) -> None: self.modified_trainers = copy.deepcopy(emerald_data.trainers) self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves) From 7f94d5f888e9658eb82dac3d8faefab550f8cead Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:42:48 -0500 Subject: [PATCH 13/51] fix pokemon rb? --- worlds/pokemon_rb/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index c1d843189820..4a32ade5e250 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -364,7 +364,7 @@ def pre_fill(self) -> None: # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not # fail. Re-use test_state from previous final loop. - all_state = self.multiworld.get_all_state(False) + all_state = self.multiworld.get_all_state(False, False) evolutions_region = self.multiworld.get_region("Evolution", self.player) for location in evolutions_region.locations.copy(): if not all_state.can_reach(location, player=self.player): @@ -420,7 +420,7 @@ def pre_fill(self) -> None: self.local_locs = locs - all_state = self.multiworld.get_all_state(False) + all_state = self.multiworld.get_all_state(False, False) reachable_mons = set() for mon in poke_data.pokemon_data: @@ -480,6 +480,12 @@ def pre_fill(self) -> None: else: raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location") + def get_pre_fill_items(self) -> typing.List["Item"]: + pool = [self.create_item(mon) for mon in poke_data.pokemon_data] + return pool + + + @classmethod def stage_post_fill(cls, multiworld): # Convert all but one of each instance of a wild Pokemon to useful classification. From 970a46970a3ab5f09a7ed7def157fdba2b6178f8 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:43:11 -0500 Subject: [PATCH 14/51] forgot the other hylics2 case --- worlds/hylics2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index 572dfad46996..f94d9c225373 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -130,6 +130,7 @@ def pre_fill(self): def get_pre_fill_items(self) -> List["Item"]: if self.options.gesture_shuffle: return [self.create_item(gesture["name"]) for gesture in Items.gesture_item_table.values()] + return [] def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { From 21d2fa0c270191f9194c7a0dfa687e945317739a Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:07:43 -0500 Subject: [PATCH 15/51] fix raft --- worlds/raft/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 71d5d1c7e44b..eab191fd2ce6 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -117,7 +117,10 @@ def create_regions(self): def get_pre_fill_items(self): if self.options.island_frequency_locations.is_filling_frequencies_in_world(): - return [loc.item for loc in self.multiworld.get_filled_locations()] + return [self.create_item(frequency) for frequency in ("Vasagatan Frequency", "Balboa Island Frequency", + "Caravan Island Frequency", "Tangaroa Frequency", + "Varuna Point Frequency", "Temperance Frequency", + "Utopia Frequency")] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: From 003368eb6ecf042b1de5771ea7a94a92e43363eb Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:21:45 -0500 Subject: [PATCH 16/51] fix shivers --- worlds/shivers/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 3ca87ae164f2..13bcd718e867 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -199,7 +199,7 @@ def pre_fill(self) -> None: storageitems += [self.create_item("Empty") for i in range(3)] - state = self.multiworld.get_all_state(True) + state = self.multiworld.get_all_state(True, False) self.random.shuffle(storagelocs) self.random.shuffle(storageitems) @@ -208,6 +208,21 @@ def pre_fill(self) -> None: self.storage_placements = {location.name: location.item.name for location in storagelocs} + def get_pre_fill_items(self) -> List["Item"]: + if self.options.full_pots == "pieces": + return [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i]) for i in range(20)] + elif self.options.full_pots == "complete": + return [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i]) for i in range(10)] + else: + pool = [] + for i in range(10): + if self.pot_completed_list[i] == 0: + pool.extend([self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i]), + self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])]) + else: + pool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])) + return pool + set_rules = set_rules def fill_slot_data(self) -> dict: From 3802767064922be9475023f561a3ce9b24739ce4 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:27:45 -0500 Subject: [PATCH 17/51] remove blasphemous changes --- worlds/blasphemous/__init__.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 6d674c526c82..f6c4492e1610 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -213,21 +213,6 @@ def create_items(self): if self.options.thorn_shuffle == "local_only": self.options.local_items.value.add("Thorn Upgrade") - def get_pre_fill_items(self): - pool = [self.create_item(item) for item in unrandomized_dict.values()] - - if self.options.thorn_shuffle in ("vanilla", "local_only"): - pool.extend(self.create_item("Thorn Upgrade") for _ in thorn_set) - - if self.options.start_wheel: - pool.append(self.create_item("The Young Mason's Wheel")) - - if not self.options.skill_randomizer: - pool.extend(self.create_item(item) for item in skill_dict.values()) - - return pool - - def place_items_from_set(self, location_set: Set[str], name: str): for loc in location_set: self.get_location(loc).place_locked_item(self.create_item(name)) From f9ffe01005730ae9f367a04b12ec60fabcfd09fe Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:28:07 -0500 Subject: [PATCH 18/51] Update __init__.py --- worlds/blasphemous/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index f6c4492e1610..559316b54f1e 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -213,6 +213,7 @@ def create_items(self): if self.options.thorn_shuffle == "local_only": self.options.local_items.value.add("Thorn Upgrade") + def place_items_from_set(self, location_set: Set[str], name: str): for loc in location_set: self.get_location(loc).place_locked_item(self.create_item(name)) From 5047eacef5b44f830e64756c56e8b6f9366fe282 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:39:34 -0500 Subject: [PATCH 19/51] fix oot --- worlds/oot/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b93f60b2a08e..187c133cc19f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from settings import get_settings from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections +from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections, PlandoItems from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from worlds.AutoWorld import World, AutoLogicRegister, WebWorld @@ -214,6 +214,8 @@ def generate_early(self): option_value = result.value elif isinstance(result, PlandoConnections): option_value = result.value + elif isinstance(result, PlandoItems): + option_value = result.value else: option_value = result.current_key setattr(self, option_name, option_value) From 6fa6d13b1d7ba3040a79b661e99af8a4164a6023 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:47:54 -0400 Subject: [PATCH 20/51] . From acec5131615619df1e2402de9bbcc5e8bb9e6731 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:04:07 -0400 Subject: [PATCH 21/51] Changes from some review (untested) --- Fill.py | 24 ++++++++++++++++-------- Options.py | 24 ++++++++++++++---------- test/general/test_implemented.py | 3 +-- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Fill.py b/Fill.py index a805aec110ef..b3ee665d465b 100644 --- a/Fill.py +++ b/Fill.py @@ -5,7 +5,7 @@ from collections import Counter, deque from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld -from Options import Accessibility, PlandoItem +from Options import Accessibility from worlds.AutoWorld import call_all from worlds.generic.Rules import add_item_rule @@ -817,13 +817,13 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: def distribute_planned(multiworld: MultiWorld) -> None: def warn(warning: str, force: typing.Union[bool, str]) -> None: - if force in [True, "fail", "failure", "none", False, "warn", "warning"]: + if isinstance(force, bool): logging.warning(f"{warning}") else: logging.debug(f"{warning}") def failed(warning: str, force: typing.Union[bool, str]) -> None: - if force in [True, "fail", "failure"]: + if force is True: raise Exception(warning) else: warn(warning, force) @@ -854,6 +854,9 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: new_block["force"] = block.force target_world = block.world + if not (isinstance(block.force, bool) or block.force == "silent"): + raise Exception(f"Plando `force` has to be boolean or `silent`, not {block.force} for player {player}") + if target_world is False or multiworld.players == 1: # target own world worlds: typing.Set[int] = {player} elif target_world is True: # target any worlds besides own @@ -895,10 +898,18 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: items = [items] new_block["items"] = items - locations: block_value = block.locations + locations: typing.List[str] = block.locations if isinstance(locations, str): locations = [locations] + elif not isinstance(locations, list): + locations_type = type(locations) + raise Exception(f"Plando 'locations' has to be a list, not {locations_type} for player {player}.") + locations_from_groups: typing.List[str] = [] + for target_player in worlds: + for group in multiworld.worlds[target_player].location_name_groups: + if group in locations: + locations_from_groups.extend(multiworld.worlds[target_player].location_name_groups[group]) if "early_locations" in locations: locations.remove("early_locations") for target_player in worlds: @@ -907,10 +918,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: locations.remove("non_early_locations") for target_player in worlds: locations += non_early_locations[target_player] - for target_player in worlds: - for group in multiworld.worlds[target_player].location_name_groups: - if group in locations: - locations.extend(multiworld.worlds[target_player].location_name_groups[group]) + locations += locations_from_groups new_block["locations"] = list(dict.fromkeys(locations)) diff --git a/Options.py b/Options.py index 3eb297b9f69d..165e0996cce4 100644 --- a/Options.py +++ b/Options.py @@ -15,6 +15,7 @@ from schema import And, Optional, Or, Schema from typing_extensions import Self +from Generate import roll_percentage from Utils import get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: @@ -971,7 +972,7 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: if isinstance(data, typing.Iterable): for text in data: if isinstance(text, typing.Mapping): - if random.random() < float(text.get("percentage", 100)/100): + if roll_percentage(text.get("percentage", 100)): at = text.get("at", None) if at is not None: if isinstance(at, dict): @@ -997,7 +998,7 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: else: raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): - if random.random() < float(text.percentage/100): + if roll_percentage(text.percentage): texts.append(text) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") @@ -1121,7 +1122,7 @@ def from_any(cls, data: PlandoConFromAnyType) -> Self: for connection in data: if isinstance(connection, typing.Mapping): percentage = connection.get("percentage", 100) - if random.random() < float(percentage / 100): + if roll_percentage(percentage): entrance = connection.get("entrance", None) if is_iterable_except_str(entrance): entrance = random.choice(sorted(entrance)) @@ -1139,7 +1140,7 @@ def from_any(cls, data: PlandoConFromAnyType) -> Self: percentage )) elif isinstance(connection, PlandoConnection): - if random.random() < float(connection.percentage / 100): + if roll_percentage(connection.percentage): value.append(connection) else: raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") @@ -1411,7 +1412,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class PlandoItem(typing.NamedTuple): items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] - locations: typing.Optional[typing.List[str]] + locations: typing.List[str] = [] world: typing.Union[int, str, bool, None, typing.Iterable[str], typing.Set[int]] = False from_pool: bool = True force: typing.Union[bool, typing.Literal["silent"]] = "silent" @@ -1437,18 +1438,21 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: value: typing.List[PlandoItem] = [] for item in data: if isinstance(item, typing.Mapping): + if not isinstance(item.get("percentage", 100), int): + percentage_type = type(item["percentage"]) + raise Exception(f"Plando `percentage` has to be int, not {percentage_type}.") percentage = item.get("percentage", 100) - if random.random() < float(percentage / 100): + if roll_percentage(percentage): count = item.get("count", False) items = item.get("items", []) if not items: - items = item.get("item", None) # explcitly throw an error here if not present + items = item.get("item", None) # explicitly throw an error here if not present if not items: raise Exception("You must specify at least one item to place items with plando.") items = [items] - locations = item.get("locations", []) + locations = item["locations"] if not locations: - locations = item.get("location", None) + locations = item.get("location", []) if locations: locations = [locations] world = item.get("world", False) @@ -1456,7 +1460,7 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: force = item.get("force", "silent") value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) elif isinstance(item, PlandoItem): - if random.random() < float(item.percentage / 100): + if roll_percentage(item.percentage): value.append(item) else: raise Exception(f"Cannot create plando item from non-Dict type, got {type(item)}.") diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 873d5f807a37..12eaa13d4675 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -1,8 +1,7 @@ import unittest -from Fill import distribute_items_restrictive, distribute_planned +from Fill import distribute_items_restrictive from NetUtils import encode -from Options import PlandoItem from worlds.AutoWorld import AutoWorldRegister, call_all from worlds import failed_world_loads from . import setup_solo_multiworld From 6fbe885b1ffd490427b56a6423065af1925fd313 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:10:47 -0400 Subject: [PATCH 22/51] Import doesn't work --- Options.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 165e0996cce4..69760df93e55 100644 --- a/Options.py +++ b/Options.py @@ -15,7 +15,6 @@ from schema import And, Optional, Or, Schema from typing_extensions import Self -from Generate import roll_percentage from Utils import get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: @@ -23,6 +22,11 @@ from worlds.AutoWorld import World import pathlib +def roll_percentage(percentage: typing.Union[int, float]) -> bool: + """Roll a percentage chance. + percentage is expected to be in range [0, 100]""" + # Copied from Generate.py + return random.random() < (float(percentage) / 100) class OptionError(ValueError): pass From 3c107e679bf515b68dc154a485260c6ecd8626bb Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:11:00 -0400 Subject: [PATCH 23/51] Import doesn't work --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index 69760df93e55..962f3d5e5dfd 100644 --- a/Options.py +++ b/Options.py @@ -22,12 +22,14 @@ from worlds.AutoWorld import World import pathlib + def roll_percentage(percentage: typing.Union[int, float]) -> bool: """Roll a percentage chance. percentage is expected to be in range [0, 100]""" # Copied from Generate.py return random.random() < (float(percentage) / 100) + class OptionError(ValueError): pass From 00f915b95c69b896641b91f33c957bea2f6c88c1 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:22:02 -0400 Subject: [PATCH 24/51] Reverting the default change --- Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options.py b/Options.py index 962f3d5e5dfd..af193c1fd777 100644 --- a/Options.py +++ b/Options.py @@ -1418,7 +1418,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class PlandoItem(typing.NamedTuple): items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] - locations: typing.List[str] = [] + locations: typing.List[str] world: typing.Union[int, str, bool, None, typing.Iterable[str], typing.Set[int]] = False from_pool: bool = True force: typing.Union[bool, typing.Literal["silent"]] = "silent" @@ -1456,7 +1456,7 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: if not items: raise Exception("You must specify at least one item to place items with plando.") items = [items] - locations = item["locations"] + locations = item.get("locations", []) if not locations: locations = item.get("location", []) if locations: From 5f7a084425f884729f8792534ff53d1fdffca94a Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:25:23 -0400 Subject: [PATCH 25/51] Cleaner exception method --- Options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index af193c1fd777..e77398166446 100644 --- a/Options.py +++ b/Options.py @@ -1444,10 +1444,10 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: value: typing.List[PlandoItem] = [] for item in data: if isinstance(item, typing.Mapping): - if not isinstance(item.get("percentage", 100), int): - percentage_type = type(item["percentage"]) - raise Exception(f"Plando `percentage` has to be int, not {percentage_type}.") percentage = item.get("percentage", 100) + if not isinstance(percentage, int): + percentage_type = type(percentage) + raise Exception(f"Plando `percentage` has to be int, not {percentage_type}.") if roll_percentage(percentage): count = item.get("count", False) items = item.get("items", []) From 6c4126dceb511b6871eb6873fdc4f15a1966edb9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:28:55 -0400 Subject: [PATCH 26/51] Update Fill.py Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> --- Fill.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index b3ee665d465b..ffc8738973d8 100644 --- a/Fill.py +++ b/Fill.py @@ -902,8 +902,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: if isinstance(locations, str): locations = [locations] elif not isinstance(locations, list): - locations_type = type(locations) - raise Exception(f"Plando 'locations' has to be a list, not {locations_type} for player {player}.") + raise Exception(f"Plando 'locations' has to be a list, not {type(locations)} for player {player}.") locations_from_groups: typing.List[str] = [] for target_player in worlds: From deb8a090d4a015cd21bd476d550144bec41ded06 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 15:55:57 -0400 Subject: [PATCH 27/51] Some recommended fixes --- Fill.py | 6 ++---- Generate.py | 10 ++-------- Options.py | 4 +--- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Fill.py b/Fill.py index ffc8738973d8..5de782d0d24e 100644 --- a/Fill.py +++ b/Fill.py @@ -841,15 +841,13 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: world_name_lookup = multiworld.world_name_lookup - block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any]] plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] player_ids = set(multiworld.player_ids) for player in player_ids: for block in multiworld.worlds[player].options.plando_items: new_block: typing.Dict[str, typing.Any] = {"player": player} if not isinstance(block.from_pool, bool): - from_pool_type = type(block.from_pool) - raise Exception(f"Plando 'from_pool' has to be boolean, not {from_pool_type} for player {player}.") + raise Exception(f"Plando 'from_pool' has to be boolean, not {type(block.from_pool)} for player {player}.") new_block["from_pool"] = block.from_pool new_block["force"] = block.force target_world = block.world @@ -886,7 +884,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: worlds = {world_name_lookup[target_world]} new_block["world"] = worlds - items: block_value = block.items + items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] = block.items if isinstance(items, dict): item_list: typing.List[str] = [] for key, value in items.items(): diff --git a/Generate.py b/Generate.py index fbbeb943bd4a..d341dcb6a27f 100644 --- a/Generate.py +++ b/Generate.py @@ -291,12 +291,6 @@ def handle_name(name: str, player: int, name_counter: Counter): return new_name -def roll_percentage(percentage: Union[int, float]) -> bool: - """Roll a percentage chance. - percentage is expected to be in range [0, 100]""" - return random.random() < (float(percentage) / 100) - - def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict: logging.debug(f'Applying {new_weights}') cleaned_weights = {} @@ -362,7 +356,7 @@ def roll_linked_options(weights: dict) -> dict: if "name" not in option_set: raise ValueError("One of your linked options does not have a name.") try: - if roll_percentage(option_set["percentage"]): + if Options.roll_percentage(option_set["percentage"]): logging.debug(f"Linked option {option_set['name']} triggered.") new_options = option_set["options"] for category_name, category_options in new_options.items(): @@ -395,7 +389,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict: trigger_result = get_choice("option_result", option_set) result = get_choice(key, currently_targeted_weights) currently_targeted_weights[key] = result - if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)): + if result == trigger_result and Options.roll_percentage(get_choice("percentage", option_set, 100)): for category_name, category_options in option_set["options"].items(): currently_targeted_weights = weights if category_name: diff --git a/Options.py b/Options.py index e77398166446..62d2feb1133f 100644 --- a/Options.py +++ b/Options.py @@ -26,7 +26,6 @@ def roll_percentage(percentage: typing.Union[int, float]) -> bool: """Roll a percentage chance. percentage is expected to be in range [0, 100]""" - # Copied from Generate.py return random.random() < (float(percentage) / 100) @@ -1446,8 +1445,7 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: if isinstance(item, typing.Mapping): percentage = item.get("percentage", 100) if not isinstance(percentage, int): - percentage_type = type(percentage) - raise Exception(f"Plando `percentage` has to be int, not {percentage_type}.") + raise Exception(f"Plando `percentage` has to be int, not {type(percentage)}.") if roll_percentage(percentage): count = item.get("count", False) items = item.get("items", []) From fdcec1a44c24d3165b661387c38324f0d7d1a2b7 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:52:11 -0500 Subject: [PATCH 28/51] Plando items fixes and item_group_method --- Options.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index 81e6497855a3..bd3b827049e2 100644 --- a/Options.py +++ b/Options.py @@ -1423,6 +1423,7 @@ class PlandoItem(typing.NamedTuple): force: typing.Union[bool, typing.Literal["silent"]] = "silent" count: typing.Union[int, bool, typing.Dict[str, int]] = False percentage: int = 100 + item_group_method: typing.Literal["all", "even", "random"] = "all" class PlandoItems(Option[typing.List[PlandoItem]]): @@ -1443,9 +1444,15 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: value: typing.List[PlandoItem] = [] for item in data: if isinstance(item, typing.Mapping): + item_group_method = item.get("item_group_method", "all") + if item_group_method not in ("all", "even", "random"): + raise Exception(f"Plando `item_group_method` has to be \"all\", \"even\", or \"random\", " + f"not {item_group_method}") percentage = item.get("percentage", 100) if not isinstance(percentage, int): raise Exception(f"Plando `percentage` has to be int, not {type(percentage)}.") + if not (0 <= percentage <= 100): + raise Exception(f"Plando `percentage` has to be between 0 and 100 (inclusive) not {percentage}.") if roll_percentage(percentage): count = item.get("count", False) items = item.get("items", []) @@ -1453,16 +1460,25 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: items = item.get("item", None) # explicitly throw an error here if not present if not items: raise Exception("You must specify at least one item to place items with plando.") - items = [items] + if isinstance(items, str): + items = [items] + elif isinstance(items, dict): + count = 1 + else: + raise Exception(f"Plando 'item' has to be string or dictionary, not {type(items)}.") locations = item.get("locations", []) if not locations: locations = item.get("location", []) - if locations: + if isinstance(locations, str): locations = [locations] + if isinstance(locations, list) and locations: + count = 1 + elif not isinstance(locations, list): + raise Exception(f"Plando `location` has to be string or list, not {type(locations)}") world = item.get("world", False) from_pool = item.get("from_pool", True) force = item.get("force", "silent") - value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) + value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage, item_group_method)) elif isinstance(item, PlandoItem): if roll_percentage(item.percentage): value.append(item) @@ -1487,7 +1503,22 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P for item in items_copy: if item in world.item_name_groups: value = plando.items.pop(item) - plando.items.update({key: value for key in world.item_name_groups[item]}) + group = sorted(world.item_name_groups[item]) + for group_item in group: + if group_item in plando.items: + raise Exception(f"Plando `items` contains both \"{group_item}\" and the group " + f"\"{item}\" which contains it. It cannot have both.") + if plando.item_group_method == "all": + plando.items.update({key: value for key in group}) + elif plando.item_group_method == "even": + group_size = len(group) + plando.items.update({key: value // group_size for key in group}) + for key in group[:value % group_size]: + plando.items[key] += 1 + else: # random + plando.items.update({key: 0 for key in group}) + for key in random.choices(group, k=value): + plando.items[key] += 1 else: assert isinstance(plando.items, list) # pycharm can't figure out the hinting without the hint for item in items_copy: From 1a684136fd03ceec0c79bf0547669ccfe155fe5d Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Thu, 21 Nov 2024 16:11:30 -0500 Subject: [PATCH 29/51] Just the review stuff --- Options.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Options.py b/Options.py index bd3b827049e2..14041bfc4d19 100644 --- a/Options.py +++ b/Options.py @@ -1423,7 +1423,6 @@ class PlandoItem(typing.NamedTuple): force: typing.Union[bool, typing.Literal["silent"]] = "silent" count: typing.Union[int, bool, typing.Dict[str, int]] = False percentage: int = 100 - item_group_method: typing.Literal["all", "even", "random"] = "all" class PlandoItems(Option[typing.List[PlandoItem]]): @@ -1444,10 +1443,6 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: value: typing.List[PlandoItem] = [] for item in data: if isinstance(item, typing.Mapping): - item_group_method = item.get("item_group_method", "all") - if item_group_method not in ("all", "even", "random"): - raise Exception(f"Plando `item_group_method` has to be \"all\", \"even\", or \"random\", " - f"not {item_group_method}") percentage = item.get("percentage", 100) if not isinstance(percentage, int): raise Exception(f"Plando `percentage` has to be int, not {type(percentage)}.") @@ -1478,7 +1473,7 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: world = item.get("world", False) from_pool = item.get("from_pool", True) force = item.get("force", "silent") - value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage, item_group_method)) + value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) elif isinstance(item, PlandoItem): if roll_percentage(item.percentage): value.append(item) @@ -1508,17 +1503,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P if group_item in plando.items: raise Exception(f"Plando `items` contains both \"{group_item}\" and the group " f"\"{item}\" which contains it. It cannot have both.") - if plando.item_group_method == "all": - plando.items.update({key: value for key in group}) - elif plando.item_group_method == "even": - group_size = len(group) - plando.items.update({key: value // group_size for key in group}) - for key in group[:value % group_size]: - plando.items[key] += 1 - else: # random - plando.items.update({key: 0 for key in group}) - for key in random.choices(group, k=value): - plando.items[key] += 1 + plando.items.update({key: value for key in group}) else: assert isinstance(plando.items, list) # pycharm can't figure out the hinting without the hint for item in items_copy: From a9220f535227958c304d930a59481fdc415997a7 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Thu, 21 Nov 2024 16:57:00 -0500 Subject: [PATCH 30/51] Changing the item/location validation --- Options.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Options.py b/Options.py index 14041bfc4d19..1921319df451 100644 --- a/Options.py +++ b/Options.py @@ -1455,20 +1455,19 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: items = item.get("item", None) # explicitly throw an error here if not present if not items: raise Exception("You must specify at least one item to place items with plando.") + count = 1 if isinstance(items, str): items = [items] - elif isinstance(items, dict): - count = 1 - else: + elif not isinstance(items, dict): raise Exception(f"Plando 'item' has to be string or dictionary, not {type(items)}.") locations = item.get("locations", []) if not locations: locations = item.get("location", []) + if locations: + count = 1 if isinstance(locations, str): locations = [locations] - if isinstance(locations, list) and locations: - count = 1 - elif not isinstance(locations, list): + if not isinstance(locations, list): raise Exception(f"Plando `location` has to be string or list, not {type(locations)}") world = item.get("world", False) from_pool = item.get("from_pool", True) From a30030a38cfc3e8a3e77e366aec2a37882403671 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:39:49 -0600 Subject: [PATCH 31/51] Apply suggestions from code review Co-authored-by: Doug Hoskisson --- Options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Options.py b/Options.py index bb439e3ccc0a..c87ea48c4c4b 100644 --- a/Options.py +++ b/Options.py @@ -23,7 +23,7 @@ import pathlib -def roll_percentage(percentage: typing.Union[int, float]) -> bool: +def roll_percentage(percentage: int | float) -> bool: """Roll a percentage chance. percentage is expected to be in range [0, 100]""" return random.random() < (float(percentage) / 100) @@ -1419,12 +1419,12 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P class PlandoItem(typing.NamedTuple): - items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] - locations: typing.List[str] - world: typing.Union[int, str, bool, None, typing.Iterable[str], typing.Set[int]] = False + items: list[str] | dict[str, typing.Any] + locations: list[str] + world: int | str | bool | None | typing.Iterable[str] | set[int] = False from_pool: bool = True - force: typing.Union[bool, typing.Literal["silent"]] = "silent" - count: typing.Union[int, bool, typing.Dict[str, int]] = False + force: bool | typing.Literal["silent"] = "silent" + count: int | bool | dict[str, int] = False percentage: int = 100 From 2794b9ac71971a13c166afc2781869fc144610f0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:38:25 -0600 Subject: [PATCH 32/51] convert plando item to dataclass maybe do this for the others? out of scope here though --- Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index c87ea48c4c4b..7bfdd1f66d56 100644 --- a/Options.py +++ b/Options.py @@ -1418,7 +1418,8 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P link["item_pool"] = list(pool) -class PlandoItem(typing.NamedTuple): +@dataclass(frozen=True) +class PlandoItem: items: list[str] | dict[str, typing.Any] locations: list[str] world: int | str | bool | None | typing.Iterable[str] | set[int] = False From 1a7dfae8ec9ff9b0c91c163845c0af0b60b7f46b Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:45:19 -0600 Subject: [PATCH 33/51] Update entrance_rando.py --- entrance_rando.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrance_rando.py b/entrance_rando.py index 5aa16fa0bb06..179c278ae02b 100644 --- a/entrance_rando.py +++ b/entrance_rando.py @@ -151,7 +151,7 @@ def __init__(self, world: World, coupled: bool): self.pairings = [] self.world = world self.coupled = coupled - self.collection_state = world.multiworld.get_all_state(False, True) + self.collection_state = world.multiworld.get_all_state(False, True, True) @property def placed_regions(self) -> set[Region]: From 19112098d5328ec05b9f54f20727d51ef25835bb Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:45:55 -0600 Subject: [PATCH 34/51] remove raft prefill items --- worlds/raft/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 15caec0cc1c6..3e33b417c04b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -114,14 +114,6 @@ def set_rules(self): def create_regions(self): create_regions(self.multiworld, self.player) - def get_pre_fill_items(self): - if self.options.island_frequency_locations.is_filling_frequencies_in_world(): - return [self.create_item(frequency) for frequency in ("Vasagatan Frequency", "Balboa Island Frequency", - "Caravan Island Frequency", "Tangaroa Frequency", - "Varuna Point Frequency", "Temperance Frequency", - "Utopia Frequency")] - return [] - def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) From 5ee9965b8564f6620c248e641e35e988c5ca6b1e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:02:09 -0600 Subject: [PATCH 35/51] fix shivers prefill items --- worlds/shivers/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index ee8f56850551..0864e7c73b3b 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -251,9 +251,9 @@ def pre_fill(self) -> None: def get_pre_fill_items(self) -> List["Item"]: if self.options.full_pots == "pieces": - return [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i]) for i in range(20)] + return [self.create_item(self.item_id_to_name[SHIVERS_ITEM_ID_OFFSET + i]) for i in range(20)] elif self.options.full_pots == "complete": - return [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i]) for i in range(10)] + return [self.create_item(self.item_id_to_name[SHIVERS_ITEM_ID_OFFSET + 20 + i]) for i in range(10)] else: pool = [] for i in range(10): From c51a43fe5d898e0175e7fdce13541e496d74d540 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:05:02 -0600 Subject: [PATCH 36/51] pep8 --- worlds/shivers/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 0864e7c73b3b..5455cd2b95e4 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -264,7 +264,6 @@ def get_pre_fill_items(self) -> List["Item"]: pool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])) return pool - def fill_slot_data(self) -> dict: return { "StoragePlacements": self.storage_placements, From 640608b6704e6d38a955055acad5c949e93d84dc Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 31 Dec 2024 23:53:46 -0600 Subject: [PATCH 37/51] actually fix shivers --- worlds/shivers/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 5455cd2b95e4..34ae23e9073a 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -249,19 +249,25 @@ def pre_fill(self) -> None: self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for location in storage_locs} - def get_pre_fill_items(self) -> List["Item"]: + def get_pre_fill_items(self) -> List[Item]: if self.options.full_pots == "pieces": - return [self.create_item(self.item_id_to_name[SHIVERS_ITEM_ID_OFFSET + i]) for i in range(20)] + return [self.create_item(name) for name, data in item_table.items() if + data.type == ItemType.POT_DUPLICATE] elif self.options.full_pots == "complete": - return [self.create_item(self.item_id_to_name[SHIVERS_ITEM_ID_OFFSET + 20 + i]) for i in range(10)] + return [self.create_item(name) for name, data in item_table.items() if + data.type == ItemType.POT_COMPELTE_DUPLICATE] else: pool = [] + pieces = [self.create_item(name) for name, data in item_table.items() if + data.type == ItemType.POT_DUPLICATE] + complete = [self.create_item(name) for name, data in item_table.items() if + data.type == ItemType.POT_COMPELTE_DUPLICATE] for i in range(10): if self.pot_completed_list[i] == 0: - pool.extend([self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i]), - self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])]) + pool.append(pieces[i]) + pool.append(pieces[i + 10]) else: - pool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])) + pool.append(complete[i]) return pool def fill_slot_data(self) -> dict: From 9ff8bf187d76fc4fd8faae381dd7160133879388 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 1 Jan 2025 01:41:25 -0600 Subject: [PATCH 38/51] three stage plando items --- BaseClasses.py | 4 +++ Fill.py | 80 ++++++++++++++++++++++++++++++++------------------ Main.py | 13 ++++---- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1b50fe3c4567..18c784990cfc 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -83,6 +83,8 @@ class MultiWorld(): start_location_hints: Dict[int, Options.StartLocationHints] item_links: Dict[int, Options.ItemLinks] + plando_item_blocks: Dict[int, Any] + game: Dict[int, str] random: random.Random @@ -160,6 +162,7 @@ def __init__(self, players: int): self.local_early_items = {player: {} for player in self.player_ids} self.indirect_connections = {} self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} + self.plando_item_blocks = {} for player in range(1, players + 1): def set_player_attr(attr: str, val) -> None: @@ -167,6 +170,7 @@ def set_player_attr(attr: str, val) -> None: set_player_attr('plando_items', []) set_player_attr('plando_texts', {}) set_player_attr('plando_connections', []) + set_player_attr('plando_item_blocks', []) set_player_attr('game', "Archipelago") set_player_attr('completion_condition', lambda state: True) self.worlds = {} diff --git a/Fill.py b/Fill.py index c093ccf62ee5..2aec705de2f0 100644 --- a/Fill.py +++ b/Fill.py @@ -837,7 +837,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: location_2.item.location = location_2 -def distribute_planned(multiworld: MultiWorld) -> None: +def parse_planned_blocks(multiworld: MultiWorld) -> typing.Dict[int, typing.List[typing.Dict[str, typing.Any]]]: def warn(warning: str, force: typing.Union[bool, str]) -> None: if isinstance(force, bool): logging.warning(f"{warning}") @@ -850,22 +850,12 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: else: warn(warning, force) - swept_state = multiworld.state.copy() - swept_state.sweep_for_advancements() - reachable = frozenset(multiworld.get_reachable_locations(swept_state)) - early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) - non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) - for loc in multiworld.get_unfilled_locations(): - if loc in reachable: - early_locations[loc.player].append(loc.name) - else: # not reachable with swept state - non_early_locations[loc.player].append(loc.name) - world_name_lookup = multiworld.world_name_lookup - plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] + plando_blocks: typing.Dict[int, typing.List[typing.Dict[str, typing.Any]]] = dict() player_ids = set(multiworld.player_ids) for player in player_ids: + plando_blocks[player] = [] for block in multiworld.worlds[player].options.plando_items: new_block: typing.Dict[str, typing.Any] = {"player": player} if not isinstance(block.from_pool, bool): @@ -925,21 +915,16 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: raise Exception(f"Plando 'locations' has to be a list, not {type(locations)} for player {player}.") locations_from_groups: typing.List[str] = [] + resolved_locations: typing.List[Location] = [] for target_player in worlds: + world_locations = multiworld.get_locations(target_player) for group in multiworld.worlds[target_player].location_name_groups: if group in locations: locations_from_groups.extend(multiworld.worlds[target_player].location_name_groups[group]) - if "early_locations" in locations: - locations.remove("early_locations") - for target_player in worlds: - locations += early_locations[target_player] - if "non_early_locations" in locations: - locations.remove("non_early_locations") - for target_player in worlds: - locations += non_early_locations[target_player] - locations += locations_from_groups - - new_block["locations"] = list(dict.fromkeys(locations)) + resolved_locations.extend(location for location in world_locations + if location.name in [*locations, *locations_from_groups]) + new_block["locations"] = sorted(dict.fromkeys(locations)) + new_block["resolved_locations"] = sorted(set(resolved_locations)) new_block["count"] = block.count if not new_block["count"]: @@ -968,20 +953,59 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: new_block["count"]["max"]) if new_block["count"]["target"] > 0: - plando_blocks.append(new_block) + plando_blocks[player].append(new_block) + + return plando_blocks + +def resolve_early_locations_for_planned(multiworld: MultiWorld): + swept_state = multiworld.state.copy() + swept_state.sweep_for_advancements() + reachable = frozenset(multiworld.get_reachable_locations(swept_state)) + early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) + non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) + for loc in multiworld.get_unfilled_locations(): + if loc in reachable: + early_locations[loc.player].append(loc.name) + else: # not reachable with swept state + non_early_locations[loc.player].append(loc.name) + + for player in multiworld.plando_item_blocks: + for block in multiworld.plando_item_blocks[player]: + locations = block["locations"] + resolved_locations = block["resolved_locations"] + worlds = block["world"] + if "early_locations" in locations: + for target_player in worlds: + resolved_locations += early_locations[target_player] + if "non_early_locations" in locations: + for target_player in worlds: + resolved_locations += non_early_locations[target_player] + +def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: typing.List[typing.Dict[str, typing.Any]]): + def warn(warning: str, force: typing.Union[bool, str]) -> None: + if isinstance(force, bool): + logging.warning(f"{warning}") + else: + logging.debug(f"{warning}") + + def failed(warning: str, force: typing.Union[bool, str]) -> None: + if force is True: + raise Exception(warning) + else: + warn(warning, force) # shuffle, but then sort blocks by number of locations minus number of items, # so less-flexible blocks get priority multiworld.random.shuffle(plando_blocks) - plando_blocks.sort(key=lambda block: (len(block["locations"]) - block["count"]["target"] - if len(block["locations"]) > 0 + plando_blocks.sort(key=lambda block: (len(block["resolved_locations"]) - block["count"]["target"] + if len(block["resolved_locations"]) > 0 else len(multiworld.get_unfilled_locations(player)) - block["count"]["target"])) for placement in plando_blocks: player = placement["player"] try: worlds = placement["world"] - locations = placement["locations"] + locations = placement["resolved_locations"] items = placement["items"] maxcount = placement["count"]["target"] from_pool = placement["from_pool"] diff --git a/Main.py b/Main.py index d105bd4ad0e5..6287f11e1ffc 100644 --- a/Main.py +++ b/Main.py @@ -11,8 +11,8 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region -from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ - flood_items +from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \ + parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings @@ -148,7 +148,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.local_items.value = set() - + + multiworld.plando_item_blocks = parse_planned_blocks(multiworld) + AutoWorld.call_all(multiworld, "generate_basic") # remove starting inventory from pool items. @@ -192,8 +194,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld._all_state = None logger.info("Running Item Plando.") - - distribute_planned(multiworld) + resolve_early_locations_for_planned(multiworld) + distribute_planned_blocks(multiworld, [x for player in multiworld.plando_item_blocks + for x in multiworld.plando_item_blocks[player]]) logger.info('Running Pre Main Fill.') From e57dda43bea51556d00dbefa46b4c1b5ba03e7e7 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 1 Jan 2025 02:04:21 -0600 Subject: [PATCH 39/51] fix ladx and early --- Fill.py | 13 ++++++------- worlds/ladx/test/testShop.py | 6 ++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Fill.py b/Fill.py index 2aec705de2f0..983228bae239 100644 --- a/Fill.py +++ b/Fill.py @@ -961,13 +961,13 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld): swept_state = multiworld.state.copy() swept_state.sweep_for_advancements() reachable = frozenset(multiworld.get_reachable_locations(swept_state)) - early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) - non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) + early_locations: typing.Dict[int, typing.List[Location]] = collections.defaultdict(list) + non_early_locations: typing.Dict[int, typing.List[Location]] = collections.defaultdict(list) for loc in multiworld.get_unfilled_locations(): if loc in reachable: - early_locations[loc.player].append(loc.name) + early_locations[loc.player].append(loc) else: # not reachable with swept state - non_early_locations[loc.player].append(loc.name) + non_early_locations[loc.player].append(loc) for player in multiworld.plando_item_blocks: for block in multiworld.plando_item_blocks[player]: @@ -1036,9 +1036,8 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: continue else: is_real = item_candidates[0].code is not None - candidates = [candidate for candidate in multiworld.get_unfilled_locations_for_players(locations, - sorted(worlds)) - if bool(candidate.address) == is_real] + candidates = [candidate for candidate in locations if candidate.item is None + and bool(candidate.address) == is_real] multiworld.random.shuffle(candidates) allstate = multiworld.get_all_state(False) mincount = placement["count"]["min"] diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py index 24ab70f1ad7c..6cdedfeeed24 100644 --- a/worlds/ladx/test/testShop.py +++ b/worlds/ladx/test/testShop.py @@ -1,6 +1,6 @@ from typing import Optional -from Fill import distribute_planned +from Fill import parse_planned_blocks, distribute_planned_blocks from Options import PlandoItems from test.general import setup_solo_multiworld from worlds.AutoWorld import call_all @@ -27,7 +27,9 @@ def world_setup(self, seed: Optional[int] = None) -> None: ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") ) self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"]) - distribute_planned(self.multiworld) + self.multiworld.plando_item_blocks = parse_planned_blocks(self.multiworld) + distribute_planned_blocks(self.multiworld, [x for player in self.multiworld.plando_item_blocks + for x in self.multiworld.plando_item_blocks[player]]) call_all(self.multiworld, "pre_fill") def test_planned(self): From d2597f0c234c1d81ea8eb0517bf8d0e80c8aa7c8 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:51:03 -0600 Subject: [PATCH 40/51] Apply suggestions from code review Co-authored-by: Doug Hoskisson --- Fill.py | 6 ------ Options.py | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Fill.py b/Fill.py index 983228bae239..a4236377c17a 100644 --- a/Fill.py +++ b/Fill.py @@ -858,15 +858,9 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: plando_blocks[player] = [] for block in multiworld.worlds[player].options.plando_items: new_block: typing.Dict[str, typing.Any] = {"player": player} - if not isinstance(block.from_pool, bool): - raise Exception(f"Plando 'from_pool' has to be boolean, not {type(block.from_pool)} for player {player}.") new_block["from_pool"] = block.from_pool new_block["force"] = block.force target_world = block.world - - if not (isinstance(block.force, bool) or block.force == "silent"): - raise Exception(f"Plando `force` has to be boolean or `silent`, not {block.force} for player {player}") - if target_world is False or multiworld.players == 1: # target own world worlds: typing.Set[int] = {player} elif target_world is True: # target any worlds besides own diff --git a/Options.py b/Options.py index b05fdcff7598..e833b34a3f08 100644 --- a/Options.py +++ b/Options.py @@ -1484,6 +1484,10 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: world = item.get("world", False) from_pool = item.get("from_pool", True) force = item.get("force", "silent") + if not isinstance(from_pool, bool): + raise Exception(f"Plando 'from_pool' has to be true or false, not {from_pool!r}.") + if not (isinstance(force, bool) or force == "silent"): + raise Exception(f"Plando `force` has to be true or false or `silent`, not {force!r}.") value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) elif isinstance(item, PlandoItem): if roll_percentage(item.percentage): From 82f14d75a94ab19c231f7b6b654a65c8a1b68f04 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:10:37 -0600 Subject: [PATCH 41/51] handle count, dataclass --- BaseClasses.py | 17 ++++++- Fill.py | 123 +++++++++++++++++++++++++++---------------------- Options.py | 2 +- 3 files changed, 84 insertions(+), 58 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 18c784990cfc..841d7a04f658 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -9,8 +9,9 @@ from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag -from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple, Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) +import dataclasses from typing_extensions import NotRequired, TypedDict @@ -54,6 +55,18 @@ class HasNameAndPlayer(Protocol): player: int +@dataclasses.dataclass +class PlandoItemBlock: + player: int + from_pool: bool + force: Union[bool, Literal["silent"]] + worlds: Set[int] = dataclasses.field(default_factory=set) + items: List[str] = dataclasses.field(default_factory=list) + locations: List[str] = dataclasses.field(default_factory=list) + resolved_locations: List[Location] = dataclasses.field(default_factory=list) + count: Dict[str, int] = dataclasses.field(default_factory=dict) + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -83,7 +96,7 @@ class MultiWorld(): start_location_hints: Dict[int, Options.StartLocationHints] item_links: Dict[int, Options.ItemLinks] - plando_item_blocks: Dict[int, Any] + plando_item_blocks: Dict[int, List[PlandoItemBlock]] game: Dict[int, str] diff --git a/Fill.py b/Fill.py index a4236377c17a..0e07896ccab7 100644 --- a/Fill.py +++ b/Fill.py @@ -4,7 +4,7 @@ import typing from collections import Counter, deque -from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock from Options import Accessibility from worlds.AutoWorld import call_all @@ -837,7 +837,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: location_2.item.location = location_2 -def parse_planned_blocks(multiworld: MultiWorld) -> typing.Dict[int, typing.List[typing.Dict[str, typing.Any]]]: +def parse_planned_blocks(multiworld: MultiWorld) -> typing.Dict[int, typing.List[PlandoItemBlock]]: def warn(warning: str, force: typing.Union[bool, str]) -> None: if isinstance(force, bool): logging.warning(f"{warning}") @@ -852,14 +852,12 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: world_name_lookup = multiworld.world_name_lookup - plando_blocks: typing.Dict[int, typing.List[typing.Dict[str, typing.Any]]] = dict() + plando_blocks: typing.Dict[int, typing.List[PlandoItemBlock]] = dict() player_ids = set(multiworld.player_ids) for player in player_ids: plando_blocks[player] = [] for block in multiworld.worlds[player].options.plando_items: - new_block: typing.Dict[str, typing.Any] = {"player": player} - new_block["from_pool"] = block.from_pool - new_block["force"] = block.force + new_block: PlandoItemBlock = PlandoItemBlock(player, block.from_pool, block.force) target_world = block.world if target_world is False or multiworld.players == 1: # target own world worlds: typing.Set[int] = {player} @@ -888,9 +886,9 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: block.force) continue worlds = {world_name_lookup[target_world]} - new_block["world"] = worlds + new_block.worlds = worlds - items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] = block.items + items: typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] = block.items if isinstance(items, dict): item_list: typing.List[str] = [] for key, value in items.items(): @@ -900,13 +898,11 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: items = item_list if isinstance(items, str): items = [items] - new_block["items"] = items + new_block.items = items locations: typing.List[str] = block.locations if isinstance(locations, str): locations = [locations] - elif not isinstance(locations, list): - raise Exception(f"Plando 'locations' has to be a list, not {type(locations)} for player {player}.") locations_from_groups: typing.List[str] = [] resolved_locations: typing.List[Location] = [] @@ -917,41 +913,35 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: locations_from_groups.extend(multiworld.worlds[target_player].location_name_groups[group]) resolved_locations.extend(location for location in world_locations if location.name in [*locations, *locations_from_groups]) - new_block["locations"] = sorted(dict.fromkeys(locations)) - new_block["resolved_locations"] = sorted(set(resolved_locations)) - - new_block["count"] = block.count - if not new_block["count"]: - new_block["count"] = (min(len(new_block["items"]), len(new_block["locations"])) if - len(new_block["locations"]) > 0 else len(new_block["items"])) - if isinstance(new_block["count"], int): - new_block["count"] = {"min": new_block["count"], "max": new_block["count"]} - if "min" not in new_block["count"]: - new_block["count"]["min"] = 0 - if "max" not in new_block["count"]: - new_block["count"]["max"] = (min(len(new_block["items"]), len(new_block["locations"])) if - len(new_block["locations"]) > 0 else len(new_block["items"])) - if new_block["count"]["max"] > len(new_block["items"]): - count = new_block["count"]["max"] - failed(f"Plando count {count} greater than items specified", block.force) - new_block["count"]["max"] = len(new_block["items"]) - if new_block["count"]["min"] > len(new_block["items"]): - new_block["count"]["min"] = len(new_block["items"]) - if new_block["count"]["max"] > len(new_block["locations"]) > 0: - count = new_block["count"]["max"] - failed(f"Plando count {count} greater than locations specified", block.force) - new_block["count"]["max"] = len(new_block["locations"]) - if new_block["count"]["min"] > len(new_block["locations"]): - new_block["count"]["min"] = len(new_block["locations"]) - new_block["count"]["target"] = multiworld.random.randint(new_block["count"]["min"], - new_block["count"]["max"]) + new_block.locations = sorted(dict.fromkeys(locations)) + new_block.locations = sorted(set(resolved_locations)) + + count = block.count + if isinstance(count, int): + count = {"min": count, "max": count} + if "min" not in count: + count["min"] = 0 + if "max" not in count: + count["max"] = count["min"] - if new_block["count"]["target"] > 0: - plando_blocks[player].append(new_block) + plando_blocks[player].append(new_block) return plando_blocks + def resolve_early_locations_for_planned(multiworld: MultiWorld): + def warn(warning: str, force: typing.Union[bool, str]) -> None: + if isinstance(force, bool): + logging.warning(f"{warning}") + else: + logging.debug(f"{warning}") + + def failed(warning: str, force: typing.Union[bool, str]) -> None: + if force is True: + raise Exception(warning) + else: + warn(warning, force) + swept_state = multiworld.state.copy() swept_state.sweep_for_advancements() reachable = frozenset(multiworld.get_reachable_locations(swept_state)) @@ -964,10 +954,11 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld): non_early_locations[loc.player].append(loc) for player in multiworld.plando_item_blocks: + removed = [] for block in multiworld.plando_item_blocks[player]: - locations = block["locations"] - resolved_locations = block["resolved_locations"] - worlds = block["world"] + locations = block.locations + resolved_locations = block.resolved_locations + worlds = block.worlds if "early_locations" in locations: for target_player in worlds: resolved_locations += early_locations[target_player] @@ -975,7 +966,29 @@ def resolve_early_locations_for_planned(multiworld: MultiWorld): for target_player in worlds: resolved_locations += non_early_locations[target_player] -def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: typing.List[typing.Dict[str, typing.Any]]): + if block.count["max"] > len(block.items): + count = block.count["max"] + failed(f"Plando count {count} greater than items specified", block.force) + block.count["max"] = len(block.items) + if block.count["min"] > len(block.items): + block.count["min"] = len(block.items) + if block.count["max"] > len(block.resolved_locations) > 0: + count = block.count["max"] + failed(f"Plando count {count} greater than locations specified", block.force) + block.count["max"] = len(block.resolved_locations) + if block.count["min"] > len(block.resolved_locations): + block.count["min"] = len(block.resolved_locations) + block.count["target"] = multiworld.random.randint(block.count["min"], + block.count["max"]) + + if block.count["target"]: + removed.append(block) + + for block in removed: + multiworld.plando_item_blocks[player].remove(block) + + +def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: typing.List[PlandoItemBlock]): def warn(warning: str, force: typing.Union[bool, str]) -> None: if isinstance(force, bool): logging.warning(f"{warning}") @@ -996,13 +1009,13 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: else len(multiworld.get_unfilled_locations(player)) - block["count"]["target"])) for placement in plando_blocks: - player = placement["player"] + player = placement.player try: - worlds = placement["world"] - locations = placement["resolved_locations"] - items = placement["items"] - maxcount = placement["count"]["target"] - from_pool = placement["from_pool"] + worlds = placement.worlds + locations = placement.resolved_locations + items = placement.items + maxcount = placement.count["target"] + from_pool = placement.from_pool item_candidates = [] if from_pool: @@ -1011,7 +1024,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: candidate = next((i for i in instances if i.name == item), None) if candidate is None: warn(f"Could not remove {item} from pool for {multiworld.player_name[player]} as " - f"it's already missing from it", placement["force"]) + f"it's already missing from it", placement.force) candidate = multiworld.worlds[player].create_item(item) else: multiworld.itempool.remove(candidate) @@ -1026,7 +1039,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: f"event items and non-event items. " f"Event items: {[item for item in item_candidates if item.code is None]}, " f"Non-event items: {[item for item in item_candidates if item.code is not None]}", - placement["force"]) + placement.force) continue else: is_real = item_candidates[0].code is not None @@ -1034,7 +1047,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: and bool(candidate.address) == is_real] multiworld.random.shuffle(candidates) allstate = multiworld.get_all_state(False) - mincount = placement["count"]["min"] + mincount = placement.count["min"] allowed_margin = len(item_candidates) - mincount fill_restrictive(multiworld, allstate, candidates, item_candidates, lock=True, allow_partial=True, name="Plando Main Fill") @@ -1044,7 +1057,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: f"of {mincount + allowed_margin} item(s) " f"for {multiworld.player_name[player]}, " f"remaining items: {item_candidates}", - placement["force"]) + placement.force) if from_pool: multiworld.itempool.extend([item for item in item_candidates if item.code is not None]) except Exception as e: diff --git a/Options.py b/Options.py index e833b34a3f08..4284ede01545 100644 --- a/Options.py +++ b/Options.py @@ -1428,7 +1428,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P @dataclass(frozen=True) class PlandoItem: - items: list[str] | dict[str, typing.Any] + items: list[str] | str | dict[str, typing.Any] locations: list[str] world: int | str | bool | None | typing.Iterable[str] | set[int] = False from_pool: bool = True From 7f50589bda327e8252c93bf703735a74e7a98b42 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:19:39 -0600 Subject: [PATCH 42/51] Update test_implemented.py --- test/general/test_implemented.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index b6d7eada9a14..3f83f36b6057 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -59,7 +59,7 @@ def test_prefill_items(self): if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"): with self.subTest(gamename): multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items", - "set_rules", "generate_basic")) + "set_rules", "connect_entrances", "generate_basic")) allstate = multiworld.get_all_state(False) locations = multiworld.get_locations() reachable = multiworld.get_reachable_locations(allstate) From 7ccbb959a569cf068b6d20828de7b645759d3830 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:41:26 -0600 Subject: [PATCH 43/51] fix count --- Fill.py | 13 ++++++++----- worlds/ladx/test/testShop.py | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Fill.py b/Fill.py index 091f8dee97c8..e3bb8635642b 100644 --- a/Fill.py +++ b/Fill.py @@ -944,13 +944,16 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: new_block.locations = sorted(set(resolved_locations)) count = block.count + if not count: + count = len(new_block.items) if isinstance(count, int): count = {"min": count, "max": count} if "min" not in count: count["min"] = 0 if "max" not in count: - count["max"] = count["min"] + count["max"] = len(new_block.items) + new_block.count = count plando_blocks[player].append(new_block) return plando_blocks @@ -1031,10 +1034,10 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: # shuffle, but then sort blocks by number of locations minus number of items, # so less-flexible blocks get priority multiworld.random.shuffle(plando_blocks) - plando_blocks.sort(key=lambda block: (len(block["resolved_locations"]) - block["count"]["target"] - if len(block["resolved_locations"]) > 0 - else len(multiworld.get_unfilled_locations(player)) - - block["count"]["target"])) + plando_blocks.sort(key=lambda block: (len(block.resolved_locations) - block.count["target"] + if len(block.resolved_locations) > 0 + else len(multiworld.get_unfilled_locations(block.player)) - + block.count["target"])) for placement in plando_blocks: player = placement.player try: diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py index 6cdedfeeed24..cbf444071445 100644 --- a/worlds/ladx/test/testShop.py +++ b/worlds/ladx/test/testShop.py @@ -1,6 +1,6 @@ from typing import Optional -from Fill import parse_planned_blocks, distribute_planned_blocks +from Fill import parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned from Options import PlandoItems from test.general import setup_solo_multiworld from worlds.AutoWorld import call_all @@ -27,7 +27,9 @@ def world_setup(self, seed: Optional[int] = None) -> None: ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") ) self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"]) + resolve_early_locations_for_planned(self.multiworld) self.multiworld.plando_item_blocks = parse_planned_blocks(self.multiworld) + distribute_planned_blocks(self.multiworld, [x for player in self.multiworld.plando_item_blocks for x in self.multiworld.plando_item_blocks[player]]) call_all(self.multiworld, "pre_fill") From 63b3dc652d93d4659237ca1095acbd7715658705 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:49:28 -0600 Subject: [PATCH 44/51] one section early --- worlds/ladx/test/testShop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py index cbf444071445..a28ba39b2f7c 100644 --- a/worlds/ladx/test/testShop.py +++ b/worlds/ladx/test/testShop.py @@ -27,9 +27,8 @@ def world_setup(self, seed: Optional[int] = None) -> None: ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") ) self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"]) - resolve_early_locations_for_planned(self.multiworld) self.multiworld.plando_item_blocks = parse_planned_blocks(self.multiworld) - + resolve_early_locations_for_planned(self.multiworld) distribute_planned_blocks(self.multiworld, [x for player in self.multiworld.plando_item_blocks for x in self.multiworld.plando_item_blocks[player]]) call_all(self.multiworld, "pre_fill") From 3073eb2719a2fa321d3cba374f57954a8830a136 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:14:20 -0600 Subject: [PATCH 45/51] Update Fill.py --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index e3bb8635642b..d9ea22598ffd 100644 --- a/Fill.py +++ b/Fill.py @@ -941,7 +941,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: resolved_locations.extend(location for location in world_locations if location.name in [*locations, *locations_from_groups]) new_block.locations = sorted(dict.fromkeys(locations)) - new_block.locations = sorted(set(resolved_locations)) + new_block.resolved_locations = sorted(set(resolved_locations)) count = block.count if not count: From 652cf5f64ae0c077903baa66dc1f75a44e51a708 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:27:04 -0600 Subject: [PATCH 46/51] Update Fill.py --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index d9ea22598ffd..e13f7391faca 100644 --- a/Fill.py +++ b/Fill.py @@ -1011,7 +1011,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: block.count["target"] = multiworld.random.randint(block.count["min"], block.count["max"]) - if block.count["target"]: + if not block.count["target"]: removed.append(block) for block in removed: From 5a8d9254b67ce2e6a0b164572043669e5d73b0bc Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:40:10 -0600 Subject: [PATCH 47/51] handle str items --- Fill.py | 4 +--- Options.py | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index e13f7391faca..e12d0f924770 100644 --- a/Fill.py +++ b/Fill.py @@ -915,7 +915,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: worlds = {world_name_lookup[target_world]} new_block.worlds = worlds - items: typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] = block.items + items: typing.Union[typing.List[str], typing.Dict[str, typing.Any]] = block.items if isinstance(items, dict): item_list: typing.List[str] = [] for key, value in items.items(): @@ -923,8 +923,6 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: value = multiworld.itempool.count(multiworld.worlds[player].create_item(key)) item_list += [key] * value items = item_list - if isinstance(items, str): - items = [items] new_block.items = items locations: typing.List[str] = block.locations diff --git a/Options.py b/Options.py index b002ee0189a1..f1e9893c08ac 100644 --- a/Options.py +++ b/Options.py @@ -1480,6 +1480,8 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: items = [items] elif not isinstance(items, dict): raise Exception(f"Plando 'item' has to be string or dictionary, not {type(items)}.") + if isinstance(items, str): + items = [items] locations = item.get("locations", []) if not locations: locations = item.get("location", []) From 02fda8e25de81b90f7cc2da0d03949e6e0d7223d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:23:08 -0600 Subject: [PATCH 48/51] options cleanup --- Options.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Options.py b/Options.py index f1e9893c08ac..5f6fad5a5043 100644 --- a/Options.py +++ b/Options.py @@ -1436,7 +1436,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P @dataclass(frozen=True) class PlandoItem: - items: list[str] | str | dict[str, typing.Any] + items: list[str] | dict[str, typing.Any] locations: list[str] world: int | str | bool | None | typing.Iterable[str] | set[int] = False from_pool: bool = True @@ -1447,7 +1447,7 @@ class PlandoItem: class PlandoItems(Option[typing.List[PlandoItem]]): """Generic items plando.""" - default = () + default = [] supports_weighting = False display_name = "Plando Items" @@ -1458,28 +1458,28 @@ def __init__(self, value: typing.Iterable[PlandoItem]) -> None: @classmethod def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: if not isinstance(data, typing.Iterable): - raise Exception(f"Cannot create plando items from non-Iterable type, got {type(data)}") + raise OptionError(f"Cannot create plando items from non-Iterable type, got {type(data)}") value: typing.List[PlandoItem] = [] for item in data: if isinstance(item, typing.Mapping): percentage = item.get("percentage", 100) if not isinstance(percentage, int): - raise Exception(f"Plando `percentage` has to be int, not {type(percentage)}.") + raise OptionError(f"Plando `percentage` has to be int, not {type(percentage)}.") if not (0 <= percentage <= 100): - raise Exception(f"Plando `percentage` has to be between 0 and 100 (inclusive) not {percentage}.") + raise OptionError(f"Plando `percentage` has to be between 0 and 100 (inclusive) not {percentage}.") if roll_percentage(percentage): count = item.get("count", False) items = item.get("items", []) if not items: items = item.get("item", None) # explicitly throw an error here if not present if not items: - raise Exception("You must specify at least one item to place items with plando.") + raise OptionError("You must specify at least one item to place items with plando.") count = 1 if isinstance(items, str): items = [items] elif not isinstance(items, dict): - raise Exception(f"Plando 'item' has to be string or dictionary, not {type(items)}.") + raise OptionError(f"Plando 'item' has to be string or dictionary, not {type(items)}.") if isinstance(items, str): items = [items] locations = item.get("locations", []) @@ -1490,20 +1490,20 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]: if isinstance(locations, str): locations = [locations] if not isinstance(locations, list): - raise Exception(f"Plando `location` has to be string or list, not {type(locations)}") + raise OptionError(f"Plando `location` has to be string or list, not {type(locations)}") world = item.get("world", False) from_pool = item.get("from_pool", True) force = item.get("force", "silent") if not isinstance(from_pool, bool): - raise Exception(f"Plando 'from_pool' has to be true or false, not {from_pool!r}.") + raise OptionError(f"Plando 'from_pool' has to be true or false, not {from_pool!r}.") if not (isinstance(force, bool) or force == "silent"): - raise Exception(f"Plando `force` has to be true or false or `silent`, not {force!r}.") + raise OptionError(f"Plando `force` has to be true or false or `silent`, not {force!r}.") value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage)) elif isinstance(item, PlandoItem): if roll_percentage(item.percentage): value.append(item) else: - raise Exception(f"Cannot create plando item from non-Dict type, got {type(item)}.") + raise OptionError(f"Cannot create plando item from non-Dict type, got {type(item)}.") return cls(value) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: @@ -1518,6 +1518,11 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P else: # filter down item groups for plando in self.value: + # confirm a valid count + if isinstance(plando.count, dict): + if "min" in plando.count and "max" in plando.count: + if plando.count["min"] > plando.count["max"]: + raise OptionError("Plando cannot have count `min` greater than `max`.") items_copy = plando.items.copy() if isinstance(plando.items, dict): for item in items_copy: @@ -1526,7 +1531,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P group = sorted(world.item_name_groups[item]) for group_item in group: if group_item in plando.items: - raise Exception(f"Plando `items` contains both \"{group_item}\" and the group " + raise OptionError(f"Plando `items` contains both \"{group_item}\" and the group " f"\"{item}\" which contains it. It cannot have both.") plando.items.update({key: value for key in group}) else: From 3f1d17ce1b32c945d13edeb2103bc7a35eb557e0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:56:03 -0600 Subject: [PATCH 49/51] revert default change Co-authored-by: Doug Hoskisson --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 5f6fad5a5043..2c1bbe555d05 100644 --- a/Options.py +++ b/Options.py @@ -1447,7 +1447,7 @@ class PlandoItem: class PlandoItems(Option[typing.List[PlandoItem]]): """Generic items plando.""" - default = [] + default = () supports_weighting = False display_name = "Plando Items" From 234d2910f98cc510c4502a30fbbab34e19993a47 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:39:37 -0600 Subject: [PATCH 50/51] Update Fill.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index e12d0f924770..361abd7af87b 100644 --- a/Fill.py +++ b/Fill.py @@ -954,7 +954,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: new_block.count = count plando_blocks[player].append(new_block) - return plando_blocks + return plando_blocks def resolve_early_locations_for_planned(multiworld: MultiWorld): From d35ea49dc4a0abf5f265852d3663ec3a3f0a9da9 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:33:03 -0600 Subject: [PATCH 51/51] Update test_state.py --- test/general/test_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/general/test_state.py b/test/general/test_state.py index 460fc3d60846..06c4046a6942 100644 --- a/test/general/test_state.py +++ b/test/general/test_state.py @@ -26,4 +26,4 @@ def test_all_state_is_available(self): for step in self.test_steps: with self.subTest("Step", step=step): call_all(multiworld, step) - self.assertTrue(multiworld.get_all_state(False, True)) + self.assertTrue(multiworld.get_all_state(False, allow_partial_entrances=True))