Add Crafting Machine awareness to Factorio logic
(should have no effect on vanilla, mostly for modded gameplay)
This commit is contained in:
parent
f8a5e8bfc7
commit
e1b4975a11
|
@ -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}}
|
|
@ -1,24 +1,37 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# Factorio technologies are imported from a .json document in /data
|
# Factorio technologies are imported from a .json document in /data
|
||||||
from typing import Dict, Set, FrozenSet
|
from typing import Dict, Set, FrozenSet
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import Utils
|
import Utils
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
factorio_id = 2 ** 17
|
factorio_id = 2 ** 17
|
||||||
source_file = Utils.local_path("data", "factorio", "techs.json")
|
source_folder = Utils.local_path("data", "factorio")
|
||||||
recipe_source_file = Utils.local_path("data", "factorio", "recipes.json")
|
|
||||||
with open(source_file) as f:
|
with open(os.path.join(source_folder, "techs.json")) as f:
|
||||||
raw = json.load(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)
|
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] = {}
|
tech_table: Dict[str, int] = {}
|
||||||
technology_table: Dict[str, Technology] = {}
|
technology_table: Dict[str, Technology] = {}
|
||||||
|
|
||||||
always = lambda state: True
|
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):
|
def __init__(self, name, ingredients, factorio_id):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.factorio_id = factorio_id
|
self.factorio_id = factorio_id
|
||||||
|
@ -27,34 +40,29 @@ class Technology(): # maybe make subclass of Location?
|
||||||
def build_rule(self, player: int):
|
def build_rule(self, player: int):
|
||||||
logging.debug(f"Building rules for {self.name}")
|
logging.debug(f"Building rules for {self.name}")
|
||||||
ingredient_rules = []
|
ingredient_rules = []
|
||||||
for ingredient in self.ingredients:
|
technologies = self.get_prior_technologies() # technologies that unlock the recipes
|
||||||
logging.debug(f"Building rules for ingredient {ingredient}")
|
if technologies:
|
||||||
technologies = required_technologies[ingredient] # technologies that unlock the recipes
|
logging.debug(f"Required Technologies: {technologies}")
|
||||||
if technologies:
|
ingredient_rules.append(
|
||||||
logging.debug(f"Required Technologies: {technologies}")
|
lambda state, technologies=technologies: all(state.has(technology.name, player)
|
||||||
ingredient_rules.append(
|
for technology in technologies))
|
||||||
lambda state, technologies=technologies: all(state.has(technology.name, player)
|
|
||||||
for technology in technologies))
|
|
||||||
if ingredient_rules:
|
if ingredient_rules:
|
||||||
ingredient_rules = frozenset(ingredient_rules)
|
ingredient_rules = frozenset(ingredient_rules)
|
||||||
return lambda state: all(rule(state) for rule in ingredient_rules)
|
return lambda state: all(rule(state) for rule in ingredient_rules)
|
||||||
|
|
||||||
return always
|
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."""
|
"""Get Technologies that have to precede this one to resolve tree connections."""
|
||||||
technologies = set()
|
technologies = set()
|
||||||
for ingredient in self.ingredients:
|
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
|
return technologies
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return self.factorio_id
|
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:
|
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
|
||||||
return CustomTechnology(self, world, allowed_packs, player)
|
return CustomTechnology(self, world, allowed_packs, player)
|
||||||
|
|
||||||
|
@ -67,13 +75,12 @@ class CustomTechnology(Technology):
|
||||||
self.player = player
|
self.player = player
|
||||||
if world.random_tech_ingredients[player]:
|
if world.random_tech_ingredients[player]:
|
||||||
ingredients = list(ingredients)
|
ingredients = list(ingredients)
|
||||||
ingredients.sort() # deterministic sample
|
ingredients.sort() # deterministic sample
|
||||||
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
||||||
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(FactorioElement):
|
||||||
class Recipe():
|
|
||||||
def __init__(self, name, category, ingredients, products):
|
def __init__(self, name, category, ingredients, products):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.category = category
|
self.category = category
|
||||||
|
@ -83,12 +90,22 @@ class Recipe():
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.name})"
|
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
|
@property
|
||||||
def unlocking_technologies(self) -> Set[Technology]:
|
def unlocking_technologies(self) -> Set[Technology]:
|
||||||
"""Unlocked by any of the returned technologies. Empty set indicates a starting recipe."""
|
"""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, ())}
|
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
|
# recipes and technologies can share names in Factorio
|
||||||
for technology_name in sorted(raw):
|
for technology_name in sorted(raw):
|
||||||
data = raw[technology_name]
|
data = raw[technology_name]
|
||||||
|
@ -108,16 +125,25 @@ for technology, data in raw.items():
|
||||||
del (raw)
|
del (raw)
|
||||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
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():
|
for recipe_name, recipe_data in raw_recipes.items():
|
||||||
# example:
|
# example:
|
||||||
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
|
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
|
||||||
|
|
||||||
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
|
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:
|
for product_name in recipe.products:
|
||||||
all_product_sources.setdefault(product_name, set()).add(recipe)
|
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
|
# build requirements graph for all technology ingredients
|
||||||
|
|
||||||
|
@ -126,7 +152,23 @@ for technology in technology_table.values():
|
||||||
all_ingredient_names |= technology.ingredients
|
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 _done:
|
||||||
if ingredient_name in _done:
|
if ingredient_name in _done:
|
||||||
return set()
|
return set()
|
||||||
|
@ -139,15 +181,50 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[T
|
||||||
return set()
|
return set()
|
||||||
current_technologies = set()
|
current_technologies = set()
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
current_technologies |= recipe.unlocking_technologies
|
current_technologies |= unlock_func(recipe, _done)
|
||||||
for ingredient_name in recipe.ingredients:
|
|
||||||
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
|
|
||||||
return current_technologies
|
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]] = {}
|
required_technologies: Dict[str, FrozenSet[Technology]] = {}
|
||||||
for ingredient_name in all_ingredient_names:
|
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()
|
advancement_technologies: Set[str] = set()
|
||||||
for technologies in required_technologies.values():
|
for technologies in required_technologies.values():
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
|
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
|
from .Shapes import get_shapes
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue