progrees i think

This commit is contained in:
Jarno Westhof
2025-02-22 15:48:38 +01:00
parent 59f7147fc4
commit 8c68f23dc7
10 changed files with 845 additions and 723 deletions

View File

@@ -52,7 +52,8 @@ class TestBase(unittest.TestCase):
state = multiworld.get_all_state(False)
for location in multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
if not location.can_reach(state):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
for region in multiworld.get_regions():
if region.name in unreachable_regions:

View File

@@ -0,0 +1,79 @@
from random import Random
from typing import Set, Tuple
from .GameLogic import GameLogic, Recipe
from .Options import SatisfactoryOptions
from .Options import SatisfactoryOptions
class CriticalPathCalculator:
logic: GameLogic
random: Random
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]
def __init__(self, logic: GameLogic, random: Random, options: SatisfactoryOptions):
self.logic = logic
self.random = random
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.select_minimal_required_parts_for(
tuple(self.logic.space_elevator_tiers[options.final_elevator_package - 1].keys())
)
for i in range(self.potential_required_belt_speed, 1):
self.select_minimal_required_parts_for(self.logic.buildings[f"Conveyor Mk.{i}"].inputs)
if self.potential_required_pipes:
self.select_minimal_required_parts_for(self.logic.buildings["Pipeline Pump Mk.1"].inputs)
self.select_minimal_required_parts_for(self.logic.buildings["Pipeline Pump Mk.2"].inputs)
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(self.potential_required_belt_speed, 1):
power_recipe = random.choice(self.logic.requirement_per_powerlevel[i])
self.select_minimal_required_parts_for(power_recipe.inputs)
self.potential_required_buildings.add(power_recipe.building)
self.potential_required_recipes_names = set(
recipe.name
for part in self.potential_required_parts
for recipe in self.logic.recipes[part]
)
self.potential_required_recipes_names.update(
"Building: "+ building
for building in self.potential_required_buildings
)
debug = True
def select_minimal_required_parts_for(self, parts: Tuple[str]) -> None:
if parts:
for part in parts:
if part in self.potential_required_parts:
continue
self.potential_required_parts.add(part)
for recipe in self.logic.recipes[part]:
self.potential_required_belt_speed = \
max(self.potential_required_belt_speed, recipe.minimal_belt_speed)
self.select_minimal_required_parts_for(recipe.inputs)
self.select_minimal_required_parts_for(self.logic.buildings[recipe.building].inputs)
self.potential_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)

View File

