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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] . From acec5131615619df1e2402de9bbcc5e8bb9e6731 Mon Sep 17 00:00:00 2001 From: Exempt-Medic Date: Wed, 30 Oct 2024 12:04:07 -0400 Subject: [PATCH 21/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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