import math import typing as tp from functools import lru_cache import pdbp # noqa: F401 ResourceGroup = dict[str, float] @lru_cache def _dot_align(seq: tuple[float, ...]) -> dict[float, str]: strs = [(f'{n:.9g}', n) for n in seq] dots = [(s.find('.') if '.' in s else len(s), s, n) for s, n in strs] md, _, _ = max(dots) rv = {n: (' ' * (md - d) + s)[: md + 4] for d, s, n in dots} assert not all(s.startswith(' ') for s in rv.values()) return rv def dot_align(seq: tuple[float, ...], n) -> str: return _dot_align(seq)[n] recipies: dict[str, ResourceGroup] = { # mining 'iron-ore': {'mining-drill': -1, 'iron-ore': 0.5}, 'copper-ore': {'mining-drill': -1, 'copper-ore': 0.5}, 'stone': {'mining-drill': -1, 'stone': 0.5}, 'coal': {'mining-drill': -1, 'coal': 0.5}, 'uranium-ore': {'mining-drill': -1, 'sulfuric-acid': -0.25, 'uranium-ore': 0.5}, 'water': {'offshore-pump': -1, 'water': 1200}, 'crude-oil': {'pumpjack': -1, 'crude-oil': 10}, # pumpjack @ 100% yield = 10 crude-oil / second # oil 'basic-oil-processing': {'oil-refinery': -5, 'crude-oil': -100, 'petroleum-gas': 45}, 'advanced-oil-processing': { 'oil-refinery': -5, 'crude-oil': -100, 'water': -50, 'petroleum-gas': 55, 'light-oil': 45, 'heavy-oil': 25, }, 'coal-liquefaction': { 'oil-refinery': -5, 'coal': -10, 'steam': -50, 'petroleum-gas': 10, 'light-oil': 20, 'heavy-oil': -25 + 90, }, 'heavy-oil-cracking': {'chemical-plant': -2, 'heavy-oil': -40, 'water': -30, 'light-oil': 30}, 'light-oil-cracking': {'chemical-plant': -2, 'light-oil': -30, 'water': -30, 'petroleum-gas': 20}, # fuel 'solid-fuel': {'chemical-plant': -2, 'petroleum-gas': -20, 'solid-fuel': 1}, 'solid-fuel-lo': {'chemical-plant': -2, 'light-oil': -10, 'solid-fuel': 1}, 'solid-fuel-ho': {'chemical-plant': -2, 'heavy-oil': -20, 'solid-fuel': 1}, 'rocket-fuel': {'assembler': -30, 'light-oil': -10, 'solid-fuel': -10, 'rocket-fuel': 1}, # smelting 'iron-plate': {'furnace': -3.2, 'iron-ore': -1, 'iron-plate': 1}, 'copper-plate': {'furnace': -3.2, 'copper-ore': -1, 'copper-plate': 1}, 'stone-brick': {'furnace': -3.2, 'stone': -2, 'stone-brick': 1}, 'steel': {'furnace': -16, 'iron-plate': -5, 'steel': 1}, # crafting basics 'iron-gear': {'assembler': -0.5, 'iron-plate': -2, 'iron-gear': 1}, 'iron-stick': {'assembler': -0.5, 'iron-plate': -1, 'iron-stick': 2}, 'pipe': {'assembler': -0.5, 'iron-plate': -1, 'pipe': 1}, 'copper-cable': {'assembler': -0.5, 'copper-plate': -1, 'copper-cable': 2}, # circuits 'circuit-a': {'assembler': -0.5, 'copper-cable': -3, 'iron-plate': -1, 'circuit-a': 1}, 'circuit-b': {'assembler': -6, 'copper-cable': -4, 'circuit-a': -2, 'plastic': -2, 'circuit-b': 1}, 'circuit-c': {'assembler': -10, 'circuit-b': -2, 'circuit-a': -20, 'sulfuric-acid': -5, 'circuit-c': 1}, # logistics 'belt-y': {'assembler': -0.5, 'iron-gear': -1, 'iron-plate': -1, 'belt-y': 2}, 'inserter-y': {'assembler': -0.5, 'circuit-a': -1, 'iron-gear': -1, 'iron-plate': -1, 'inserter-y': 1}, # transport 'concrete': {'assembler': -10, 'iron-ore': -1, 'stone-brick': -5, 'water': -100, 'concrete': 10}, 'rail': {'assembler': -0.5, 'iron-stick': -1, 'steel': -1, 'stone': -1, 'rail': 2}, # chemicals 'sulfur': {'chemical-plant': -1, 'petroleum-gas': -30, 'water': -30, 'sulfur': 2}, 'sulfuric-acid': {'chemical-plant': -1, 'iron-plate': -1, 'sulfur': -5, 'water': -100, 'sulfuric-acid': 50}, 'plastic': {'chemical-plant': -1, 'coal': -1, 'petroleum-gas': -20, 'plastic': 2}, 'explosives': {'chemical-plant': -4, 'coal': -1, 'sulfur': -1, 'water': -10, 'explosives': 2}, 'battery': {'chemical-plant': -4, 'copper-plate': -1, 'iron-plate': -1, 'sulfuric-acid': -20, 'battery': 1}, 'lubricant': {'chemical-plant': -1, 'heavy-oil': -10, 'lubricant': 10}, # military 'radar': {'assembler': -0.5, 'circuit-a': -5, 'iron-gear': -5, 'iron-plate': -10, 'radar': 1}, 'magazine-y': {'assembler': -1, 'iron-plate': -4, 'magazine-y': 1}, 'magazine-r': {'assembler': -3, 'copper-plate': -5, 'magazine-y': -1, 'steel': -1, 'magazine-r': 1}, 'grenade': {'assembler': -8, 'coal': -10, 'iron-plate': -5, 'grenade': 1}, 'wall': {'assembler': -0.5, 'stone-brick': -5, 'wall': 1}, 'rocket-y': {'assembler': -8, 'circuit-a': -1, 'explosives': -1, 'iron-plate': -2, 'rocket-y': 1}, 'rocket-r': {'assembler': -8, 'explosives': -2, 'rocket-r': 1}, # intermediates 'engine-unit': {'assembler': -10, 'iron-gear': -1, 'pipe': -2, 'steel': -1, 'engine-unit': 1}, 'engine-unit-electric': { 'assembler': -10, 'circuit-a': -2, 'engine-unit': -1, 'lubricant': -15, 'engine-unit-electric': 1, }, 'accumulator': {'assembler': -10, 'battery': -5, 'iron-plate': -2, 'accumulator': 1}, 'solar-panel': {'assembler': -10, 'copper-plate': -5, 'circuit-a': -15, 'steel': -5, 'solar-panel': 1}, 'electric-furnace': {'assembler': -5, 'circuit-b': -5, 'steel': -10, 'stone-brick': -10, 'electric-furnace': 1}, 'flying-robot-frame': { 'assembler': -20, 'battery': -2, 'engine-unit-electric': -1, 'circuit-a': -3, 'steel': -1, 'flying-robot-frame': 1, }, 'low-density-structure': { 'assembler': -20, 'copper-plate': -20, 'plastic': -5, 'steel': -2, 'low-density-structure': 1, }, # modules 'module-productivity': {'assembler': -15, 'circuit-b': -5, 'circuit-a': -5, 'module-productivity': 1}, 'module-speed': {'assembler': -15, 'circuit-b': -5, 'circuit-a': -5, 'module-speed': 1}, # end-game 'rocket-control-unit': {'assembler': -30, 'circuit-c': -1, 'module-speed': -1, 'rocket-control-unit': 1}, 'rocket-part': { 'rocket-silo': -3, 'low-density-structure': -10, 'rocket-control-unit': -10, 'rocket-fuel': -10, 'rocket-part': 1, }, 'sattelite': { 'assembler': -5, 'accumulator': -100, 'low-density-structure': -100, 'circuit-c': -100, 'radar': -5, 'rocket-fuel': -50, 'solar-panel': -100, 'sattelite': 1, }, # science packs 'science-red': {'assembler': -5, 'copper-plate': -1, 'iron-gear': -1, 'science-red': 1}, 'science-green': {'assembler': -6, 'belt-y': -1, 'inserter-y': -1, 'science-green': 1}, 'science-gray': {'assembler': -10, 'magazine-r': -1, 'grenade': -1, 'wall': -2, 'science-gray': 2}, 'science-blue': {'assembler': -24, 'sulfur': -1, 'circuit-b': -3, 'engine-unit': -2, 'science-blue': 2}, 'science-purple': { 'assembler': -21, 'rail': -30, 'electric-furnace': -1, 'module-productivity': -1, 'science-purple': 3, }, 'science-yellow': { 'assembler': -21, 'circuit-c': -2, 'flying-robot-frame': -1, 'low-density-structure': -3, 'science-yellow': 3, }, 'science-white': {'rocket-part': -100, 'sattelite': -1, 'science-white': 1000}, } MACHINES = { 'assembler', 'chemical-plant', 'oil-refinery', 'rocket-silo', 'furnace', 'mining-drill', 'pumpjack', 'offshore-pump', } def get_resource_name(orig_name: str, recipe_name: str) -> str: if orig_name in MACHINES: return f'{orig_name}[{recipe_name}]' else: return orig_name def add_assembler_name(recipe_name: str, resources: ResourceGroup) -> ResourceGroup: return {get_resource_name(resource, recipe_name): count for resource, count in resources.items()} recipies = {name: add_assembler_name(name, resources) for name, resources in recipies.items()} def add_recipe(targets: ResourceGroup, recipe: ResourceGroup, multiplier: float) -> ResourceGroup: return { resource: targets.get(resource, 0) - (multiplier * recipe.get(resource, 0)) for resource in targets.keys() | recipe.keys() if targets.get(resource, 0) - (multiplier * recipe.get(resource, 0)) != 0 } def sum_recipies(recipies: tp.Iterable[ResourceGroup]) -> ResourceGroup: result = {} for recipe in recipies: result = add_recipe(result, recipe, -1) return result def reduce_to_base_resources(group: ResourceGroup, base: set[str]) -> tuple[ResourceGroup, ResourceGroup]: group = group.copy() reduced = {} intermediates = {} while len(group) > 0: resource, count = next(iter(group.items())) # print(f'{resource}: {count}') if resource in base or '[' in resource: reduced = add_recipe(reduced, {resource: count}, -1) del group[resource] continue if resource not in recipies: raise RuntimeError(f'{resource=} not in base or recipies') intermediates = add_recipe(intermediates, {resource: count}, -1) recipe = recipies[resource] multiplier = count / recipe[resource] group = add_recipe(group, recipe, multiplier) return reduced, intermediates def drop_machines(group: ResourceGroup) -> tuple[ResourceGroup, ResourceGroup]: group_wo_machines = {k: v for k, v in group.items() if not k.startswith(tuple(MACHINES))} group_machines = {k: v for k, v in group.items() if k.startswith(tuple(MACHINES))} return group_wo_machines, group_machines def print_resource_group(group: ResourceGroup) -> None: for resource, count in sorted(group.items()): print(f' {resource:40}: {dot_align(tuple(group.values()), count)}') SPM = 1 bus_groups: list[ResourceGroup] = [ {'science-red': SPM}, {'science-green': SPM}, {'science-gray': SPM}, {'science-blue': SPM}, {'science-purple': SPM}, {'science-yellow': SPM}, {'science-white': SPM}, ] bus_base = { 'iron-plate', 'copper-plate', 'stone', 'stone-brick', 'coal', 'steel', 'sulfur', 'sulfuric-acid', 'plastic', 'lubricant', 'rocket-fuel', } print(f'{SPM=}') print(f'{bus_base=}') bus_inputs = {} for group in bus_groups: reduced, intermediates = reduce_to_base_resources(group, bus_base) print() print(f'{group=}') print('intermediates') print_resource_group(intermediates) print('reduced') print_resource_group(reduced) bus_inputs = add_recipe(bus_inputs, reduced, -1) bus_inputs, bus_machines = drop_machines(bus_inputs) print() print('main bus inputs') print_resource_group(bus_inputs) # chemical processing chemical_outputs_keys = {'sulfur', 'sulfuric-acid', 'plastic', 'rocket-fuel', 'lubricant'} chemical_outputs = {k: v for k, v in bus_inputs.items() if k in chemical_outputs_keys} bus_inputs = {k: v for k, v in bus_inputs.items() if k not in chemical_outputs_keys} chemical_base = {'petroleum-gas', 'light-oil', 'heavy-oil', 'water', 'iron-plate', 'coal'} print() print('chemical processing') print() print(f'{chemical_outputs=}') print(f'{chemical_base=}') chemical_inputs, intermediates = reduce_to_base_resources(chemical_outputs, chemical_base) print('intermediates') print_resource_group(intermediates) print('reduced') print_resource_group(chemical_inputs) chemical_inputs, chemical_machines = drop_machines(chemical_inputs) print() print('chemical processing inputs') print_resource_group(chemical_inputs) # oil refinery oil_outputs_keys = {'petroleum-gas', 'light-oil', 'heavy-oil'} oil_outputs = {k: v for k, v in chemical_inputs.items() if k in oil_outputs_keys} chemical_inputs = {k: v for k, v in chemical_inputs.items() if k not in oil_outputs_keys} # TODO: create exactly enough oil such that there is enough petroleum-gas # free A, B, C, define m, n where A * X + B * Y + C * Z + m * U + n * V = outputs # A, B, C = multiplier on basic/advanced/coal refinery # m, n = multiplier on heavy/light cracking # define J, K, L proportional to A, B, C ratio = { 'basic-oil-processing': 0, 'advanced-oil-processing': 35, 'coal-liquefaction': 5, } # ratio = { # 'basic-oil-processing': 0, # 'advanced-oil-processing': 10, # 'coal-liquefaction': 5, # } oil_inputs = oil_outputs.copy() for recipe_name, ratio in ratio.items(): or_secs = recipies[recipe_name][f'oil-refinery[{recipe_name}]'] oil_inputs = add_recipe(oil_inputs, recipies[recipe_name], -ratio / or_secs) hoc = recipies['heavy-oil-cracking'] oil_inputs = add_recipe(oil_inputs, hoc, oil_inputs['heavy-oil'] / hoc['heavy-oil']) loc = recipies['light-oil-cracking'] oil_inputs = add_recipe(oil_inputs, loc, oil_inputs['light-oil'] / loc['light-oil']) print() print('oil processing') print_resource_group(oil_inputs) oil_inputs, oil_machines = drop_machines(oil_inputs) if pg := oil_inputs.pop('petroleum-gas', 0) > 0: raise RuntimeError(f'petrolium-gas > 0: {pg=}') smelting_outputs = add_recipe(bus_inputs, oil_inputs, -1) smelting_outputs = add_recipe(smelting_outputs, chemical_inputs, -1) print() print('smelting outputs') print_resource_group(smelting_outputs) # smelting smelting_base = {'furnace', 'iron-ore', 'copper-ore', 'coal', 'stone', 'steam', 'water', 'crude-oil'} # combine to compute mining requirements smelting_inputs, intermediates = reduce_to_base_resources(smelting_outputs, smelting_base) print('intermediates') print_resource_group(intermediates) print('reduced') print_resource_group(smelting_inputs) # mining mining_outputs, smelting_machines = drop_machines(smelting_inputs) mining_base = {'mining-drill', 'pumpjack', 'offshore-pump', 'steam'} mining_inputs, intermediates = reduce_to_base_resources(mining_outputs, mining_base) print() print('mining intermediates') print_resource_group(intermediates) print('reduced') print_resource_group(mining_inputs) _, mining_machines = drop_machines(mining_inputs) def despecify_machines(group: ResourceGroup) -> ResourceGroup: # assembler[belt-y] -> assembler, etc. result = {} for resource, count in group.items(): if '[' in resource: resource = resource[: resource.find('[')] count = math.ceil(count) result[resource] = result.get(resource, 0) + count return result # TODO: assembers work at 0.75 efficiency... machines = sum_recipies([bus_machines, chemical_machines, oil_machines, smelting_machines, mining_machines]) machines = despecify_machines(machines) print() print('machines (despecified)') print_resource_group(machines) # TODO: total machine count # TODO: belt bandwidth