Skip to content

Commit

Permalink
Factorio: flatten science pack curve (ArchipelagoMW#1660)
Browse files Browse the repository at this point in the history
Co-authored-by: black-sliver <[email protected]>
  • Loading branch information
Berserker66 and black-sliver authored Apr 9, 2023
1 parent 6628e8c commit 8e7bbb4
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 90 deletions.
12 changes: 5 additions & 7 deletions worlds/factorio/Locations.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
from typing import Dict, List

from .Technologies import factorio_base_id, factorio_id
from .Technologies import factorio_base_id
from .Options import MaxSciencePack

boundary: int = 0xff
total_locations: int = 0xff

assert total_locations <= boundary
assert factorio_base_id != factorio_id


def make_pools() -> Dict[str, List[str]]:
pools: Dict[str, List[str]] = {}
for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1):
max_needed: int = sum(divmod(boundary, i))
scale: float = boundary / max_needed
max_needed: int = 0xff
prefix: str = f"AP-{i}-"
pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)]
pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)]
return pools


location_pools: Dict[str, List[str]] = make_pools()

location_table: Dict[str, int] = {}
end_id: int = factorio_id
end_id: int = factorio_base_id
for pool in location_pools.values():
location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)})
end_id += len(pool)

assert end_id - len(location_table) == factorio_id
assert end_id - len(location_table) == factorio_base_id
del pool
2 changes: 1 addition & 1 deletion worlds/factorio/Mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def load_template(name: str):
settings_template = template_env.get_template("settings.lua")
# get data for templates
locations = [(location, location.item)
for location in world.locations]
for location in world.science_locations]
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"

random = multiworld.per_slot_randoms[player]
Expand Down
2 changes: 1 addition & 1 deletion worlds/factorio/Shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se
player = factorio_world.player
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value
locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name)
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
world.random.shuffle(locations)

if layout == TechTreeLayout.option_single:
Expand Down
28 changes: 13 additions & 15 deletions worlds/factorio/Technologies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
import logging
import os
import string
import pkgutil
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any

import Utils
from . import Options

factorio_id = factorio_base_id = 2 ** 17
factorio_tech_id = factorio_base_id = 2 ** 17
# Factorio technologies are imported from a .json document in /data
source_folder = os.path.join(os.path.dirname(__file__), "data")

pool = ThreadPoolExecutor(1)


def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
import pkgutil
return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())


Expand All @@ -33,7 +33,9 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {}

always = lambda state: True

def always(state):
return True


class FactorioElement:
Expand All @@ -49,7 +51,6 @@ def __hash__(self):
class Technology(FactorioElement): # maybe make subclass of Location?
has_modifier: bool
factorio_id: int
name: str
ingredients: Set[str]
progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
Expand Down Expand Up @@ -192,9 +193,9 @@ def __init__(self, name, categories):
# recipes and technologies can share names in Factorio
for technology_name, data in sorted(techs_future.result().items()):
current_ingredients = set(data["ingredients"])
technology = Technology(technology_name, current_ingredients, factorio_id,
technology = Technology(technology_name, current_ingredients, factorio_tech_id,
has_modifier=data["has_modifier"], unlocks=set(data["unlocks"]))
factorio_id += 1
factorio_tech_id += 1
tech_table[technology_name] = technology.factorio_id
technology_table[technology_name] = technology
for recipe_name in technology.unlocks:
Expand Down Expand Up @@ -391,17 +392,13 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_
progressive_rows["progressive-wall"] = ("stone-wall", "gate")
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")

sorted_rows = sorted(progressive_rows)
# to keep ID mappings the same.
# If there's a breaking change at some point, then this should be moved in with the sorted ordering
progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret")
sorted_rows.append("progressive-turret")
progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing
sorted_rows.append("progressive-flamethrower")
progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment",
"personal-roboport-mk2-equipment")
sorted_rows.append("progressive-personal-roboport-equipment")

sorted_rows = sorted(progressive_rows)

# integrate into
source_target_mapping: Dict[str, str] = {
"progressive-braking-force": "progressive-train-network",
Expand All @@ -421,8 +418,8 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_
for root in sorted_rows:
progressive = progressive_rows[root]
assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology"
factorio_id += 1
progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id,
factorio_tech_id += 1
progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id,
progressive,
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
unlocks=any(technology_table[tech].unlocks for tech in progressive))
Expand Down Expand Up @@ -504,3 +501,4 @@ def get_estimated_difficulty(recipe: Recipe):
# cleanup async helpers
pool.shutdown()
del pool
del factorio_tech_id
133 changes: 67 additions & 66 deletions worlds/factorio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components
from worlds.generic import Rules
from .Locations import location_pools, location_table
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Shapes import get_shapes
Expand All @@ -14,8 +17,6 @@
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
fluids, stacking_items, valid_ingredients, progressive_rows
from .Locations import location_pools, location_table
from worlds.LauncherComponents import Component, components

