Some refactorings

This commit is contained in:
Jarno Westhof
2025-03-24 20:38:58 +01:00
parent 5fdb33460d
commit dcb820b483
6 changed files with 61 additions and 208 deletions

View File

@@ -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
self.required_power_level = \
max(self.required_power_level,
self.logic.buildings[recipe.building].power_requirement)

View File

@@ -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
))

View File

@@ -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
return hard_drive_locations

View File

@@ -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))

View File

@@ -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"))):

View File

@@ -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: