Factorio: recipe randomization (rocket-part and science-packs only for now)

This commit is contained in:
Fabian Dill 2021-07-07 10:14:58 +02:00
parent 007f2caecf
commit 9db506ef42
9 changed files with 224 additions and 81 deletions

View File

@ -1492,6 +1492,12 @@ class Spoiler(object):
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.world.factorio_player_ids:
outfile.write('\n\nRecipes:\n')
for player in self.world.factorio_player_ids:
for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"{recipe.name}: {recipe.ingredients} -> {recipe.products}\n")
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))

View File

@ -23,6 +23,7 @@ import sys
import pickle
import functools
import io
import collections
from yaml import load, dump, safe_load
@ -53,7 +54,6 @@ def snes_to_pc(value):
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
if len(names) != len(set(names)):
import collections
name_counter = collections.Counter(names)
raise ValueError(f"Duplicate Player names is not supported, "
f'found multiple "{name_counter.most_common(1)[0][0]}".')
@ -405,4 +405,9 @@ class RestrictedUnpickler(pickle.Unpickler):
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
return RestrictedUnpickler(io.BytesIO(s)).load()
class KeyedDefaultDict(collections.defaultdict):
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value

View File

@ -2,7 +2,9 @@
-- 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 = {{ dict_to_recipe(rocket_recipe) }}
{%- for recipe_name, recipe in custom_recipes.items() %}
data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ingredients) }}
{%- endfor %}
local technologies = data.raw["technology"]
local original_tech

View File

@ -70,6 +70,9 @@ Factorio:
normal: 0 # 50 % to 200% of original time
slow: 0 # 100% to 400% of original time
chaos: 0 # 25% to 400% of original time
recipe_ingredients:
rocket: 1 # only randomize rocket part recipe
science_packs: 1 # also randomize science pack ingredients
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0

View File

@ -9,7 +9,6 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
from worlds.generic.Rules import forbid_items_for_player
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@ -349,9 +348,7 @@ def generate_itempool(world, player: int):
world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items():
world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
world.get_location(location, player).event = True
world.get_location(location, player).locked = True
world.get_location(location, player).place_locked_item(ItemFactory(item, player))
items = ItemFactory(pool, player)

View File

@ -43,8 +43,9 @@ recipe_time_scales = {
Options.RecipeTime.option_vanilla: None
}
def generate_mod(world: MultiWorld, player: int):
def generate_mod(world):
player = world.player
multiworld = world.world
global data_final_template, locale_template, control_template, data_template
with template_load_lock:
if not data_final_template:
@ -56,36 +57,36 @@ def generate_mod(world: MultiWorld, player: int):
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}
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids}
locations = []
for location in world.get_filled_locations(player):
for location in multiworld.get_filled_locations(player):
if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}"
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}"
tech_cost_scale = {0: 0.1,
1: 0.25,
2: 0.5,
3: 1,
4: 2,
5: 5,
6: 10}[world.tech_cost[player].value]
6: 10}[multiworld.tech_cost[player].value]
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": world.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
"rocket_recipe": rocket_recipes[world.max_science_pack[player].value],
"slot_name": world.player_names[player][0], "seed_name": world.seed_name,
"starting_items": world.starting_items[player], "recipes": recipes,
"random": world.slot_seeds[player], "static_nodes": world.worlds[player].static_nodes,
"recipe_time_scale": recipe_time_scales[world.recipe_time[player].value],
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name,
"starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes,
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
"progressive_technology_table": {tech.name : tech.progressive for tech in
progressive_technology_table.values()}}
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes}
for factorio_option in Options.factorio_options:
template_data[factorio_option] = getattr(world, factorio_option)[player].value
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
control_code = control_template.render(**template_data)
data_template_code = data_template.render(**template_data)

View File