components.append(Component("Factorio Client", "FactorioClient"))

Expand Down Expand Up @@ -64,18 +65,19 @@ class Factorio(World):
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
data_version = 7
required_client_version = (0, 3, 6)
data_version = 8
required_client_version = (0, 4, 0)

ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
tech_mix: int = 0
skip_silo: bool = False
science_locations: typing.List[FactorioScienceLocation]

def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player)
self.advancement_technologies = set()
self.custom_recipes = {}
self.locations = []
self.science_locations = []

generate_output = generate_mod

Expand Down Expand Up @@ -115,29 +117,29 @@ def create_regions(self):
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e

self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names]
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
min_cost = self.multiworld.min_tech_cost[self.player]
max_cost = self.multiworld.max_tech_cost[self.player]
if distribution == distribution.option_even:
rand_values = (random.randint(min_cost, max_cost) for _ in self.locations)
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
else:
mode = {distribution.option_low: min_cost,
distribution.option_middle: (min_cost+max_cost)//2,
distribution.option_high: max_cost}[distribution.value]
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.locations)
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]:
def sorter(loc: FactorioScienceLocation):
return loc.complexity, loc.rel_cost
else:
def sorter(loc: FactorioScienceLocation):
return loc.rel_cost
for i, location in enumerate(sorted(self.locations, key=sorter)):
for i, location in enumerate(sorted(self.science_locations, key=sorter)):
location.count = rand_values[i]
del rand_values
nauvis.locations.extend(self.locations)
nauvis.locations.extend(self.science_locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location)
event = FactorioItem("Victory", ItemClassification.progression, None, player)
Expand All @@ -154,73 +156,25 @@ def sorter(loc: FactorioScienceLocation):

def create_items(self) -> None:
player = self.player
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld,
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))

def set_rules(self):
world = self.multiworld
player = 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.multiworld.max_science_pack[self.player].get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player)

if self.multiworld.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 location in self.locations:
Rules.set_rule(location, lambda state, ingredients=location.ingredients:
all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
prerequisites = shapes.get(location)
if prerequisites:
Rules.add_rule(location, lambda state, locations=
prerequisites: all(state.can_reach(loc) for loc in locations))

silo_recipe = None
if self.multiworld.silo[self.player] == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None
if self.multiworld.goal[self.player] == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
if self.multiworld.silo[self.player] != Silo.option_spawn:
victory_tech_names.add("rocket-silo")
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)

world.completion_condition[player] = lambda state: state.has('Victory', player)

def generate_basic(self):
player = self.player
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
want_progressives(self.multiworld.random))

cost_sorted_locations = sorted(self.locations, key=lambda location: location.name)
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
special_index = {"automation": 0,
"logistics": 1,
"rocket-silo": -1}
loc: FactorioScienceLocation
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
# mark all locations as pre-hinted
for loc in self.locations:
for loc in self.science_locations:
loc.revealed = True
if self.skip_silo:
removed = useless_technologies | {"rocket-silo"}
Expand All @@ -244,13 +198,60 @@ def generate_basic(self):
loc.place_locked_item(tech_item)
loc.revealed = True

map_basic_settings = self.multiworld.world_gen[player].value["basic"]
def set_rules(self):
world = self.multiworld
player = self.player
shapes = get_shapes(self)

for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player)

if self.multiworld.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 location in self.science_locations:
Rules.set_rule(location, lambda state, ingredients=location.ingredients:
all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
prerequisites = shapes.get(location)
if prerequisites:
Rules.add_rule(location, lambda state, locations=
prerequisites: all(state.can_reach(loc) for loc in locations))

silo_recipe = None
if self.multiworld.silo[self.player] == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None
if self.multiworld.goal[self.player] == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
if self.multiworld.silo[self.player] != Silo.option_spawn:
victory_tech_names.add("rocket-silo")
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)

world.completion_condition[player] = lambda state: state.has('Victory', player)

def generate_basic(self):
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[player].randint(0, 2 ** 32 - 1) # 32 bit uint
# 32 bit uint
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)

start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value

for loc in self.locations:
for loc in self.science_locations:
# show start_location_hints ingame
if loc.name in start_location_hints:
loc.revealed = True
Expand Down

0 comments on commit 8e7bbb4

Please sign in to comment.