satisfactory/requirements.py

175 lines
5.2 KiB
Python
Executable File

#!/usr/bin/env python3
import math
import collections
from fractions import Fraction
from typing import Dict, List
import satisfactory
def basic_rate(recipe: Dict) -> Fraction:
"""
Calculate the rate at which the item is crafted with the default recipe.
:param recipe:
:return:
"""
for output, count in recipe.output.items():
return Fraction(
count, recipe.crafting_time
)
def required_rates(
recipes: satisfactory.Cookbook,
remain: List[Dict[str, Fraction]]
) -> Dict[str, Fraction]:
"""
Calculate the total production rates needed to produce items at the rates
listed in 'remain' using the provided recipes.
:param recipes: The source Cookbook
:param remain: A mapping from requested items to their desired rate.
:return: A mapping from all required items to their required output rates.
"""
# Iteratively accumulate a mapping from items to required rates
required_items = collections.defaultdict(Fraction)
while remain:
for dst_name, dst_rate in remain.pop().items():
# Append the requested item to the output rate. We'll add the
# inputs just below.
required_items[dst_name] += dst_rate
# We can't craft resources, so just ignore them.
if not recipes.is_component(dst_name):
assert recipes.is_resource(dst_name)
continue
# Assume we're using the default recipe
dst_recipe = recipes[dst_name].recipes[0]
# Calculate the fraction of the base rate we need to satisfy, and
# append our (scaled) inputs to the request queue.
normal_rate = basic_rate(dst_recipe)
scale = dst_rate / normal_rate
for src_name, src_count in dst_recipe.input.items():
src_rate = Fraction(
src_count,
dst_recipe.crafting_time
) * scale
remain.append({src_name: src_rate})
return required_items
# The number of items per minute that each tier of conveyor can carry
conveyor_rates = [0, 60, 120, 270, 480, 780, 900]
def required_machines(recipes: satisfactory.Cookbook, rates: Dict[str, Fraction]) -> Dict[str, int]:
"""
Calculate the number of machines required to build each item at the
requested rates. This will, by necessity, round up rates where they are
below the output rate of the relevant machine.
:param recipes: The cookbook object we're working from
:param rates: A mapping from item names to requested output rates.
:return: A mapping from machine names to counts
"""
def numberdict():
return collections.defaultdict(int)
required_machines = collections.defaultdict(numberdict)
for name, requested_rate in rates.items():
if recipes.is_resource(name):
continue
descriptor = recipes[name]
normal_rate = Fraction(
descriptor.recipes[0].output[name],
descriptor.recipes[0].crafting_time
)
machine = descriptor.machine
required_machines[machine][name] += requested_rate / normal_rate
return required_machines
def required_power(recipes: satisfactory.Cookbook, machines: Dict[str, int]) -> int:
"""
Calculate the cumulative power requirements for a mapping of machine
names to counts
:param recipes: The cookbook we're working from
:param machines: A mapping from machine names to counts
:return: The total required power in MW
"""
# Calculate the power requirements for all the machines
total = 0
for machine, buckets in machines.items():
for result, rate in buckets.items():
count = int(math.ceil(rate))
source = recipes[machine]
total += count * source.power_usage
print(machine, result, math.ceil(rate))
return total
def plan(recipes: satisfactory.Cookbook, required: Dict[str, Fraction]):
"""
Print the items and rates, machines and counts, and power requirements
need to produce the items named in the dict at the mapped rate.
:param recipes:
:param required:
:return:
"""
rates = required_rates(recipes, required)
# Note if any particular item is (in aggregate) going to exceed the
# highest conveyor belt capacity.
for name, rate in rates.items():
print(name, rate, float(rate * 60))
if rate * 60 > conveyor_rates[-1]:
print("Rate exceeds max conveyor")
machines = required_machines(recipes, rates)
power = required_power(recipes, machines)
print(power, "MW")
print(math.ceil(power / 150), "fuel generators")
def main():
recipes = satisfactory.Cookbook('data/recipes')
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"item",
nargs="*",
type=str,
default=recipes.components(),
help="The name of an item to produce at full rate"
)
args = parser.parse_args()
# Create an initial name:rate request for all of the target items, then
# create a plan for their creation.
request = [{n: basic_rate(recipes[n].recipes[0]) for n in args.item}]
plan(recipes, request)
if __name__ == '__main__':
main()