Factorio: flatten science pack curve (#1660)

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
Fabian Dill 2023-04-09 20:58:24 +02:00 committed by GitHub
parent 6628e8c85d
commit 8e7bbb4ea8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 90 deletions

View File

@ -1,32 +1,30 @@
from typing import Dict, List from typing import Dict, List
from .Technologies import factorio_base_id, factorio_id from .Technologies import factorio_base_id
from .Options import MaxSciencePack from .Options import MaxSciencePack
boundary: int = 0xff boundary: int = 0xff
total_locations: int = 0xff total_locations: int = 0xff
assert total_locations <= boundary assert total_locations <= boundary
assert factorio_base_id != factorio_id
def make_pools() -> Dict[str, List[str]]: def make_pools() -> Dict[str, List[str]]:
pools: Dict[str, List[str]] = {} pools: Dict[str, List[str]] = {}
for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1):
max_needed: int = sum(divmod(boundary, i)) max_needed: int = 0xff
scale: float = boundary / max_needed
prefix: str = f"AP-{i}-" 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 return pools
location_pools: Dict[str, List[str]] = make_pools() location_pools: Dict[str, List[str]] = make_pools()
location_table: Dict[str, int] = {} location_table: Dict[str, int] = {}
end_id: int = factorio_id end_id: int = factorio_base_id
for pool in location_pools.values(): for pool in location_pools.values():
location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)})
end_id += len(pool) end_id += len(pool)
assert end_id - len(location_table) == factorio_id assert end_id - len(location_table) == factorio_base_id
del pool del pool

View File

@ -96,7 +96,7 @@ def generate_mod(world: "Factorio", output_directory: str):
settings_template = template_env.get_template("settings.lua") settings_template = template_env.get_template("settings.lua")
# get data for templates # get data for templates
locations = [(location, location.item) 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)}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
random = multiworld.per_slot_randoms[player] random = multiworld.per_slot_randoms[player]

View File

@ -24,7 +24,7 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se
player = factorio_world.player player = factorio_world.player
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value 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) world.random.shuffle(locations)
if layout == TechTreeLayout.option_single: if layout == TechTreeLayout.option_single:

View File

@ -4,6 +4,7 @@ import json
import logging import logging
import os import os
import string import string
import pkgutil
from collections import Counter from collections import Counter
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any
@ -11,7 +12,7 @@ from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any
import Utils import Utils
from . import Options 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 # Factorio technologies are imported from a .json document in /data
source_folder = os.path.join(os.path.dirname(__file__), "data") source_folder = os.path.join(os.path.dirname(__file__), "data")
@ -19,7 +20,6 @@ pool = ThreadPoolExecutor(1)
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: 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()) return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
@ -33,7 +33,9 @@ items_future = pool.submit(load_json_data, "items")
tech_table: Dict[str, int] = {} tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {} technology_table: Dict[str, Technology] = {}
always = lambda state: True
def always(state):
return True
class FactorioElement: class FactorioElement:
@ -49,7 +51,6 @@ class FactorioElement:
class Technology(FactorioElement): # maybe make subclass of Location? class Technology(FactorioElement): # maybe make subclass of Location?
has_modifier: bool has_modifier: bool
factorio_id: int factorio_id: int
name: str
ingredients: Set[str] ingredients: Set[str]
progressive: Tuple[str] progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies unlocks: Union[Set[str], bool] # bool case is for progressive technologies
@ -192,9 +193,9 @@ recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
# recipes and technologies can share names in Factorio # recipes and technologies can share names in Factorio
for technology_name, data in sorted(techs_future.result().items()): for technology_name, data in sorted(techs_future.result().items()):
current_ingredients = set(data["ingredients"]) 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"])) has_modifier=data["has_modifier"], unlocks=set(data["unlocks"]))
factorio_id += 1 factorio_tech_id += 1
tech_table[technology_name] = technology.factorio_id tech_table[technology_name] = technology.factorio_id
technology_table[technology_name] = technology technology_table[technology_name] = technology
for recipe_name in technology.unlocks: for recipe_name in technology.unlocks:
@ -391,17 +392,13 @@ progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "ene
progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-wall"] = ("stone-wall", "gate")
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") 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") 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 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", progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment",
"personal-roboport-mk2-equipment") "personal-roboport-mk2-equipment")
sorted_rows.append("progressive-personal-roboport-equipment")
sorted_rows = sorted(progressive_rows)
# integrate into # integrate into
source_target_mapping: Dict[str, str] = { source_target_mapping: Dict[str, str] = {
"progressive-braking-force": "progressive-train-network", "progressive-braking-force": "progressive-train-network",
@ -421,8 +418,8 @@ progressive_technology_table: Dict[str, Technology] = {}
for root in sorted_rows: for root in sorted_rows:
progressive = progressive_rows[root] progressive = progressive_rows[root]
assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology"
factorio_id += 1 factorio_tech_id += 1
progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id, progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id,
progressive, progressive,
has_modifier=any(technology_table[tech].has_modifier for tech in progressive), has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
unlocks=any(technology_table[tech].unlocks for tech in progressive)) unlocks=any(technology_table[tech].unlocks for tech in progressive))
@ -504,3 +501,4 @@ valid_ingredients: Set[str] = stacking_items | fluids
# cleanup async helpers # cleanup async helpers
pool.shutdown() pool.shutdown()
del pool del pool
del factorio_tech_id

View File

@ -6,6 +6,9 @@ import typing
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld 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 .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
from .Shapes import get_shapes from .Shapes import get_shapes
@ -14,8 +17,6 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
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, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
fluids, stacking_items, valid_ingredients, progressive_rows 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")) components.append(Component("Factorio Client", "FactorioClient"))
@ -64,18 +65,19 @@ class Factorio(World):
item_name_groups = { item_name_groups = {
"Progressive": set(progressive_tech_table.keys()), "Progressive": set(progressive_tech_table.keys()),
} }
data_version = 7 data_version = 8
required_client_version = (0, 3, 6) required_client_version = (0, 4, 0)
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
tech_mix: int = 0 tech_mix: int = 0
skip_silo: bool = False skip_silo: bool = False
science_locations: typing.List[FactorioScienceLocation]
def __init__(self, world, player: int): def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player) super(Factorio, self).__init__(world, player)
self.advancement_technologies = set() self.advancement_technologies = set()
self.custom_recipes = {} self.custom_recipes = {}
self.locations = [] self.science_locations = []
generate_output = generate_mod generate_output = generate_mod
@ -115,18 +117,18 @@ class Factorio(World):
raise Exception("Too many traps for too few locations. Either decrease the trap count, " 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 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) self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names] for loc_name in location_names]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
min_cost = self.multiworld.min_tech_cost[self.player] min_cost = self.multiworld.min_tech_cost[self.player]
max_cost = self.multiworld.max_tech_cost[self.player] max_cost = self.multiworld.max_tech_cost[self.player]
if distribution == distribution.option_even: 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: else:
mode = {distribution.option_low: min_cost, mode = {distribution.option_low: min_cost,
distribution.option_middle: (min_cost+max_cost)//2, distribution.option_middle: (min_cost+max_cost)//2,
distribution.option_high: max_cost}[distribution.value] 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) rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]: if self.multiworld.ramping_tech_costs[self.player]:
def sorter(loc: FactorioScienceLocation): def sorter(loc: FactorioScienceLocation):
@ -134,10 +136,10 @@ class Factorio(World):
else: else:
def sorter(loc: FactorioScienceLocation): def sorter(loc: FactorioScienceLocation):
return loc.rel_cost 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] location.count = rand_values[i]
del rand_values del rand_values
nauvis.locations.extend(self.locations) nauvis.locations.extend(self.science_locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis) location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = FactorioItem("Victory", ItemClassification.progression, None, player) event = FactorioItem("Victory", ItemClassification.progression, None, player)
@ -154,20 +156,53 @@ class Factorio(World):
def create_items(self) -> None: def create_items(self) -> None:
player = self.player player = self.player
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps: for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld, range(getattr(self.multiworld,
f"{trap_name.lower().replace(' ', '_')}_traps")[player])) f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
want_progressives(self.multiworld.random))
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.science_locations:
loc.revealed = True
if self.skip_silo:
removed = useless_technologies | {"rocket-silo"}
else:
removed = useless_technologies
for tech_name in base_tech_table:
if tech_name not in removed:
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
want_progressive = want_progressives[progressive_item_name]
item_name = progressive_item_name if want_progressive else tech_name
tech_item = self.create_item(item_name)
index = special_index.get(tech_name, None)
if index is None:
self.multiworld.itempool.append(tech_item)
else:
loc = cost_sorted_locations[index]
if index >= 0:
# beginning techs - limit cost to 10
# as automation is not achievable yet and hand-crafting for hours is not fun gameplay
loc.count = min(loc.count, 10)
loc.place_locked_item(tech_item)
loc.revealed = True
def set_rules(self): def set_rules(self):
world = self.multiworld world = self.multiworld
player = self.player player = self.player
self.custom_technologies = self.set_custom_technologies()
self.set_custom_recipes()
shapes = get_shapes(self) 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(): for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player) location = world.get_location(f"Automate {ingredient}", player)
@ -182,7 +217,7 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for location in self.locations: for location in self.science_locations:
Rules.set_rule(location, lambda state, ingredients=location.ingredients: Rules.set_rule(location, lambda state, ingredients=location.ingredients:
all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
prerequisites = shapes.get(location) prerequisites = shapes.get(location)
@ -209,48 +244,14 @@ class Factorio(World):
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def generate_basic(self): def generate_basic(self):
player = self.player map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
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)
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:
loc.revealed = True
if self.skip_silo:
removed = useless_technologies | {"rocket-silo"}
else:
removed = useless_technologies
for tech_name in base_tech_table:
if tech_name not in removed:
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
want_progressive = want_progressives[progressive_item_name]
item_name = progressive_item_name if want_progressive else tech_name
tech_item = self.create_item(item_name)
index = special_index.get(tech_name, None)
if index is None:
self.multiworld.itempool.append(tech_item)
else:
loc = cost_sorted_locations[index]
if index >= 0:
# beginning techs - limit cost to 10
# as automation is not achievable yet and hand-crafting for hours is not fun gameplay
loc.count = min(loc.count, 10)
loc.place_locked_item(tech_item)
loc.revealed = True
map_basic_settings = self.multiworld.world_gen[player].value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0 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 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 # show start_location_hints ingame
if loc.name in start_location_hints: if loc.name in start_location_hints:
loc.revealed = True loc.revealed = True