Created
February 24, 2024 10:47
-
-
Save plammens/55a2b93b7e171cee15a942821c319b91 to your computer and use it in GitHub Desktop.
Compute the minimal integral solution for producing in exact ratios in Factorio
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import typing as t | |
from abc import ABCMeta, abstractmethod | |
from dataclasses import dataclass, field | |
from decimal import Decimal | |
from fractions import Fraction | |
from functools import cache | |
from math import lcm | |
from frozendict import frozendict | |
@dataclass(frozen=True) | |
class Resource(metaclass=ABCMeta): | |
name: str | |
def __str__(self): | |
return self.name | |
@abstractmethod | |
def compute_production_module(self) -> "ProductionModule": | |
pass | |
class InputResource(Resource): | |
def compute_production_module(self) -> "ProductionModule": | |
return InputModule(self) | |
@dataclass(frozen=True) | |
class Recipe(Resource): | |
amount_per_craft: int | |
seconds: Fraction | |
dependencies: t.Mapping["Recipe", int] = frozendict() | |
@property | |
@cache | |
def rate(self) -> Fraction: | |
"""Rate per machine per second.""" | |
return Fraction(self.amount_per_craft, self.seconds) | |
@property | |
@cache | |
def is_basic_resource(self): | |
return not self.dependencies | |
def compute_production_module(self) -> "CraftingModule": | |
subparts = {} | |
for item, amount_needed in self.dependencies.items(): | |
submodule = item.compute_production_module() | |
submodule, number_of_copies = submodule.scale( | |
rate_needed=amount_needed / self.seconds | |
) | |
subparts[submodule] = number_of_copies | |
# eliminate denominators | |
common_denominator = lcm(*(x.denominator for x in subparts.values())) | |
subparts = { | |
submodule: amount * common_denominator | |
for submodule, amount in subparts.items() | |
} | |
return CraftingModule( | |
resource=self, | |
number_of_output_machines=common_denominator, | |
subparts=frozendict(subparts), | |
) | |
def print_dependencies_transitively(self, *, _level=0): | |
if _level == 0: | |
print( | |
f"To produce {self.amount_per_craft}" | |
f" {pluralize(self.name, self.amount_per_craft)}, you need:" | |
) | |
print() | |
for item, amount in self.dependencies.items(): | |
print(_level * "\t" + f"{amount}x {item.name}") | |
item.print_dependencies_transitively(_level=_level + 1) | |
@dataclass(frozen=True) | |
class ProductionModule(metaclass=ABCMeta): | |
resource: "Resource" | |
rate: Fraction | |
@abstractmethod | |
def scale(self, rate_needed: Fraction) -> t.Tuple["ProductionModule", Fraction]: | |
"""Scale up this module by changing it and/or increasing the number of copies.""" | |
pass | |
@abstractmethod | |
def print(self, *, _level: int = 1): | |
pass | |
@dataclass(frozen=True) | |
class InputModule(ProductionModule): | |
rate: Fraction = Fraction(1) | |
def print(self, *, _level=1): | |
print(f"INPUT: {self.resource.name} ({self.rate} ups)") | |
def scale(self, rate_needed: Fraction) -> t.Tuple["ProductionModule", int]: | |
return InputModule(resource=self.resource, rate=rate_needed), 1 | |
@dataclass(frozen=True) | |
class CraftingModule(ProductionModule): | |
rate: Fraction = field(init=False) | |
resource: "Recipe" | |
number_of_output_machines: int | |
subparts: t.Mapping["CraftingModule", int] | |
def __post_init__(self): | |
object.__setattr__( | |
self, "rate", self.resource.rate * self.number_of_output_machines | |
) | |
def scale(self, rate_needed: Fraction) -> t.Tuple["ProductionModule", Fraction]: | |
return self, rate_needed / self.rate | |
def print(self, *, _level=1): | |
print(f"{self.resource.name} module ({self.rate} ups)") | |
print( | |
_level * "\t" + f"{self.number_of_output_machines}x {self.resource.name}" | |
f" {pluralize('machine', self.number_of_output_machines)}" | |
f" ({self.resource.rate} ups)" | |
) | |
for submodule, amount in self.subparts.items(): | |
print(_level * "\t" + f"{amount}x ", end="") | |
submodule.print(_level=_level + 1) | |
def pluralize(word: str, count) -> str: | |
"""Pluralize (or not) a word as appropriate given a count.""" | |
return word if abs(count) == 1 else f"{word}s" | |
def f(x: str) -> Fraction: | |
"""Shortcut to input a fraction literal.""" | |
return Fraction.from_decimal(Decimal(x)) | |
# examples | |
copper = InputResource("copper") | |
iron = InputResource("iron") | |
plastic = InputResource("plastic") | |
sulfuric_acid = InputResource("sulfuric acid") | |
copper_wire = Recipe( | |
"copper wire", | |
amount_per_craft=2, | |
seconds=f("0.5"), | |
dependencies=frozendict( | |
{ | |
copper: 1, | |
} | |
), | |
) | |
green_circuit = Recipe( | |
"green circuit", | |
amount_per_craft=1, | |
seconds=f("0.5"), | |
dependencies=frozendict( | |
{ | |
copper_wire: 3, | |
iron: 1, | |
} | |
), | |
) | |
red_circuit = Recipe( | |
"red circuit", | |
amount_per_craft=1, | |
seconds=f("6"), | |
dependencies=frozendict( | |
{ | |
copper_wire: 4, | |
plastic: 2, | |
green_circuit: 2, | |
} | |
), | |
) | |
processing_unit = Recipe( | |
"processing unit", | |
amount_per_craft=1, | |
seconds=f("10"), | |
dependencies=frozendict( | |
{ | |
red_circuit: 2, | |
green_circuit: 20, | |
sulfuric_acid: 5, | |
} | |
), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment