From 80b7e2e1884cd7b56d61b8a699604cdda3cfc47e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 22 May 2021 10:06:21 +0200 Subject: [PATCH] Factorio: Build logic for rocket launch, allow beatable only to work correctly Convert Science requirements to Event of "automate " --- data/factorio/mod_template/control.lua | 9 +--- .../mod_template/data-final-fixes.lua | 3 +- data/factorio/mod_template/macros.lua | 14 +++++++ worlds/factorio/Mod.py | 30 ++++--------- worlds/factorio/Technologies.py | 42 +++++++++++++------ worlds/factorio/__init__.py | 25 +++++++++-- 6 files changed, 78 insertions(+), 45 deletions(-) create mode 100644 data/factorio/mod_template/macros.lua diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index b57c26e5..17d80e1c 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -1,10 +1,5 @@ -{% macro dict_to_lua(dict) -%} -{ - {% for key, value in dict.items() %} - ["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %} - {% endfor %} -} -{%- endmacro %} +{% from "macros.lua" import dict_to_lua %} +-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require "lib" require "util" diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index 8c3d65a9..4d44b187 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -1,7 +1,8 @@ +{% from "macros.lua" import dict_to_recipe %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') -data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }} +data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }} local technologies = data.raw["technology"] local original_tech diff --git a/data/factorio/mod_template/macros.lua b/data/factorio/mod_template/macros.lua new file mode 100644 index 00000000..47b179ed --- /dev/null +++ b/data/factorio/mod_template/macros.lua @@ -0,0 +1,14 @@ +{% macro dict_to_lua(dict) -%} +{ +{%- for key, value in dict.items() -%} + ["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %} +{% endfor -%} +} +{%- endmacro %} +{% macro dict_to_recipe(dict) -%} +{ +{%- for key, value in dict.items() -%} + {"{{ key }}", {{ value | safe }}}{% if not loop.last %},{% endif %} +{% endfor -%} +} +{%- endmacro %} \ No newline at end of file diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 2cbee1f4..7f34f684 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -11,7 +11,9 @@ import Utils import shutil import Options from BaseClasses import MultiWorld -from .Technologies import tech_table +from .Technologies import tech_table, rocket_recipes + +template_env: Optional[jinja2.Environment] = None template: Optional[jinja2.Template] = None locale_template: Optional[jinja2.Template] = None @@ -28,32 +30,18 @@ base_info = { "factorio_version": "1.1" } -# TODO: clean this up, probably as a jinja macro; then add logic for the recipes in completion condition -rocket_recipes = { - Options.MaxSciencePack.option_space_science_pack: - '{{"rocket-control-unit", 10}, {"low-density-structure", 10}, {"rocket-fuel", 10}}', - Options.MaxSciencePack.option_utility_science_pack: - '{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}', - Options.MaxSciencePack.option_production_science_pack: - '{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}', - Options.MaxSciencePack.option_chemical_science_pack: - '{{"advanced-circuit", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}', - Options.MaxSciencePack.option_military_science_pack: - '{{"defender-capsule", 10}, {"stone-wall", 10}, {"coal", 10}}', - Options.MaxSciencePack.option_logistic_science_pack: - '{{"electronic-circuit", 10}, {"stone-brick", 10}, {"coal", 10}}', - Options.MaxSciencePack.option_automation_science_pack: - '{{"copper-cable", 10}, {"iron-plate", 10}, {"wood", 10}}' -} def generate_mod(world: MultiWorld, player: int): global template, locale_template, control_template with template_load_lock: if not template: mod_template_folder = Utils.local_path("data", "factorio", "mod_template") - template = jinja2.Template(open(os.path.join(mod_template_folder, "data-final-fixes.lua")).read()) - locale_template = jinja2.Template(open(os.path.join(mod_template_folder, "locale", "en", "locale.cfg")).read()) - control_template = jinja2.Template(open(os.path.join(mod_template_folder, "control.lua")).read()) + template_env: Optional[jinja2.Environment] = \ + jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder])) + + template = template_env.get_template("data-final-fixes.lua") + locale_template = template_env.get_template(r"locale/en/locale.cfg") + control_template = template_env.get_template("control.lua") # get data for templates player_names = {x: world.player_names[x][0] for x in world.player_ids} locations = [] diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index f810df29..bfdd9822 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -3,8 +3,11 @@ from __future__ import annotations from typing import Dict, Set, FrozenSet import os import json + +import Options import Utils import logging +import functools factorio_id = 2 ** 17 source_folder = Utils.local_path("data", "factorio") @@ -39,19 +42,9 @@ class Technology(FactorioElement): # maybe make subclass of Location? def build_rule(self, player: int): logging.debug(f"Building rules for {self.name}") - ingredient_rules = [] - 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 + return lambda state, technologies=technologies: all(state.has(f"Automated {ingredient}", player) + for ingredient in self.ingredients) def get_prior_technologies(self) -> Set[Technology]: """Get Technologies that have to precede this one to resolve tree connections.""" @@ -229,3 +222,28 @@ for ingredient_name in all_ingredient_names: advancement_technologies: Set[str] = set() for technologies in required_technologies.values(): advancement_technologies |= {technology.name for technology in technologies} + +@functools.lru_cache(10) +def get_rocket_requirements(ingredients: Set[str]) -> Set[str]: + techs = set() + for ingredient in ingredients: + techs |= recursively_get_unlocking_technologies(ingredient) + return {tech.name for tech in techs} + + +rocket_recipes = { + Options.MaxSciencePack.option_space_science_pack: + {"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10}, + Options.MaxSciencePack.option_utility_science_pack: + {"speed-module": 10, "steel-plate": 10, "solid-fuel": 10}, + Options.MaxSciencePack.option_production_science_pack: + {"speed-module": 10, "steel-plate": 10, "solid-fuel": 10}, + Options.MaxSciencePack.option_chemical_science_pack: + {"advanced-circuit": 10, "steel-plate": 10, "solid-fuel": 10}, + Options.MaxSciencePack.option_military_science_pack: + {"defender-capsule": 10, "stone-wall": 10, "coal": 10}, + Options.MaxSciencePack.option_logistic_science_pack: + {"electronic-circuit": 10, "stone-brick": 10, "coal": 10}, + Options.MaxSciencePack.option_automation_science_pack: + {"copper-cable": 10, "iron-plate": 10, "wood": 10} +} \ No newline at end of file diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index ca5e469b..c58b07d7 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,12 +1,15 @@ from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies +from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \ + all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes from .Shapes import get_shapes def gen_factorio(world: MultiWorld, player: int): static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? + victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) for tech_name, tech_id in tech_table.items(): - tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player) + tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, + tech_id, player) tech_item.game = "Factorio" if tech_name in static_nodes: loc = world.get_location(tech_name, player) @@ -25,6 +28,7 @@ def factorio_create_regions(world: MultiWorld, player: int): menu.exits.append(crash) nauvis = Region("Nauvis", None, "Nauvis", player) nauvis.world = menu.world = world + for tech_name, tech_id in tech_table.items(): tech = Location(player, tech_name, tech_id, nauvis) nauvis.locations.append(tech) @@ -34,9 +38,16 @@ def factorio_create_regions(world: MultiWorld, player: int): event = Item("Victory", True, None, player) world.push_item(location, event, False) location.event = location.locked = True + for ingredient in all_ingredient_names: + location = Location(player, f"Automate {ingredient}", None, nauvis) + nauvis.locations.append(location) + event = Item(f"Automated {ingredient}", True, None, player) + world.push_item(location, event, False) + location.event = location.locked = True crash.connect(nauvis) world.regions += [menu, nauvis] + def set_custom_technologies(world: MultiWorld, player: int): custom_technologies = {} world_custom = getattr(world, "_custom_technologies", {}) @@ -47,11 +58,15 @@ def set_custom_technologies(world: MultiWorld, player: int): custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player) return custom_technologies + def set_rules(world: MultiWorld, player: int, custom_technologies): shapes = get_shapes(world, player) if world.logic[player] != 'nologic': from worlds.generic import Rules - + for ingredient in all_ingredient_names: + location = world.get_location(f"Automate {ingredient}", player) + location.access_rule = lambda state, ingredient=ingredient: \ + all(state.has(technology.name, player) for technology in required_technologies[ingredient]) for tech_name, technology in custom_technologies.items(): location = world.get_location(tech_name, player) Rules.set_rule(location, technology.build_rule(player)) @@ -61,7 +76,9 @@ def set_rules(world: MultiWorld, player: int, custom_technologies): Rules.add_rule(location, lambda state, locations=locations: all(state.can_reach(loc) for loc in locations)) # get all science pack technologies (but not the ability to craft them) + victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in advancement_technologies) + for technology in + victory_tech_names) world.completion_condition[player] = lambda state: state.has('Victory', player)