mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 15:03:20 -07:00
Some refactorings
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"))):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user