@ -17,6 +17,12 @@ class MaxSciencePack(Choice):
return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \
{"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense
@classmethod
def get_ordered_science_packs(cls):
return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])]
def get_max_pack(self):
return self.get_ordered_science_packs()[self.value].replace("_", "-")
class TechCost(Choice):
option_very_easy = 0
@ -68,6 +74,19 @@ class RecipeTime(Choice):
option_slow = 4
option_chaos = 5
# TODO: implement random
class Progressive(Choice):
option_off = 0
option_random = 1
option_on = 2
default = 2
def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_random else int(self.value)
class RecipeIngredients(Choice):
option_rocket = 0
option_science_pack = 1
class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19}
@ -95,6 +114,7 @@ factorio_options: typing.Dict[str, type(Option)] = {
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"imported_blueprints": DefaultOnToggle,
"world_gen": FactorioWorldGen,
"progressive": DefaultOnToggle

View File

@ -1,6 +1,7 @@
from __future__ import annotations
# Factorio technologies are imported from a .json document in /data
from typing import Dict, Set, FrozenSet, Tuple
from collections import Counter, defaultdict
import os
import json
import string
@ -92,15 +93,37 @@ class Recipe(FactorioElement):
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]
def crafting_machine(self) -> str:
"""cheapest crafting machine name able to run this recipe"""
return machine_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, ())}
@property
def recursive_unlocking_technologies(self) -> Set[Technology]:
base = {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
for ingredient in self.ingredients:
base |= required_technologies[ingredient]
return base
@property
def rel_cost(self) -> float:
ingredients = sum(self.ingredients.values())
return min(ingredients/amount for product, amount in self.products.items())
@property
def base_cost(self) -> Dict[str, int]:
ingredients = Counter()
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
for recipe in all_product_sources[ingredient]:
ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()})
else:
ingredients[ingredient] += cost
return ingredients
class Machine(FactorioElement):
def __init__(self, name, categories):
@ -137,7 +160,9 @@ for recipe_name, recipe_data in raw_recipes.items():
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"])
recipes[recipe_name] = recipe
if set(recipe.products).isdisjoint(
set(recipe.ingredients)) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
# prevents loop recipes like uranium centrifuging
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
not recipe_name.endswith("-reprocessing"):
for product_name in recipe.products:
all_product_sources.setdefault(product_name, set()).add(recipe)
@ -151,6 +176,8 @@ for name, categories in raw_machines.items():
# add electric mining drill as a crafting machine to resolve uranium-ore
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"})
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
del (raw_machines)
# build requirements graph for all technology ingredients
@ -161,18 +188,17 @@ for technology in technology_table.values():
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done,
unlock_func=unlock_just_tech)
return current_technologies
def unlock(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
current_technologies = recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock)
current_technologies |= required_category_technologies[recipe.category]
return current_technologies
@ -202,48 +228,41 @@ for ingredient_name in machines:
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
logical_machines = {}
machine_tech_cost = {}
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)
current_cost, current_machine = machine_tech_cost.get(category, (10000, "character"))
machine_cost = len(required_machine_technologies[machine.name])
if machine_cost < current_cost:
machine_tech_cost[category] = machine_cost, machine.name
machine_per_category: Dict[str: str] = {}
for category, (cost, machine_name) in machine_tech_cost.items():
machine_per_category[category] = machine_name
del (machine_tech_cost)
# 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():
for category_name, machine_name in machine_per_category.items():
techs = set()
for machine in cat_machines:
techs |= recursively_get_unlocking_technologies(machine.name)
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, unlock_func=unlock))
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset(
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
advancement_technologies: Set[str] = set()
for technologies in required_technologies.values():
for ingredient_name in all_ingredient_names:
technologies = required_technologies[ingredient_name]
advancement_technologies |= {technology.name for technology in technologies}
@functools.lru_cache(10)
def get_rocket_requirements(ingredients: Set[str]) -> Set[str]:
def get_rocket_requirements(recipe: Recipe) -> Set[str]:
techs = recursively_get_unlocking_technologies("rocket-silo")
for ingredient in ingredients:
for ingredient in recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
return {tech.name for tech in techs}
@ -266,9 +285,8 @@ rocket_recipes = {
Options.MaxSciencePack.option_automation_science_pack:
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
}
for products in rocket_recipes.values():
requirements = get_rocket_requirements(frozenset(products))
advancement_technologies |= requirements
advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]}
# progressive technologies
# auto-progressive
@ -357,4 +375,46 @@ technology_table.update(progressive_technology_table)
common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items()
if tech_name not in progressive_tech_table}
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()}
rel_cost = {
"wood" : 10000,
"iron-ore": 1,
"copper-ore": 1,
"stone": 1,
"crude-oil": 0.5,
"water": 0.001,
"coal": 1,
"raw-fish": 1000,
"steam": 0.01,
"used-up-uranium-fuel-cell": 1000
}
# forbid liquids for now, TODO: allow a single liquid per assembler
blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil",
"heavy-oil", "lubricant", "steam"}
def get_estimated_difficulty(recipe: Recipe):
base_ingredients = recipe.base_cost
cost = 0
for ingredient_name, amount in base_ingredients.items():
if ingredient_name not in rel_cost:
raise Exception((recipe.name, ingredient_name))
cost += rel_cost.get(ingredient_name, 1) * amount
return cost
science_pack_pools = {}
already_taken = blacklist.copy()
current_difficulty = 5
for science_pack in Options.MaxSciencePack.get_ordered_science_packs():
current = science_pack_pools[science_pack] = set()
for name, recipe in recipes.items():
if (science_pack != "automation-science-pack" or not recipe.recursive_unlocking_technologies) \
and get_estimated_difficulty(recipe) < current_difficulty:
current |= set(recipe.products)
current -= already_taken
already_taken |= current
current_difficulty *= 2

