From e1b4975a11ac5fb60ed7c19d8b6586b28df01dea Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 19 May 2021 06:52:53 +0200 Subject: [PATCH] Add Crafting Machine awareness to Factorio logic (should have no effect on vanilla, mostly for modded gameplay) --- data/factorio/machines.json | 1 + worlds/factorio/Technologies.py | 135 +++++++++++++++++++++++++------- worlds/factorio/__init__.py | 2 +- 3 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 data/factorio/machines.json diff --git a/data/factorio/machines.json b/data/factorio/machines.json new file mode 100644 index 00000000..15a79580 --- /dev/null +++ b/data/factorio/machines.json @@ -0,0 +1 @@ +{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}} \ No newline at end of file diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 188c240b..f810df29 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,24 +1,37 @@ from __future__ import annotations # Factorio technologies are imported from a .json document in /data from typing import Dict, Set, FrozenSet +import os import json import Utils import logging factorio_id = 2 ** 17 -source_file = Utils.local_path("data", "factorio", "techs.json") -recipe_source_file = Utils.local_path("data", "factorio", "recipes.json") -with open(source_file) as f: +source_folder = Utils.local_path("data", "factorio") + +with open(os.path.join(source_folder, "techs.json")) as f: raw = json.load(f) -with open(recipe_source_file) as f: +with open(os.path.join(source_folder, "recipes.json")) as f: raw_recipes = json.load(f) +with open(os.path.join(source_folder, "machines.json")) as f: + raw_machines = json.load(f) tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} always = lambda state: True -class Technology(): # maybe make subclass of Location? +class FactorioElement(): + name: str + + def __repr__(self): + return f"{self.__class__.__name__}({self.name})" + + def __hash__(self): + return hash(self.name) + + +class Technology(FactorioElement): # maybe make subclass of Location? def __init__(self, name, ingredients, factorio_id): self.name = name self.factorio_id = factorio_id @@ -27,34 +40,29 @@ class Technology(): # maybe make subclass of Location? def build_rule(self, player: int): logging.debug(f"Building rules for {self.name}") ingredient_rules = [] - for ingredient in self.ingredients: - logging.debug(f"Building rules for ingredient {ingredient}") - technologies = required_technologies[ingredient] # technologies that unlock the recipes - if technologies: - logging.debug(f"Required Technologies: {technologies}") - ingredient_rules.append( - lambda state, technologies=technologies: all(state.has(technology.name, player) - for technology in technologies)) + technologies = self.get_prior_technologies() # technologies that unlock the recipes + if technologies: + logging.debug(f"Required Technologies: {technologies}") + ingredient_rules.append( + lambda state, technologies=technologies: all(state.has(technology.name, player) + for technology in technologies)) + if ingredient_rules: ingredient_rules = frozenset(ingredient_rules) return lambda state: all(rule(state) for rule in ingredient_rules) return always - def get_prior_technologies(self, allowed_packs) -> Set[Technology]: + def get_prior_technologies(self) -> Set[Technology]: """Get Technologies that have to precede this one to resolve tree connections.""" technologies = set() for ingredient in self.ingredients: - if ingredient in allowed_packs: - technologies |= required_technologies[ingredient] # technologies that unlock the recipes + technologies |= required_technologies[ingredient] # technologies that unlock the recipes return technologies def __hash__(self): return self.factorio_id - def __repr__(self): - return f"{self.__class__.__name__}({self.name})" - def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology: return CustomTechnology(self, world, allowed_packs, player) @@ -67,13 +75,12 @@ class CustomTechnology(Technology): self.player = player if world.random_tech_ingredients[player]: ingredients = list(ingredients) - ingredients.sort() # deterministic sample + ingredients.sort() # deterministic sample ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) - -class Recipe(): +class Recipe(FactorioElement): def __init__(self, name, category, ingredients, products): self.name = name self.category = category @@ -83,12 +90,22 @@ class Recipe(): def __repr__(self): return f"{self.__class__.__name__}({self.name})" + @property + def crafting_machines(self) -> Set[Machine]: + """crafting machines able to run this recipe""" + return machines_per_category[self.category] + @property def unlocking_technologies(self) -> Set[Technology]: """Unlocked by any of the returned technologies. Empty set indicates a starting recipe.""" return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())} +class Machine(FactorioElement): + def __init__(self, name, categories): + self.name: str = name + self.categories: set = categories + # recipes and technologies can share names in Factorio for technology_name in sorted(raw): data = raw[technology_name] @@ -108,16 +125,25 @@ for technology, data in raw.items(): del (raw) lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} -all_product_sources: Dict[str, Set[Recipe]] = {} +all_product_sources: Dict[str, Set[Recipe]] = {"character": set()} for recipe_name, recipe_data in raw_recipes.items(): # example: # "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"} recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"])) - if recipe.products != recipe.ingredients and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging + if recipe.products.isdisjoint(recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) +del (raw_recipes) + +machines: Dict[str, Machine] = {} + +for name, categories in raw_machines.items(): + machine = Machine(name, set(categories)) + machines[name] = machine + +del (raw_machines) # build requirements graph for all technology ingredients @@ -126,7 +152,23 @@ for technology in technology_table.values(): all_ingredient_names |= technology.ingredients -def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]: +def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: + current_technologies = set() + current_technologies |= recipe.unlocking_technologies + for ingredient_name in recipe.ingredients: + current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + return current_technologies + +def unlock(recipe: Recipe, _done) -> Set[Technology]: + current_technologies = set() + current_technologies |= recipe.unlocking_technologies + for ingredient_name in recipe.ingredients: + current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + current_technologies |= required_category_technologies[recipe.category] + + return current_technologies + +def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[Technology]: if _done: if ingredient_name in _done: return set() @@ -139,15 +181,50 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[T return set() current_technologies = set() for recipe in recipes: - current_technologies |= recipe.unlocking_technologies - for ingredient_name in recipe.ingredients: - current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + current_technologies |= unlock_func(recipe, _done) + return current_technologies + +required_machine_technologies: Dict[str, FrozenSet[Technology]] = {} +for ingredient_name in machines: + required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) + +logical_machines = {} +for machine in machines.values(): + logically_useful = True + for pot_source_machine in machines.values(): + if machine != pot_source_machine \ + and machine.categories.issuperset(pot_source_machine.categories) \ + and required_machine_technologies[machine.name].issuperset( + required_machine_technologies[pot_source_machine.name]): + logically_useful = False + break + + if logically_useful: + logical_machines[machine.name] = machine + +del(required_machine_technologies) + +machines_per_category: Dict[str: Set[Machine]] = {} +for machine in logical_machines.values(): + for category in machine.categories: + machines_per_category.setdefault(category, set()).add(machine) + +# required technologies to be able to craft recipes from a certain category +required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {} +for category_name, cat_machines in machines_per_category.items(): + techs = set() + for machine in cat_machines: + techs |= recursively_get_unlocking_technologies(machine.name) + required_category_technologies[category_name] = frozenset(techs) + required_technologies: Dict[str, FrozenSet[Technology]] = {} for ingredient_name in all_ingredient_names: - required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) + required_technologies[ingredient_name] = frozenset( + recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)) + advancement_technologies: Set[str] = set() for technologies in required_technologies.values(): diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 72613ac1..9072495e 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,5 +1,5 @@ from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies +from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies from .Shapes import get_shapes