Factorio: flatten science pack curve (#1660)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
parent
6628e8c85d
commit
8e7bbb4ea8
|
@ -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
|
||||
|
|
|
@ -96,7 +96,7 @@ def generate_mod(world: "Factorio", output_directory: 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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -4,6 +4,7 @@ import json
|
|||
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
|
||||
|
@ -11,7 +12,7 @@ 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")
|
||||
|
||||
|
@ -19,7 +20,6 @@ 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())
|
||||
|
||||
|
||||
|
@ -33,7 +33,9 @@ items_future = pool.submit(load_json_data, "items")
|
|||
tech_table: Dict[str, int] = {}
|
||||
technology_table: Dict[str, Technology] = {}
|
||||
|
||||
always = lambda state: True
|
||||
|
||||
def always(state):
|
||||
return True
|
||||
|
||||
|
||||
class FactorioElement:
|
||||
|
@ -49,7 +51,6 @@ class FactorioElement:
|
|||
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
|
||||
|
@ -192,9 +193,9 @@ recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
|
|||
# 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:
|
||||
|
@ -391,17 +392,13 @@ progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "ene
|
|||
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",
|
||||
|
@ -421,8 +418,8 @@ progressive_technology_table: Dict[str, Technology] = {}
|
|||
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))
|
||||
|
@ -504,3 +501,4 @@ valid_ingredients: Set[str] = stacking_items | fluids
|
|||
# cleanup async helpers
|
||||
pool.shutdown()
|
||||
del pool
|
||||
del factorio_tech_id
|
||||
|
|
|
@ -6,6 +6,9 @@ import typing
|
|||
|
||||
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
|
||||
|
@ -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, \
|
||||
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"))
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -115,18 +117,18 @@ class Factorio(World):
|
|||
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):
|
||||
|
@ -134,10 +136,10 @@ class Factorio(World):
|
|||
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)
|
||||
|
@ -154,73 +156,25 @@ class Factorio(World):
|
|||
|
||||
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"}
|
||||
|
@ -244,13 +198,60 @@ class Factorio(World):
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue