From a98611c10585d2516fd4a73d6eba17faa2c1f1b4 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:45:29 +0200 Subject: [PATCH] Move it all to a new file --- worlds/witness/__init__.py | 104 +------------------- worlds/witness/place_early_item.py | 146 +++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 102 deletions(-) create mode 100644 worlds/witness/place_early_item.py diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 3af59c1799cc..bb869e932b6b 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -18,6 +18,7 @@ from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups +from .place_early_item import place_early_items from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic from .presets import witness_option_presets @@ -336,110 +337,9 @@ def create_items(self) -> None: if self.player_items.item_data[item_name].local_only: self.options.local_items.value.add(item_name) - def find_good_items_from_itempool(self, itempool: List[Item], - eligible_locations: List[Location]) -> List[Tuple[Location, Item]]: - early_items = self.player_items.get_early_items(self.own_itempool) - - if not early_items: - return [] - - for item_list in early_items.values(): - self.random.shuffle(item_list) - - state = CollectionState(self.multiworld) - - placements = [] - found_early_items: List[Item] = [] - - while any(early_items.values()) and eligible_locations: - next_findable_items_dict = {item_list.pop(): item_type for item_type, item_list in early_items.items()} - next_findable_items = {self.item_name_to_id[item_name] for item_name in next_findable_items_dict} - - found_early_items = [] - - def keep_or_take_out(item: Item) -> bool: - if ( - item.code not in next_findable_items - or not any(location.can_fill(state, item, check_access=False) for location in eligible_locations) - ): - return True # Keep - next_findable_items.remove(item.code) - found_early_items.append(item) - return False # Take out - - local_player = self.player - itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)] - - # Bring them back into Symbol -> Door -> Obelisk Key order - # The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable - correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)} - found_early_items.sort(key=lambda item: correct_order[item.name]) - - for item in found_early_items: - location = next( - (location for location in eligible_locations if location.can_fill(state, item, check_access=False)), - None, - ) - if location is not None: - placements.append((location, item)) - eligible_locations.remove(location) - del early_items[next_findable_items_dict[item.name]] - else: - itempool.append(item) # Put item back if it can't be placed anywhere - - if early_items: - if not eligible_locations: - error(f'Could not find a suitable location for "early good items" of types {list(early_items)} in ' - f"{self.player_name}'s world. They are excluded or already contain plandoed items.\n") - else: - error(f"Could not find any \"early good item\" of types {list(early_items)} in {self.player_name}'s" - f" world, they were all plandoed elsewhere.") - - return placements - def fill_hook(self, progitempool: List[Item], _: List[Item], _2: List[Item], fill_locations: List[Location]) -> None: - if not self.options.early_good_items.value: - return - - # Pick an early item to put on Tutorial Gate Open. - # Done after plando to avoid conflicting with it. - # Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill. - - tutorial_checks_in_order = [ - "Tutorial Gate Open", - "Tutorial Back Left", - "Tutorial Back Right", - "Tutorial Front Left", - "Tutorial First Hallway Straight", - "Tutorial First Hallway Bend", - "Tutorial Patio Floor", # Don't think this can ever happen, but why not classify as many as possible - "Tutorial First Hallway EP", - "Tutorial Cloud EP", - "Tutorial Patio Flowers EP", - ] - - available_locations = [ - self.get_location(location_name) for location_name in tutorial_checks_in_order - if location_name in self.reachable_early_locations - ] - - available_locations += sorted( - ( - self.get_location(location_name) for location_name in self.reachable_early_locations - if location_name not in tutorial_checks_in_order - ), - key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["processing_order"] - ) - - available_locations = [location for location in available_locations if not location.item] - - early_good_item_placements = self.find_good_items_from_itempool(progitempool, available_locations) - - for location, item in early_good_item_placements: - debug(f"Placing early good item {item} on early location {location}.") - location.place_locked_item(item) - fill_locations.remove(location) + place_early_items(self, progitempool, fill_locations) def fill_slot_data(self) -> Dict[str, Any]: self.log_ids_to_hints: Dict[int, CompactHintData] = {} diff --git a/worlds/witness/place_early_item.py b/worlds/witness/place_early_item.py new file mode 100644 index 000000000000..d81819e8ea53 --- /dev/null +++ b/worlds/witness/place_early_item.py @@ -0,0 +1,146 @@ +from logging import debug, error +from typing import TYPE_CHECKING, List, Dict, Set + +from BaseClasses import Item, Location, CollectionState, LocationProgressType +from .data import static_logic as static_witness_logic + +if TYPE_CHECKING: + from . import WitnessWorld + + +def get_available_early_locations(world: "WitnessWorld") -> List[Location]: + # Pick an early item to put on Tutorial Gate Open. + # Done after plando to avoid conflicting with it. + # Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill. + + # Prioritize Tutorial locations in a specific order + tutorial_checks_in_order = [ + "Tutorial Gate Open", + "Tutorial Back Left", + "Tutorial Back Right", + "Tutorial Front Left", + "Tutorial First Hallway Straight", + "Tutorial First Hallway Bend", + "Tutorial Patio Floor", + "Tutorial First Hallway EP", + "Tutorial Cloud EP", + "Tutorial Patio Flowers EP", + ] + available_locations = [ + world.get_location(location_name) for location_name in tutorial_checks_in_order + if location_name in world.reachable_early_locations # May not actually be sphere 1 (e.g. Obelisk Keys for EPs) + ] + + # Then, add the rest of sphere 1 in "game order" + available_locations += sorted( + ( + world.get_location(location_name) for location_name in world.reachable_early_locations + if location_name not in tutorial_checks_in_order + ), + key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["processing_order"] + ) + + return [ + location for location in available_locations + if not location.item and not location.progress_type == LocationProgressType.EXCLUDED + ] + + +def get_eligible_items_by_type_in_random_order(world: "WitnessWorld") -> Dict[str, List[str]]: + eligible_early_items_by_type = world.player_items.get_early_items(world.own_itempool) + + for item_list in eligible_early_items_by_type.values(): + world.random.shuffle(item_list) + + return eligible_early_items_by_type + + +def grab_own_items_from_itempool(world: "WitnessWorld", itempool: List[Item], ids_to_find: Set[int]) -> List[Item]: + found_early_items = [] + + def keep_or_take_out(item: Item) -> bool: + if item.code not in ids_to_find: + return True # Keep + ids_to_find.remove(item.code) + found_early_items.append(item) + return False # Take out + + local_player = world.player + itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)] + + return found_early_items + + +def place_items_onto_locations(world: "WitnessWorld", items: List[Item], locations: List[Location]): + fake_state = CollectionState(world.multiworld) + + placed_items = [] + unplaced_items = [] + + for item in items: + location = next( + (location for location in locations if location.can_fill(fake_state, item, check_access=False)), + None, + ) + if location is not None: + location.place_locked_item(item) + placed_items.append(item) + locations.remove(location) + else: + unplaced_items.append(item) + + return placed_items, unplaced_items + + +def place_early_items(world: "WitnessWorld", fill_hook_progitempool: List[Item], fill_hook_locations: List[Location]): + if not world.options.early_good_items.value: + return + + # Get a list of good early locations in a determinstic order + eligible_early_locations = get_available_early_locations(world) + # Get a list of good early items of each desired item type + eligible_early_items_by_type = get_eligible_items_by_type_in_random_order(world) + + if not eligible_early_items_by_type: + return [] + + while any(eligible_early_items_by_type.values()) and eligible_early_locations: + # Get one item of each type + next_findable_items_dict = { + item_list.pop(): item_type + for item_type, item_list in eligible_early_items_by_type.items() + } + + # Get their IDs as a set + next_findable_item_ids = {world.item_name_to_id[item_name] for item_name in next_findable_items_dict} + + found_early_items = grab_own_items_from_itempool(world, fill_hook_progitempool, next_findable_item_ids) + + # Bring them back into Symbol -> Door -> Obelisk Key order + # The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable + correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)} + found_early_items.sort(key=lambda item: correct_order[item.name]) + + # Place found early items on eligible early locations. + placed_items, unplaced_items = place_items_onto_locations(world, found_early_items, eligible_early_locations) + + for item in placed_items: + debug(f"Placed early good item {item} on early location {item.location}.") + # Item type is satisfied + del eligible_early_items_by_type[next_findable_items_dict[item.name]] + fill_hook_locations.remove(item.location) + for item in unplaced_items: + debug(f"Could not find a suitable placemenet for item {item}.") + + unfilled_types = list(eligible_early_items_by_type) + if unfilled_types: + if not eligible_early_locations: + error( + f'Could not find a suitable location for "early good items" of types {unfilled_types} in ' + f"{world.player_name}'s world. They are excluded or already contain plandoed items.\n" + ) + else: + error( + f"Could not find any \"early good item\" of types {unfilled_types} in {world.player_name}'s world, " + "they were all plandoed elsewhere." + )