From ceea55e3c679bf6b08e6988f6fe8be9c24a164bd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 9 Apr 2021 22:10:04 +0200 Subject: [PATCH] traverse recipe tree for Factorio logic --- playerSettings.yaml | 14 ++--- worlds/factorio/Technologies.py | 92 +++++++++++++++++++++++++-------- worlds/factorio/__init__.py | 14 ++--- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index 3e470690..380cdaa4 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -196,11 +196,9 @@ retro: hints: 'on': 50 # Hint tiles sometimes give item location hints 'off': 0 # Hint tiles provide gameplay tips -weapons: # Specifically, swords - randomized: 0 # Swords are placed randomly throughout the world - assured: 50 # Begin with a sword, the rest are placed randomly throughout the world - vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) - swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change +swordless: + on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change + off: 1 item_pool: easy: 0 # Doubled upgrades, progressives, and etc normal: 50 # Item availability remains unchanged from vanilla game @@ -259,12 +257,14 @@ beemizer: # Remove items from the global item pool and replace them with single 2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees 3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees 4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees + 5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees ### Shop Settings ### shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) 0: 50 5: 0 15: 0 30: 0 + random: 0 # 0 to 30 evenly distributed shop_shuffle: none: 50 g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops @@ -325,8 +325,8 @@ meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option - inverted # Never play inverted seeds retro: - on # Never play retro seeds - weapons: - - swordless # Never play a swordless seed + swordless: + - on # Never play a swordless seed linked_options: - name: crosskeys options: # These overwrite earlier options if the percentage chance triggers diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 590cfbe5..7c9f41e8 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,7 +1,8 @@ # Factorio technologies are imported from a .json document in /data -from typing import Dict, Set +from typing import Dict, Set, FrozenSet import json import Utils +import logging factorio_id = 2 ** 17 @@ -23,31 +24,43 @@ class Technology(): # maybe make subclass of Location? factorio_id += 1 self.ingredients = ingredients - def get_required_technologies(self): - requirements = set() - for ingredient in self.ingredients: - if ingredient in recipe_sources: # no source likely means starting item - requirements |= recipe_sources[ingredient] # technically any, not all, need to improve later - return requirements - - def build_rule(self): + def build_rule(self, player: int): + logging.debug(f"Building rules for {self.name}") ingredient_rules = [] for ingredient in self.ingredients: - if ingredient in recipe_sources: - technologies = recipe_sources[ingredient] # technologies that unlock the recipe - ingredient_rules.append(lambda state, technologies=technologies: any(state.has(technology) for technology in technologies)) + logging.debug(f"Building rules for ingredient {ingredient}") + if ingredient in required_technologies: + 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)) ingredient_rules = frozenset(ingredient_rules) return lambda state: all(rule(state) for rule in ingredient_rules) def __hash__(self): return self.factorio_id + def __repr__(self): + return f"{self.__class__.__name__}({self.name})" + + class Recipe(): def __init__(self, name, category, ingredients, products): self.name = name self.category = category - self.products = ingredients - self.ingredients = products + self.ingredients = ingredients + self.products = products + + def __repr__(self): + return f"{self.__class__.__name__}({self.name})" + + @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, ())} + # recipes and technologies can share names in Factorio for technology_name in sorted(raw): @@ -62,20 +75,55 @@ for technology_name in sorted(raw): tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology - -recipe_sources = {} # recipe_name -> technology source +recipe_sources: Dict[str, str] = {} # recipe_name -> technology source for technology, data in raw.items(): - for recipe in data["unlocks"]: - recipe_sources.setdefault(recipe, set()).add(technology) - + for recipe_name in data["unlocks"]: + recipe_sources.setdefault(recipe_name, set()).add(technology) del (raw) lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} - -all_recipes = set() +all_recipes: Dict[str, Recipe] = {} +all_product_sources: Dict[str, Recipe] = {} for recipe_name, recipe_data in raw_recipes.items(): # example: # "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"} - all_recipes.add(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: # prevents loop recipes like uranium centrifuging + all_recipes[recipe_name] = recipe + for product_name in recipe.products: + all_product_sources[product_name] = recipe + +# build requirements graph for all technology ingredients + +all_ingredient_names: Set[str] = set() +for technology in technology_table.values(): + all_ingredient_names |= technology.ingredients + + +def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]: + if _done: + if ingredient_name in _done: + return set() + else: + _done.add(ingredient_name) + else: + _done = set(ingredient_name) + recipe = all_product_sources.get(ingredient_name) + if not recipe: + return set() + current_technologies = recipe.unlocking_technologies.copy() + for ingredient_name in recipe.ingredients: + current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + return current_technologies + + +required_technologies: Dict[str, FrozenSet[Technology]] = {} +for ingredient_name in all_ingredient_names: + required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) + +advancement_technologies: Set[str] = set() +for technologies in required_technologies.values(): + advancement_technologies |= {technology.name for technology in technologies} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8b53f889..36d437aa 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,19 +2,21 @@ import logging from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from .Technologies import tech_table, recipe_sources, technology_table +from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies static_nodes = {"automation", "logistics"} def gen_factorio(world: MultiWorld, player: int): for tech_name, tech_id in tech_table.items(): + # TODO: some techs don't need the advancement marker tech_item = Item(tech_name, True, tech_id, player) tech_item.game = "Factorio" if tech_name in static_nodes: loc = world.get_location(tech_name, player) loc.item = tech_item - loc.locked = loc.event = True + loc.locked = True + loc.event = tech_item.advancement else: world.itempool.append(tech_item) set_rules(world, player) @@ -39,11 +41,9 @@ def set_rules(world: MultiWorld, player: int): from worlds.generic import Rules for tech_name, technology in technology_table.items(): # loose nodes - rules = technology.get_required_technologies() - if rules: - location = world.get_location(tech_name, player) - Rules.set_rule(location, lambda state, rules=rules: all(state.has(rule, player) for rule in rules)) + location = world.get_location(tech_name, player) + Rules.set_rule(location, technology.build_rule(player)) # get all technologies world.completion_condition[player] = lambda state: all(state.has(technology, player) - for technology in tech_table) + for technology in advancement_technologies)