@@ -572,8 +572,6 @@ class GameLogic:
Recipe("AI Expansion Server", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Magnetic Field Generator", "Neural-Quantum Processor", "Superposition Oscillator")), ),
###
#1.0
# TODO transport types aren't currently in logic
}
buildings: Dict[str, Building] = {

View File

@@ -1,40 +1,42 @@
from enum import Enum
from enum import IntFlag
from typing import NamedTuple, Set
from BaseClasses import ItemClassification
class ItemGroups(str, Enum):
Parts = 1
Equipment = 2
Ammo = 3
Recipe = 4
Building = 5
Trap = 6
Lights = 7
Foundations = 8
Transport = 9
Trains = 10
ConveyorMk1 = 11
ConveyorMk2 = 12
ConveyorMk3 = 13
ConveyorMk4 = 14
ConveyorMk5 = 15
ConveyorSupports = 16
PipesMk1 = 17
PipesMk2 = 18
PipelineSupports = 19
HyperTubes = 20
Signs = 21
Pilars = 22
Beams = 23
Walls = 24
Upgrades = 25
Vehicles = 26
Customizer = 27
ConveyorMk6 = 28
class ItemGroups(IntFlag):
Parts = 1 << 1
Equipment = 1 << 2
Ammo = 1 << 3
Recipe = 1 << 4
Building = 1 << 5
Trap = 1 << 6
Lights = 1 << 7
Foundations = 1 << 8
Transport = 1 << 9
Trains = 1 << 10
ConveyorMk1 = 1 << 11
ConveyorMk2 = 1 << 12
ConveyorMk3 = 1 << 13
ConveyorMk4 = 1 << 14
ConveyorMk5 = 1 << 15
ConveyorSupports = 1 << 16
PipesMk1 = 1 << 17
PipesMk2 = 1 << 18
PipelineSupports = 1 << 19
HyperTubes = 1 << 20
Signs = 1 << 21
Pilars = 1 << 22
Beams = 1 << 23
Walls = 1 << 24
Upgrades = 1 << 25
Vehicles = 1 << 26
Customizer = 1 << 27
ConveyorMk6 = 1 << 28
BasicNeeds = 1 << 29
class ItemData(NamedTuple):
"""Represents an item in the pool, it could be a resource bundle, production recipe, trap, etc."""
category: Set[ItemGroups]
category: ItemGroups
code: int
type: ItemClassification = ItemClassification.filler
count: int = 1

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ from .GameLogic import GameLogic, Recipe, Building, PowerInfrastructureLevel, Dr
from .StateLogic import StateLogic, EventId, part_event_prefix, building_event_prefix
from .Items import Items
from .Options import SatisfactoryOptions
from .CriticalPathCalculator import CriticalPathCalculator
from math import ceil, floor
@@ -43,9 +44,6 @@ 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 name == "Steel Ingot":
debug = True
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])
@@ -76,9 +74,6 @@ class EventBuilding(LocationData):
) -> Callable[[CollectionState], bool]:
def can_build(state: CollectionState) -> bool:
if building.name == "Building: Foundry":
debug = True
return state_logic.has_recipe(state, building) \
and state_logic.can_power(state, building.power_requirement) \
and state_logic.can_produce_all_allowing_handcrafting(state, game_logic, building.inputs)
@@ -177,6 +172,7 @@ class Locations():
options: Optional[SatisfactoryOptions]
state_logic: Optional[StateLogic]
items: Optional[Items]
critical_path: Optional[CriticalPathCalculator]
hub_location_start: ClassVar[int] = 1338000
max_tiers: ClassVar[int] = 10
@@ -186,11 +182,13 @@ class Locations():
drop_pod_location_id_end: ClassVar[int] = 1338699
def __init__(self, game_logic: Optional[GameLogic] = None, options: Optional[SatisfactoryOptions] = None,
state_logic: Optional[StateLogic] = None, items: Optional[Items] = None):
state_logic: Optional[StateLogic] = None, items: Optional[Items] = None,
critical_path: Optional[CriticalPathCalculator] = None):
self.game_logic = game_logic
self.options = options
self.state_logic = state_logic
self.items = items
self.critical_path = critical_path
def get_base_location_table(self) -> List[LocationData]:
return [
@@ -310,8 +308,8 @@ class Locations():
"Must include all possible location names and their id's"
location_table = self.get_base_location_table()
location_table.extend(self.get_hub_locations())
location_table.extend(self.get_drop_pod_locations())
location_table.extend(self.get_hub_locations(True, self.max_tiers))
location_table.extend(self.get_drop_pod_locations(True, self.max_tiers))
location_table.append(LocationData("Overworld", "UpperBound", 1338999))
return {location.name: location.code for location in location_table}
@@ -322,26 +320,38 @@ class Locations():
if not self.game_logic or not self.options or not self.state_logic or not self.items:
raise Exception("Locations need to be initialized with logic, options and items before using this method")
max_tier_for_game = min(self.options.final_elevator_package * 2, len(self.game_logic.hub_layout))
location_table = self.get_base_location_table()
location_table.extend(self.get_hub_locations())
location_table.extend(self.get_drop_pod_locations())
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))
location_table.extend(self.get_logical_event_locations())
return location_table
def get_hub_locations(self) -> List[LocationData]:
def get_hub_locations(self, for_data_package: bool, max_tier: int) -> List[LocationData]:
location_table: List[LocationData] = []
number_of_slots_per_milestone_for_game: int
if (for_data_package):
number_of_slots_per_milestone_for_game = self.max_slots
else:
if self.options.final_elevator_package <= 2:
number_of_slots_per_milestone_for_game = 10
else:
number_of_slots_per_milestone_for_game = self.game_logic.slots_per_milestone
hub_location_id = self.hub_location_start
for tier in range(1, self.max_tiers + 1):
for milestone in range(1, self.max_milestones + 1):
for slot in range(1, self.max_slots + 1):
if not self.game_logic:
for milestone in range(1, max_tier + 1):
for slot in range(1, number_of_slots_per_milestone_for_game + 1):
if for_data_package:
location_table.append(HubSlot(tier, milestone, slot, hub_location_id))
else:
if tier <= len(self.game_logic.hub_layout) \
if tier <= max_tier \
and milestone <= len(self.game_logic.hub_layout[tier - 1]) \
and slot <= self.game_logic.slots_per_milestone:
and slot <= number_of_slots_per_milestone_for_game:
location_table.append(HubSlot(tier, milestone, slot, hub_location_id))
hub_location_id += 1
@@ -356,11 +366,13 @@ class Locations():
location_table.extend(
ElevatorTier(index, self.state_logic, self.game_logic)
for index, parts in enumerate(self.game_logic.space_elevator_tiers))
for index, parts in enumerate(self.game_logic.space_elevator_tiers)
if index < self.options.final_elevator_package)
location_table.extend(
part
for part_name, recipes in self.game_logic.recipes.items()
for part in Part.get_parts(self.state_logic, recipes, part_name, self.items))
for part in Part.get_parts(self.state_logic, recipes, part_name, self.items)
if part in self.critical_path.potential_required_parts)
location_table.extend(
EventBuilding(self.game_logic, self.state_logic, name, building)
for name, building in self.game_logic.buildings.items())
@@ -370,28 +382,28 @@ class Locations():
return location_table
def get_drop_pod_locations(self) -> List[LocationData]:
def get_drop_pod_locations(self, for_data_package: bool, max_tier: int) -> List[LocationData]:
drop_pod_locations: List[DropPod] = []
bucket_size: int = 0
drop_pod_data: List[DropPodData] = []
if self.game_logic:
bucket_size = floor(
(self.drop_pod_location_id_end - self.drop_pod_location_id_start) / len(self.game_logic.hub_layout))
bucket_size: int
drop_pod_data: List[DropPodData]
if for_data_package:
bucket_size = 0
drop_pod_data = []
else:
bucket_size = floor((self.drop_pod_location_id_end - self.drop_pod_location_id_start) / max_tier)
drop_pod_data = self.game_logic.drop_pods
# sort, easily obtainable first, should be deterministic
drop_pod_data.sort(key = lambda data: ("!" if data.item == None else data.item) + str(data.x - data.z))
for location_id in range(self.drop_pod_location_id_start, self.drop_pod_location_id_end + 1):
if not self.game_logic or not self.state_logic or not self.options:
if for_data_package:
drop_pod_locations.append(DropPod(DropPodData(0, 0, 0, None, 0), None, location_id, 1, False))
else:
location_id_normalized: int = location_id - self.drop_pod_location_id_start
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), len(self.game_logic.hub_layout))
tier = min(ceil((location_id_normalized + 1) / bucket_size), max_tier)
drop_pod_locations.append(DropPod(data, self.state_logic, location_id, tier, can_hold_progression))

View File

@@ -44,13 +44,13 @@ class ChoiceMap(Choice, metaclass=ChoiceMapMeta):
class ElevatorTier(NamedRange):
"""
Ship these Space Elevator packages to finish.
Does nothing if *Space Elevator Tier* goal is not enabled.
Put these Shipments to Space Elevator packages in logic.
if your goal selection contains *Space Elevator Tier* then the goal will be to complete these shipments.
"""
display_name = "Goal: Space Elevator shipment"
default = 2
range_start = 1
range_end = 4
range_end = 5
special_range_names = {
"one package (tiers 1-2)": 1,
"two packages (tiers 1-4)": 2,

View File

@@ -42,6 +42,9 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory
]
for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1):
if (hub_tier > (options.final_elevator_package * 2)):
break
region_names.append(f"Hub Tier {hub_tier}")
for minestone, _ in enumerate(milestones_per_hub_tier, 1):
@@ -85,18 +88,27 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory
super_early_game_buildings.append("Conveyor Splitter")
super_early_game_buildings.append("Conveyor Merger")
if options.final_elevator_package == 1:
super_early_game_buildings.append(early_game_buildings)
connect(regions, "Menu", "Overworld")
connect(regions, "Overworld", "Hub Tier 1")
connect(regions, "Hub Tier 1", "Hub Tier 2",
lambda state: state_logic.can_build_all(state, super_early_game_buildings))
connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Tier 1", player)
if options.final_elevator_package >= 2:
connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Tier 1", player)
and state_logic.can_build_all(state, early_game_buildings))
connect(regions, "Hub Tier 3", "Hub Tier 4")
connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Tier 2", player))
connect(regions, "Hub Tier 5", "Hub Tier 6")
connect(regions, "Hub Tier 6", "Hub Tier 7", lambda state: state.has("Elevator Tier 3", player))
connect(regions, "Hub Tier 7", "Hub Tier 8")
connect(regions, "Hub Tier 8", "Hub Tier 9", lambda state: state.has("Elevator Tier 4", player))
connect(regions, "Hub Tier 3", "Hub Tier 4")
if options.final_elevator_package >= 3:
connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Tier 2", player))
connect(regions, "Hub Tier 5", "Hub Tier 6")
if options.final_elevator_package >= 4:
connect(regions, "Hub Tier 6", "Hub Tier 7", lambda state: state.has("Elevator Tier 3", player))
connect(regions, "Hub Tier 7", "Hub Tier 8")
if options.final_elevator_package >= 5:
connect(regions, "Hub Tier 8", "Hub Tier 9", lambda state: state.has("Elevator Tier 4", player))
connect(regions, "Overworld", "Gas Area", lambda state:
state_logic.can_produce_all(state, ("Gas Mask", "Gas Filter")))
connect(regions, "Overworld", "Radioactive Area", lambda state:
@@ -112,6 +124,9 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory
return logic_rule
for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1):
if (hub_tier > (options.final_elevator_package * 2)):
break
for minestone, parts_per_milestone in enumerate(milestones_per_hub_tier, 1):
connect(regions, f"Hub Tier {hub_tier}", f"Hub {hub_tier}-{minestone}",
can_produce_all_allowing_handcrafting(parts_per_milestone.keys()))