View File

@ -1,9 +1,10 @@
from ..AutoWorld import World
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from BaseClasses import Region, Entrance, Location, Item
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
science_pack_pools, Recipe, recipes, technology_table
from .Shapes import get_shapes
from .Mod import generate_mod
from .Options import factorio_options
@ -11,6 +12,8 @@ from .Options import factorio_options
class Factorio(World):
game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes = {}
additional_advancement_technologies = set()
def generate_basic(self):
for tech_name, tech_id in base_tech_table.items():
@ -20,7 +23,8 @@ class Factorio(World):
else:
item_name = tech_name
tech_item = Item(item_name, item_name in advancement_technologies,
tech_item = Item(item_name, item_name in advancement_technologies or
item_name in self.additional_advancement_technologies,
tech_id, self.player)
tech_item.game = "Factorio"
if tech_name in self.static_nodes:
@ -28,11 +32,10 @@ class Factorio(World):
else:
self.world.itempool.append(tech_item)
world_gen = self.world.world_gen[self.player].value
if world_gen.get("seed", None) is None: # allow seed 0
if world_gen.get("seed", None) is None: # allow seed 0
world_gen["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint
def generate_output(self):
generate_mod(self.world, self.player)
generate_output = generate_mod
def create_regions(self):
player = self.player
@ -48,11 +51,14 @@ class Factorio(World):
tech.game = "Factorio"
location = Location(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location)
location.game = "Factorio"
event = Item("Victory", True, None, player)
event.game = "Factorio"
self.world.push_item(location, event, False)
location.event = location.locked = True
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
location = Location(player, f"Automate {ingredient}", None, nauvis)
location.game = "Factorio"
nauvis.locations.append(location)
event = Item(f"Automated {ingredient}", True, None, player)
self.world.push_item(location, event, False)
@ -63,14 +69,25 @@ class Factorio(World):
def set_rules(self):
world = self.world
player = self.player
self.custom_technologies = set_custom_technologies(self.world, self.player)
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
shapes = get_shapes(self)
if world.logic[player] != 'nologic':
from worlds.generic import Rules
for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
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])
if self.world.recipe_ingredients[self.player]:
custom_recipe = self.custom_recipes[ingredient]
location.access_rule = lambda state, ingredient=ingredient, custom_recipe = custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
for technology in required_technologies[sub_ingredient])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for tech_name, technology in self.custom_technologies.items():
location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player))
@ -79,8 +96,8 @@ class Factorio(World):
locations = {world.get_location(requisite, player) for requisite in prequisites}
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]))
victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"])
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
@ -101,9 +118,41 @@ class Factorio(World):
options = factorio_options
def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies = {}
allowed_packs = world.max_science_pack[player].get_allowed_packs()
for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
return custom_technologies
def set_custom_technologies(self):
custom_technologies = {}
allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs()
for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(self.world, allowed_packs, self.player)
return custom_technologies
def set_custom_recipes(self):
original_rocket_part = recipes["rocket-part"]
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
self.world.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x] : 10 for x in range(3)},
original_rocket_part.products)}
self.additional_advancement_technologies = {tech.name for tech in
self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
if self.world.recipe_ingredients[self.player]:
valid_pool = []
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack])
self.world.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack
original = recipes[pack]
new_ingredients = {}
for _ in original.ingredients:
new_ingredients[valid_pool.pop()] = 1
new_recipe = Recipe(pack, original.category, new_ingredients, original.products)
self.additional_advancement_technologies |= {tech.name for tech in
new_recipe.recursive_unlocking_technologies}
self.custom_recipes[pack] = new_recipe
# handle marking progressive techs as advancement
prog_add = set()
for tech in self.additional_advancement_technologies:
if tech in tech_to_progressive_lookup:
prog_add.add(tech_to_progressive_lookup[tech])
self.additional_advancement_technologies |= prog_add