From dcb820b483b1b6c8605e9eb90b0ba1fc942d2148 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Mon, 24 Mar 2025 20:38:58 +0100 Subject: [PATCH] Some refactorings --- worlds/satisfactory/CriticalPathCalculator.py | 75 +++++----- worlds/satisfactory/Items.py | 141 +----------------- worlds/satisfactory/Locations.py | 44 ++---- worlds/satisfactory/Regions.py | 4 +- worlds/satisfactory/StateLogic.py | 3 - worlds/satisfactory/__init__.py | 2 +- 6 files changed, 61 insertions(+), 208 deletions(-) diff --git a/worlds/satisfactory/CriticalPathCalculator.py b/worlds/satisfactory/CriticalPathCalculator.py index 74f9e47404..14b4695a25 100644 --- a/worlds/satisfactory/CriticalPathCalculator.py +++ b/worlds/satisfactory/CriticalPathCalculator.py @@ -8,26 +8,29 @@ from .Options import SatisfactoryOptions class CriticalPathCalculator: logic: GameLogic random: Random + options: SatisfactoryOptions - potential_required_parts: set[str] - potential_required_buildings: set[str] - potential_required_belt_speed: int - potential_required_pipes: bool - potential_required_radioactive: bool - potential_required_power: int - potential_required_recipes_names: set[str] + required_parts: set[str] + required_buildings: set[str] + required_item_names: set[str] + required_power_level: int + + __potential_required_belt_speed: int + __potential_required_pipes: bool + __potential_required_radioactive: bool def __init__(self, logic: GameLogic, random: Random, options: SatisfactoryOptions): self.logic = logic self.random = random self.options = options - self.potential_required_parts = set() - self.potential_required_buildings = set() - self.potential_required_belt_speed = 1 - self.potential_required_pipes = False - self.potential_required_radioactive = False - self.potential_required_power: int = 1 + self.required_parts = set() + self.required_buildings = set() + self.required_power_level: int = 1 + + self.__potential_required_belt_speed = 1 + self.__potential_required_pipes = False + self.__potential_required_radioactive = False selected_power_infrastructure: dict[int, Recipe] = {} @@ -52,73 +55,69 @@ class CriticalPathCalculator: self.select_minimal_required_parts_for_building("Foundation") self.select_minimal_required_parts_for_building("Walls Orange") self.select_minimal_required_parts_for_building("Power Storage") + self.select_minimal_required_parts_for_building("Miner Mk.2") #equipment self.select_minimal_required_parts_for(self.logic.recipes["Hazmat Suit"][0].inputs) self.select_minimal_required_parts_for(self.logic.recipes["Iodine Infused Filter"][0].inputs) - for i in range(1, self.potential_required_belt_speed + 1): + for i in range(1, self.__potential_required_belt_speed + 1): self.select_minimal_required_parts_for_building(f"Conveyor Mk.{i}") - if self.potential_required_pipes: + if self.__potential_required_pipes: self.select_minimal_required_parts_for_building("Pipes Mk.1") + self.select_minimal_required_parts_for_building("Pipes Mk.2") self.select_minimal_required_parts_for_building("Pipeline Pump Mk.1") - if self.potential_required_radioactive: + self.select_minimal_required_parts_for_building("Pipeline Pump Mk.2") + if self.__potential_required_radioactive: self.select_minimal_required_parts_for(self.logic.recipes["Hazmat Suit"][0].inputs) self.select_minimal_required_parts_for(self.logic.recipes["Iodine Infused Filter"][0].inputs) - for i in range(1, self.potential_required_power + 1): + for i in range(1, self.required_power_level + 1): power_recipe = random.choice(self.logic.requirement_per_powerlevel[i]) selected_power_infrastructure[i] = power_recipe self.select_minimal_required_parts_for(power_recipe.inputs) self.select_minimal_required_parts_for_building(power_recipe.building) - self.potential_required_recipes_names = set( + self.required_item_names = set( recipe.name - for part in self.potential_required_parts + for part in self.required_parts for recipe in self.logic.recipes[part] if recipe.minimal_tier <= self.options.final_elevator_package ) - self.potential_required_recipes_names.update( - "Building: "+ building - for building in self.potential_required_buildings - ) - - debug = True + self.required_item_names.update("Building: "+ building for building in self.required_buildings) def select_minimal_required_parts_for_building(self, building: str) -> None: self.select_minimal_required_parts_for(self.logic.buildings[building].inputs) - self.potential_required_buildings.add(building) + self.required_buildings.add(building) def select_minimal_required_parts_for(self, parts: Optional[Iterable[str]]) -> None: if parts is None: return for part in parts: - if part in self.potential_required_parts: + if part in self.required_parts: continue - self.potential_required_parts.add(part) + self.required_parts.add(part) for recipe in self.logic.recipes[part]: if recipe.minimal_tier > self.options.final_elevator_package: continue - self.potential_required_belt_speed = \ - max(self.potential_required_belt_speed, recipe.minimal_belt_speed) + self.__potential_required_belt_speed = \ + max(self.__potential_required_belt_speed, recipe.minimal_belt_speed) self.select_minimal_required_parts_for(recipe.inputs) if recipe.needs_pipes: - self.potential_required_pipes = True + self.__potential_required_pipes = True if recipe.is_radio_active: - self.potential_required_radioactive = True + self.__potential_required_radioactive = True if recipe.building: self.select_minimal_required_parts_for(self.logic.buildings[recipe.building].inputs) - self.potential_required_buildings.add(recipe.building) + self.required_buildings.add(recipe.building) if self.logic.buildings[recipe.building].power_requirement: - self.potential_required_power = \ - max(self.potential_required_power, - self.logic.buildings[recipe.building].power_requirement) - - debug = True \ No newline at end of file + self.required_power_level = \ + max(self.required_power_level, + self.logic.buildings[recipe.building].power_requirement) \ No newline at end of file diff --git a/worlds/satisfactory/Items.py b/worlds/satisfactory/Items.py index 6ac4f0f857..49f113ff48 100644 --- a/worlds/satisfactory/Items.py +++ b/worlds/satisfactory/Items.py @@ -1,8 +1,8 @@ import copy from random import Random -from typing import ClassVar, Dict, Set, List, TextIO, Tuple, Optional +from typing import ClassVar, Dict, Set, List, Tuple, Optional from BaseClasses import Item, ItemClassification as C, MultiWorld -from .GameLogic import GameLogic, Recipe +from .GameLogic import GameLogic from .Options import SatisfactoryOptions from .ItemData import ItemData, ItemGroups as G from .Options import SatisfactoryOptions @@ -709,8 +709,6 @@ class Items: logic: GameLogic random: Random critical_path: CriticalPathCalculator - precalculated_progression_recipes: Optional[Dict[str, Recipe]] - precalculated_progression_recipes_names: Optional[Set[str]] def __init__(self, player: Optional[int], logic: GameLogic, random: Random, options: SatisfactoryOptions, critical_path: CriticalPathCalculator): @@ -719,129 +717,18 @@ class Items: self.random = random self.critical_path = critical_path - if options.experimental_generation: # TODO major performance boost if we can get it stable - self.precalculated_progression_recipes = self.select_progression_recipes() - self.precalculated_progression_recipes_names = set( - recipe.name for recipe in self.precalculated_progression_recipes.values() - ) - else: - self.precalculated_progression_recipes = None - self.precalculated_progression_recipes_names = None - - - def select_recipe_for_part_that_does_not_depend_on_parent_recipes(self, - part: str, parts_to_avoid: Dict[str, str]) -> Recipe: - - recipes: List[Recipe] = list(self.logic.recipes[part]) - - implicit_recipe = next(filter(lambda r: r.implicitly_unlocked, recipes), None) - if implicit_recipe: - return implicit_recipe - - while (len(recipes) > 0): - recipe: Recipe = recipes.pop(self.random.randrange(len(recipes))) - - if recipe.inputs and any(input in parts_to_avoid for input in recipe.inputs): - continue - - return recipe - - raise Exception(f"No recipe available for {part}") - - - def build_progression_recipe_tree(self, parts: tuple[str, ...], selected_recipes: Dict[str, str]): - for part in parts: - recipe: Recipe = \ - self.select_recipe_for_part_that_does_not_depend_on_parent_recipes(part, selected_recipes) - - selected_recipes[part] = recipe.name - - child_recipes: Dict[str, Recipe] = {} - if (recipe.inputs): - for input in recipe.inputs: - child_recipes[input] = \ - self.select_recipe_for_part_that_does_not_depend_on_parent_recipes(input, selected_recipes) - - for part, child_recipe in child_recipes.items(): - selected_recipes[part] = child_recipe.name - - for child_recipe in child_recipes.values(): - if child_recipe.inputs: - self.build_progression_recipe_tree(child_recipe.inputs, selected_recipes) - - - def select_progression_recipes(self) -> Dict[str, Recipe]: - selected_recipes: Dict[str, Recipe] = {} - - while not self.is_beatable(selected_recipes): - selected_recipes = self.select_random_progression_recipes() - - return selected_recipes - - - def is_beatable(self, recipes: Dict[str, Recipe]) -> bool: - if not recipes: - return False - - craftable_parts: Set[str] = set() - pending_recipes_by_part: Dict[str, Recipe] = copy.deepcopy(recipes) - - for part, recipe_tuples in self.logic.recipes.items(): - for recipe in recipe_tuples: - if recipe.implicitly_unlocked: - craftable_parts.add(part) - - while pending_recipes_by_part: - new_collected_parts: Set[str] = set() - - for part, recipe in pending_recipes_by_part.items(): - if all(input in craftable_parts for input in recipe.inputs): - new_collected_parts.add(part) - - if not new_collected_parts: - return False - - craftable_parts = craftable_parts.union(new_collected_parts) - - for part in new_collected_parts: - del pending_recipes_by_part[part] - - return True - - - def select_random_progression_recipes(self) -> Dict[str, Recipe]: - selected_recipes: Dict[str, str] = {} - - for part, recipes in self.logic.recipes.items(): - - implicit_recipe: Recipe = next(filter(lambda r: r.implicitly_unlocked, recipes), None) - if implicit_recipe: - continue - - selected_recipes[part] = self.random.choice(recipes) - - return selected_recipes - @classmethod def create_item(cls, instance: Optional["Items"], name: str, player: int) -> Item: data: ItemData = cls.item_data[name] type = data.type - if instance and type == C.progression: - if instance.precalculated_progression_recipes_names: - if not name.startswith("Building: "): - if name not in instance.precalculated_progression_recipes_names: - type = C.useful - logging.info(f"Downscaling .. {name}") - else: - logging.warning(f"Preserving .. {name}") - if instance.critical_path.potential_required_recipes_names: - if not (data.category & G.BasicNeeds) and name not in instance.critical_path.potential_required_recipes_names: - type = C.filler - logging.info(f"Dropping... {name}") - #else: - # logging.warning(f"Required .. {name}") + if type == C.progression \ + and instance and instance.critical_path.required_item_names \ + and (data.category & (G.Recipe | G.Building)) and not (data.category & G.BasicNeeds) \ + and name not in instance.critical_path.required_item_names: + type = C.filler + logging.info(f"Dropping... {name}") return Item(name, type, data.code, player) @@ -894,15 +781,3 @@ class Items: pool.append(item) return pool - - - def write_progression_chain(self, multiworld: MultiWorld, spoiler_handle: TextIO): - if self.precalculated_progression_recipes: - player_name = f'{multiworld.get_player_name(self.player)}: ' if multiworld.players > 1 else '' - spoiler_handle.write('\n\nSelected Satisfactory Recipes:\n\n') - spoiler_handle.write('\n'.join( - f"{player_name}{part} -> {recipe.name}" - for part, recipes_per_part in self.logic.recipes.items() - for recipe in recipes_per_part - if recipe.name in self.precalculated_progression_recipes_names - )) diff --git a/worlds/satisfactory/Locations.py b/worlds/satisfactory/Locations.py index 53059a9118..a239b6bd7c 100644 --- a/worlds/satisfactory/Locations.py +++ b/worlds/satisfactory/Locations.py @@ -48,23 +48,7 @@ class Part(LocationData): def can_produce_any_recipe_for_part(self, state_logic: StateLogic, recipes: Iterable[Recipe], name: str, items: Items) -> Callable[[CollectionState], bool]: def can_build_by_any_recipe(state: CollectionState) -> bool: - if items.precalculated_progression_recipes and name in items.precalculated_progression_recipes: - can_produce: bool = state_logic.can_produce_specific_recipe_for_part( - state, items.precalculated_progression_recipes[name]) - - can_produce_anyway: bool - if can_produce: - return can_produce - else: - can_produce_anyway = \ - any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes) - - if can_produce_anyway: - debug = True - - return False # can_produce_anyway - else: - return any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes) + return any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes) return can_build_by_any_recipe @@ -140,7 +124,7 @@ class ShopSlot(LocationData): return can_purchase -class DropPod(LocationData): +class HardDrive(LocationData): def __init__(self, data: DropPodData, state_logic: Optional[StateLogic], locationId: int, tier: int, can_hold_progression: bool): @@ -330,7 +314,7 @@ class Locations(): location_table = self.get_base_location_table(self.max_tiers) location_table.extend(self.get_hub_locations(True, self.max_tiers)) - location_table.extend(self.get_drop_pod_locations(True, self.max_tiers, set())) + location_table.extend(self.get_hard_drive_locations(True, self.max_tiers, set())) location_table.append(LocationData("Overworld", "UpperBound", 1338999)) return {location.name: location.code for location in location_table} @@ -345,7 +329,7 @@ class Locations(): location_table = self.get_base_location_table(max_tier_for_game) location_table.extend(self.get_hub_locations(False, max_tier_for_game)) - location_table.extend(self.get_drop_pod_locations(False, max_tier_for_game, self.critical_path.potential_required_parts)) + location_table.extend(self.get_hard_drive_locations(False, max_tier_for_game,self.critical_path.required_parts)) location_table.extend(self.get_logical_event_locations(self.options.final_elevator_package)) return location_table @@ -392,22 +376,22 @@ class Locations(): location_table.extend( part for part_name, recipes in self.game_logic.recipes.items() - if part_name in self.critical_path.potential_required_parts + if part_name in self.critical_path.required_parts for part in Part.get_parts(self.state_logic, recipes, part_name, self.items, final_elevator_tier)) location_table.extend( EventBuilding(self.game_logic, self.state_logic, name, building) for name, building in self.game_logic.buildings.items() - if name in self.critical_path.potential_required_buildings) + if name in self.critical_path.required_buildings) location_table.extend( PowerInfrastructure(self.game_logic, self.state_logic, power_level, recipes) for power_level, recipes in self.game_logic.requirement_per_powerlevel.items() - if power_level <= self.critical_path.potential_required_power) + if power_level <= self.critical_path.required_power_level) return location_table - def get_drop_pod_locations(self, for_data_package: bool, max_tier: int, available_parts: set[str]) \ + def get_hard_drive_locations(self, for_data_package: bool, max_tier: int, available_parts: set[str]) \ -> List[LocationData]: - drop_pod_locations: List[DropPod] = [] + hard_drive_locations: List[HardDrive] = [] bucket_size: int drop_pod_data: List[DropPodData] @@ -422,18 +406,16 @@ class Locations(): for location_id in range(self.drop_pod_location_id_start, self.drop_pod_location_id_end + 1): if for_data_package: - drop_pod_locations.append(DropPod(DropPodData(0, 0, 0, None, 0), None, location_id, 1, False)) + hard_drive_locations.append(HardDrive(DropPodData(0, 0, 0, None, 0), None, location_id, 1, False)) else: location_id_normalized: int = location_id - self.drop_pod_location_id_start - if location_id_normalized == 81: - debug = True - data: DropPodData = drop_pod_data[location_id_normalized] can_hold_progression: bool = location_id_normalized < self.options.hard_drive_progression_limit.value tier = min(ceil((location_id_normalized + 1) / bucket_size), max_tier) if not data.item or data.item in available_parts: - drop_pod_locations.append(DropPod(data, self.state_logic, location_id, tier, can_hold_progression)) + hard_drive_locations.append( + HardDrive(data, self.state_logic, location_id, tier, can_hold_progression)) - return drop_pod_locations \ No newline at end of file + return hard_drive_locations \ No newline at end of file diff --git a/worlds/satisfactory/Regions.py b/worlds/satisfactory/Regions.py index b0ba5b0023..852ad11eb5 100644 --- a/worlds/satisfactory/Regions.py +++ b/worlds/satisfactory/Regions.py @@ -50,7 +50,7 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory region_names.append(f"Hub {hub_tier}-{minestone}") for building_name, building in game_logic.buildings.items(): - if building.can_produce and building_name in critical_path.potential_required_buildings: + if building.can_produce and building_name in critical_path.required_buildings: region_names.append(building_name) for tree_name, tree in game_logic.man_trees.items(): @@ -127,7 +127,7 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory can_produce_all_allowing_handcrafting(parts_per_milestone.keys())) for building_name, building in game_logic.buildings.items(): - if building.can_produce and building_name in critical_path.potential_required_buildings: + if building.can_produce and building_name in critical_path.required_buildings: connect(regions, "Overworld", building_name, lambda state, building_name=building_name: state_logic.can_build(state, building_name)) diff --git a/worlds/satisfactory/StateLogic.py b/worlds/satisfactory/StateLogic.py index a4bf40af6c..8659707ad6 100644 --- a/worlds/satisfactory/StateLogic.py +++ b/worlds/satisfactory/StateLogic.py @@ -60,9 +60,6 @@ class StateLogic: return not parts or all(can_handcraft_part(part) for part in parts) def can_produce_specific_recipe_for_part(self, state: CollectionState, recipe: Recipe) -> bool: - if recipe.name == "Recipe: Iron Ingot": - debug = True - if recipe.needs_pipes and ( not self.can_build_any(state, ("Pipes Mk.1", "Pipes Mk.2")) or not self.can_build_any(state, ("Pipeline Pump Mk.1", "Pipeline Pump Mk.2"))): diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py index b12093d412..9cf049ae08 100644 --- a/worlds/satisfactory/__init__.py +++ b/worlds/satisfactory/__init__.py @@ -145,7 +145,7 @@ class SatisfactoryWorld(World): def write_spoiler(self, spoiler_handle: TextIO): - self.items.write_progression_chain(self.multiworld, spoiler_handle) + pass def get_filler_item_name(self) -> str: