Skip to content

Instantly share code, notes, and snippets.

@plammens
Created February 24, 2024 10:47
Show Gist options
  • Save plammens/55a2b93b7e171cee15a942821c319b91 to your computer and use it in GitHub Desktop.
Save plammens/55a2b93b7e171cee15a942821c319b91 to your computer and use it in GitHub Desktop.
Compute the minimal integral solution for producing in exact ratios in Factorio
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