View File

@@ -38,10 +38,7 @@ class StateLogic:
return power_level is None or state.has(building_event_prefix + power_level.to_name(), self.player)
def can_produce_all(self, state: CollectionState, parts: Optional[Iterable[str]]) -> bool:
if parts and "SAM" in parts:
debug = "Now"
return parts is None or \
return parts is None or \
state.has_all(map(self.to_part_event, parts), self.player)
def can_produce_all_allowing_handcrafting(self, state: CollectionState, logic: GameLogic,
@@ -80,7 +77,9 @@ class StateLogic:
and self.can_produce_all(state, recipe.inputs)
def is_game_phase(self, state: CollectionState, phase: int) -> bool:
return state.has(f"Elevator Tier {phase}", self.player)
limited_phase = min(self.options.final_elevator_package * 2, phase)
return state.has(f"Elevator Tier {limited_phase}", self.player)
@staticmethod
def to_part_event(part: str) -> str:

View File

@@ -6,6 +6,7 @@ from .Locations import Locations, LocationData
from .StateLogic import EventId, StateLogic
from .Options import SatisfactoryOptions, Placement
from .Regions import SatisfactoryLocation, create_regions_and_return_locations
from .CriticalPathCalculator import CriticalPathCalculator
from .Web import SatisfactoryWebWorld
from ..AutoWorld import World
@@ -30,6 +31,7 @@ class SatisfactoryWorld(World):
game_logic: ClassVar[GameLogic] = GameLogic()
state_logic: StateLogic
items: Items
critical_path: CriticalPathCalculator
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
@@ -37,8 +39,11 @@ class SatisfactoryWorld(World):
def generate_early(self) -> None:
self.options.final_elevator_package.value = 1
self.state_logic = StateLogic(self.player, self.options)
self.items = Items(self.player, self.game_logic, self.random, self.options)
self.critical_path = CriticalPathCalculator(self.game_logic, self.random, self.options)
self.items = Items(self.player, self.game_logic, self.random, self.options, self.critical_path)
if not self.options.goal_selection.value:
raise Exception("""Satisfactory: player {} needs to choose a goal, the option goal_selection is empty"""
@@ -65,7 +70,7 @@ class SatisfactoryWorld(World):
def create_regions(self) -> None:
locations: List[LocationData] = \
Locations(self.game_logic, self.options, self.state_logic, self.items).get_locations()
Locations(self.game_logic, self.options, self.state_logic, self.items, self.critical_path).get_locations()
create_regions_and_return_locations(
self.multiworld, self.options, self.player, self.game_logic, self.state_logic, locations)