mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 06:23:21 -07:00
Removed space elevator parts from fillers Removed more AWESOME shop purchaseables from minimal item pool Added all equipment to minimal item pool Removed non fissile and fertile uranium from minimal item pool Removed portal from minimal item pool Removed Ionized fuel from minimal item pool Removed recipes for Hoverpack and Turbo Rifle Ammo from minimal item pool Lowered the chance for rolling steel on randomized starter recipes
278 lines
12 KiB
Python
278 lines
12 KiB
Python
from random import Random
|
|
from typing import Optional
|
|
from collections.abc import Iterable
|
|
from .GameLogic import GameLogic, Recipe
|
|
from .Options import SatisfactoryOptions
|
|
|
|
class CriticalPathCalculator:
|
|
logic: GameLogic
|
|
random: Random
|
|
final_elevator_package: int
|
|
randomize_starter_recipes: bool
|
|
|
|
required_parts: set[str]
|
|
required_buildings: set[str]
|
|
required_item_names: set[str]
|
|
required_power_level: int
|
|
|
|
__potential_required_belt_speed: int
|
|
|
|
parts_to_exclude: set[str]
|
|
recipes_to_exclude: set[str]
|
|
buildings_to_exclude: set[str]
|
|
|
|
implicitly_unlocked: set[str]
|
|
handcraftable_parts: dict[str, list[Recipe]]
|
|
tier_0_recipes: set[str]
|
|
|
|
def __init__(self, logic: GameLogic, seed: float, options: SatisfactoryOptions):
|
|
self.logic = logic
|
|
self.random = Random(seed)
|
|
self.final_elevator_package = options.final_elevator_package.value
|
|
self.randomize_starter_recipes = bool(options.randomize_starter_recipes.value)
|
|
|
|
def calculate(self) -> None:
|
|
self.required_parts = set()
|
|
self.required_buildings = set()
|
|
self.required_power_level: int = 1
|
|
|
|
self.__potential_required_belt_speed = 1
|
|
|
|
self.configure_implicitly_unlocked_and_handcraftable_parts()
|
|
|
|
self.select_minimal_required_parts_for(
|
|
self.logic.space_elevator_tiers[self.final_elevator_package-1].keys())
|
|
|
|
for tree in self.logic.man_trees.values():
|
|
self.select_minimal_required_parts_for(tree.access_items)
|
|
|
|
for node in tree.nodes:
|
|
if node.minimal_tier > self.final_elevator_package:
|
|
continue
|
|
|
|
self.select_minimal_required_parts_for(node.unlock_cost.keys())
|
|
|
|
self.select_minimal_required_parts_for_building("MAM")
|
|
self.select_minimal_required_parts_for_building("AWESOME Sink")
|
|
self.select_minimal_required_parts_for_building("AWESOME Shop")
|
|
self.select_minimal_required_parts_for_building("Space Elevator")
|
|
self.select_minimal_required_parts_for_building("Conveyor Splitter")
|
|
self.select_minimal_required_parts_for_building("Conveyor Merger")
|
|
self.select_minimal_required_parts_for_building("Equipment Workshop")
|
|
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")
|
|
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")
|
|
self.select_minimal_required_parts_for_building("Pipeline Pump Mk.2")
|
|
|
|
if self.logic.recipes["Uranium"][0].minimal_tier <= self.final_elevator_package:
|
|
self.select_minimal_required_parts_for(("Hazmat Suit", "Iodine-Infused Filter"))
|
|
|
|
for i in range(1, self.__potential_required_belt_speed + 1):
|
|
self.select_minimal_required_parts_for_building(f"Conveyor Mk.{i}")
|
|
|
|
for i in range(1, self.required_power_level + 1):
|
|
power_recipe = self.random.choice(self.logic.requirement_per_powerlevel[i])
|
|
self.select_minimal_required_parts_for(power_recipe.inputs)
|
|
self.select_minimal_required_parts_for_building(power_recipe.building)
|
|
|
|
self.required_item_names = {
|
|
recipe.name
|
|
for part in self.required_parts
|
|
for recipe in self.logic.recipes[part]
|
|
if recipe.minimal_tier <= self.final_elevator_package
|
|
}
|
|
self.required_item_names.update({"Building: "+ building for building in self.required_buildings})
|
|
|
|
self.calculate_excluded_things()
|
|
self.select_starter_recipes()
|
|
|
|
|
|
def select_minimal_required_parts_for_building(self, building: str) -> None:
|
|
self.select_minimal_required_parts_for(self.logic.buildings[building].inputs)
|
|
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.required_parts:
|
|
continue
|
|
|
|
self.required_parts.add(part)
|
|
|
|
for recipe in self.logic.recipes[part]:
|
|
if recipe.minimal_tier > self.final_elevator_package:
|
|
continue
|
|
|
|
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.building:
|
|
self.select_minimal_required_parts_for(self.logic.buildings[recipe.building].inputs)
|
|
self.required_buildings.add(recipe.building)
|
|
|
|
if self.logic.buildings[recipe.building].power_requirement:
|
|
self.required_power_level = \
|
|
max(self.required_power_level,
|
|
self.logic.buildings[recipe.building].power_requirement)
|
|
|
|
|
|
def calculate_excluded_things(self) -> None:
|
|
self.parts_to_exclude = set()
|
|
self.buildings_to_exclude = set()
|
|
self.recipes_to_exclude = {
|
|
recipe.name
|
|
for part in self.logic.recipes
|
|
for recipe in self.logic.recipes[part]
|
|
if recipe.minimal_tier > self.final_elevator_package
|
|
}
|
|
|
|
excluded_count = len(self.recipes_to_exclude)
|
|
while True:
|
|
for part in self.logic.recipes:
|
|
if part in self.parts_to_exclude:
|
|
continue
|
|
|
|
for recipe in self.logic.recipes[part]:
|
|
if recipe.name in self.recipes_to_exclude:
|
|
continue
|
|
|
|
if recipe.inputs and any(input in self.parts_to_exclude for input in recipe.inputs):
|
|
self.recipes_to_exclude.add(recipe.name)
|
|
|
|
if all(r.name in self.recipes_to_exclude for r in self.logic.recipes[part]):
|
|
self.parts_to_exclude.add(part)
|
|
|
|
new_buildings_to_exclude = {
|
|
building_name
|
|
for building_name, building in self.logic.buildings.items()
|
|
if building_name not in self.buildings_to_exclude
|
|
and building.inputs and any(input in self.parts_to_exclude for input in building.inputs)
|
|
}
|
|
|
|
self.recipes_to_exclude.update({
|
|
recipe_per_part.name
|
|
for building_to_exclude in new_buildings_to_exclude
|
|
for recipes_per_part in self.logic.recipes.values()
|
|
for recipe_per_part in recipes_per_part
|
|
if recipe_per_part.building == building_to_exclude
|
|
})
|
|
|
|
self.buildings_to_exclude.update(new_buildings_to_exclude)
|
|
|
|
new_length = len(self.recipes_to_exclude)
|
|
if new_length == excluded_count:
|
|
break
|
|
excluded_count = new_length
|
|
|
|
|
|
def configure_implicitly_unlocked_and_handcraftable_parts(self) -> None:
|
|
self.implicitly_unlocked: set[str] = {
|
|
recipe.name
|
|
for recipes_per_part in self.logic.recipes.values()
|
|
for recipe in recipes_per_part if recipe.implicitly_unlocked
|
|
}
|
|
self.implicitly_unlocked.update({
|
|
building.name
|
|
for building in self.logic.buildings.values() if building.implicitly_unlocked
|
|
})
|
|
|
|
self.handcraftable_parts: dict[str, list[Recipe]] = {}
|
|
for part, recipes_per_part in self.logic.recipes.items():
|
|
for recipe in recipes_per_part:
|
|
if recipe.handcraftable:
|
|
self.handcraftable_parts.setdefault(part, []).append(recipe)
|
|
|
|
|
|
def select_starter_recipes(self) -> None:
|
|
# cable is left unaffected as all its alternative recipes require refinery
|
|
if not self.randomize_starter_recipes:
|
|
self.tier_0_recipes = {
|
|
"Recipe: Iron Ingot",
|
|
"Recipe: Iron Plate",
|
|
"Recipe: Iron Rod",
|
|
"Recipe: Copper Ingot",
|
|
"Recipe: Wire",
|
|
"Recipe: Concrete",
|
|
"Recipe: Screw",
|
|
"Recipe: Reinforced Iron Plate"
|
|
}
|
|
else:
|
|
# we only allow basic parts to be made without the need of refineries
|
|
# could be made more based of GameLogic rather than hardcoded but this is likely faster
|
|
# would likely need to be based of GameLogic when we add mod support
|
|
self.tier_0_recipes = set()
|
|
|
|
self.tier_0_recipes.add(self.random.choice(
|
|
("Recipe: Iron Ingot", "Recipe: Basic Iron Ingot", "Recipe: Iron Alloy Ingot")))
|
|
|
|
selected_recipe = self.random.choice(
|
|
("Recipe: Iron Plate", "Recipe: Iron Plate", "Recipe: Iron Plate", "Recipe: Steel Cast Plate"))
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
if selected_recipe == "Recipe: Steel Cast Plate":
|
|
self.add_steel_ingot_to_starter_recipes()
|
|
|
|
selected_recipe = self.random.choice(
|
|
("Recipe: Iron Rod", "Recipe: Iron Rod", "Recipe: Iron Rod", "Recipe: Steel Rod"))
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
if selected_recipe == "Recipe: Steel Rod":
|
|
self.add_steel_ingot_to_starter_recipes()
|
|
|
|
self.tier_0_recipes.add(self.random.choice(("Recipe: Copper Ingot", "Recipe: Copper Alloy Ingot")))
|
|
|
|
selected_recipe = self.random.choice(
|
|
("Recipe: Wire", "Recipe: Caterium Wire", "Recipe: Fused Wire", "Recipe: Iron Wire"))
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
if selected_recipe in {"Recipe: Caterium Wire", "Recipe: Fused Wire"}:
|
|
# add Caterium Ingot
|
|
self.tier_0_recipes.add("Recipe: Caterium Ingot")
|
|
|
|
selected_recipe = self.random.choice(("Recipe: Concrete", "Recipe: Fine Concrete"))
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
if selected_recipe == "Recipe: Fine Concrete":
|
|
# add Silica
|
|
self.tier_0_recipes.add(self.random.choice(("Recipe: Silica", "Recipe: Cheap Silica")))
|
|
|
|
selected_recipe = self.random.choice(
|
|
("Recipe: Screw", "Recipe: Screw","Recipe: Cast Screw", "Recipe: Cast Screw", "Recipe: Steel Screw"))
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
if selected_recipe == "Recipe: Steel Screw":
|
|
# add Steel Beam and steel Ingot
|
|
self.add_steel_ingot_to_starter_recipes()
|
|
self.tier_0_recipes.add(self.random.choice(("Recipe: Steel Beam", "Recipe: Molded Beam")))
|
|
|
|
self.tier_0_recipes.add(self.random.choice(
|
|
("Recipe: Reinforced Iron Plate", "Recipe: Bolted Iron Plate", "Recipe: Stitched Iron Plate")))
|
|
|
|
for part, recipes in self.logic.recipes.items():
|
|
for recipe in recipes:
|
|
if recipe.name in self.tier_0_recipes:
|
|
if part in self.handcraftable_parts:
|
|
self.handcraftable_parts[part].append(recipe)
|
|
else:
|
|
self.handcraftable_parts[part] = [recipe]
|
|
self.tier_0_recipes.add(self.logic.buildings[recipe.building].name)
|
|
|
|
self.implicitly_unlocked.update(self.tier_0_recipes)
|
|
|
|
def add_steel_ingot_to_starter_recipes(self) -> None:
|
|
if "Recipe: Steel Ingot" not in self.tier_0_recipes \
|
|
and "Recipe: Compacted Steel Ingot" not in self.tier_0_recipes \
|
|
and "Recipe: Solid Steel Ingot" not in self.tier_0_recipes:
|
|
|
|
selected_recipe = self.random.choice(
|
|
("Recipe: Steel Ingot", "Recipe: Compacted Steel Ingot", "Recipe: Solid Steel Ingot"))
|
|
|
|
self.tier_0_recipes.add(selected_recipe)
|
|
|
|
if selected_recipe == "Recipe: Compacted Steel Ingot":
|
|
self.tier_0_recipes.add("Recipe: Compacted Coal")
|